diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e8ce112 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + backend-tests: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r backend/requirements.txt + + - name: Run pytest + run: | + python -m pytest tests/backend -q + + frontend-tests: + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run Vitest + run: npm run test:run + diff --git a/.gitignore b/.gitignore index c3c2407..b62bbc1 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,6 @@ dist/ .env/ # personal -tests/ project-plans/ .DS_Store .cursorrules diff --git a/backend/main.py b/backend/main.py index b5d8a8e..6ea2d58 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,4 +1,5 @@ from fastapi import FastAPI, UploadFile, HTTPException, File, Depends +from starlette.datastructures import UploadFile as StarletteUploadFile from fastapi.middleware.cors import CORSMiddleware import uvicorn from services.xlsform_parser import XLSFormParser @@ -11,6 +12,7 @@ from typing import List import time import os from fastapi.responses import JSONResponse +from fastapi import Request import pandas as pd logging.basicConfig(level=logging.INFO) @@ -51,7 +53,7 @@ async def shutdown_event(): await close_mongo_connection() @app.post("/api/validate", response_model=FormValidation) -async def validate_file(file: UploadFile): +async def validate_file(file: UploadFile = File(...)): """ Validate the uploaded Excel file format """ @@ -74,12 +76,20 @@ async def validate_file(file: UploadFile): raise HTTPException(status_code=400, detail=str(e)) @app.post("/api/forms/parse") -async def parse_file(file: UploadFile): +async def parse_file(request: Request): """ Parse the uploaded Excel file and return JSON schema without saving to database """ - # Enhanced file validation - if not file.filename: + form = await request.form() + file_field = form.get('file') + filename = None + upload: UploadFile | None = None + if isinstance(file_field, (UploadFile, StarletteUploadFile)): + upload = file_field + filename = file_field.filename + elif isinstance(file_field, str): + filename = file_field + if not filename: raise HTTPException( status_code=400, detail={ @@ -93,13 +103,13 @@ async def parse_file(file: UploadFile): } ) - if not file.filename.endswith(('.xls', '.xlsx')): - file_extension = file.filename.split('.')[-1] if '.' in file.filename else 'unknown' + if not filename.endswith(('.xls', '.xlsx')): + file_extension = filename.split('.')[-1] if '.' in filename else 'unknown' raise HTTPException( status_code=400, detail={ "error": "Invalid file format", - "message": f"File '{file.filename}' has extension '.{file_extension}' but only Excel files (.xls, .xlsx) are supported.", + "message": f"File '{filename}' has extension '.{file_extension}' but only Excel files (.xls, .xlsx) are supported.", "error_type": "INVALID_FILE_FORMAT", "received_format": file_extension, "supported_formats": ["xls", "xlsx"], @@ -113,15 +123,24 @@ async def parse_file(file: UploadFile): # Enhanced file size validation try: - file_size = len(await file.read()) - await file.seek(0) # Reset file pointer + if not upload: + raise HTTPException( + status_code=400, + detail={ + "error": "Missing file", + "message": "No file content was uploaded.", + "error_type": "MISSING_FILE" + } + ) + file_size = len(await upload.read()) + await upload.seek(0) if file_size == 0: raise HTTPException( status_code=400, detail={ "error": "Empty file", - "message": f"The uploaded file '{file.filename}' is empty (0 bytes).", + "message": f"The uploaded file '{filename}' is empty (0 bytes).", "error_type": "EMPTY_FILE", "file_size": file_size, "suggestions": [ @@ -134,7 +153,7 @@ async def parse_file(file: UploadFile): # Warn about large files (>10MB) if file_size > 10 * 1024 * 1024: - logger.warning(f"Large file uploaded: {file.filename} ({file_size} bytes)") + logger.warning(f"Large file uploaded: {filename} ({file_size} bytes)") except Exception as e: logger.error(f"Error reading file size: {str(e)}") @@ -142,7 +161,7 @@ async def parse_file(file: UploadFile): status_code=400, detail={ "error": "File access error", - "message": f"Unable to read the uploaded file '{file.filename}'. The file may be corrupted or inaccessible.", + "message": f"Unable to read the uploaded file '{filename}'. The file may be corrupted or inaccessible.", "error_type": "FILE_ACCESS_ERROR", "suggestions": [ "Try uploading the file again", @@ -154,7 +173,7 @@ async def parse_file(file: UploadFile): try: parser = XLSFormParser() - result = await parser.parse_file_only(file) + result = await parser.parse_file_only(upload) # Check if validation failed if isinstance(result, dict) and result.get('valid') == False: @@ -165,7 +184,7 @@ async def parse_file(file: UploadFile): "error": "Validation failed", "message": result.get('message', 'File validation failed'), "error_type": "VALIDATION_ERROR", - "file_name": result.get('file_name', file.filename), + "file_name": result.get('file_name', filename), "errors": result.get('errors', []), "warnings": result.get('warnings', []), "suggestions": [ @@ -183,14 +202,14 @@ async def parse_file(file: UploadFile): raise except Exception as e: error_message = str(e) - logger.error(f"Error parsing file {file.filename}: {error_message}") + logger.error(f"Error parsing file {filename}: {error_message}") # Provide more specific error details based on the exception error_detail = { "error": "Parsing failed", - "message": f"Failed to parse Excel file '{file.filename}': {error_message}", + "message": f"Failed to parse Excel file '{filename}': {error_message}", "error_type": "PARSING_ERROR", - "file_name": file.filename, + "file_name": filename, "raw_error": error_message, "suggestions": [ "Check that the Excel file has the required sheets: 'Forms', 'Questions Info', 'Answer Options'", diff --git a/backend/requirements.txt b/backend/requirements.txt index 81fd395..57646bc 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -8,3 +8,6 @@ python-dotenv==1.0.0 motor==3.3.1 pymongo==4.5.0 xlrd==2.0.1 +pytest +pytest-asyncio +httpx diff --git a/backend/services/xlsform_parser.py b/backend/services/xlsform_parser.py index b36d066..a1c0bcc 100644 --- a/backend/services/xlsform_parser.py +++ b/backend/services/xlsform_parser.py @@ -871,7 +871,7 @@ class XLSFormParser: 'title': form_title, 'version': form_version, 'language': form_metadata.get('language', 'en'), - 'groups': [group.dict() for group in groups], + 'groups': [group.model_dump() for group in groups], 'settings': None, 'metadata': { 'questions_count': len(questions_data), diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ac3e8b2..be20cb9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,8 +32,11 @@ "@angular/cli": "^19.2.9", "@angular/compiler-cli": "^19.2.0", "@types/express": "^4.17.21", + "@types/jsdom": "^21.1.7", "@types/node": "^24.0.3", - "typescript": "~5.7.2" + "jsdom": "^25.0.0", + "typescript": "~5.7.2", + "vitest": "^2.0.0" } }, "node_modules/@ampproject/remapping": { @@ -832,6 +835,27 @@ } } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -2514,6 +2538,121 @@ "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", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "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", + "engines": { + "node": ">=18" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", @@ -4733,8 +4872,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.34.8", @@ -5068,6 +5206,18 @@ "@types/node": "*" } }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -5166,6 +5316,13 @@ "@types/node": "*" } }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -5189,6 +5346,119 @@ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -5602,6 +5872,23 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "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", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -5970,6 +6257,16 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cacache": { "version": "19.0.1", "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", @@ -6115,6 +6412,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -6139,6 +6453,16 @@ "dev": true, "license": "MIT" }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -6384,6 +6708,19 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -6713,6 +7050,41 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -6731,6 +7103,23 @@ } } }, + "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/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/default-browser": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", @@ -6787,6 +7176,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7099,6 +7498,22 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", @@ -7216,6 +7631,16 @@ "node": ">=4.0" } }, + "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/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -7252,6 +7677,16 @@ "node": ">=0.8.x" } }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", @@ -7561,6 +7996,23 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -7820,6 +8272,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -7898,6 +8366,19 @@ "safe-buffer": "~5.1.0" } }, + "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/htmlparser2": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", @@ -8358,6 +8839,13 @@ "node": ">=0.10.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/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -8532,6 +9020,47 @@ "dev": true, "license": "MIT" }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -8956,6 +9485,13 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -9812,6 +10348,13 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nwsapi": { + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", + "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "dev": true, + "license": "MIT" + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -10295,6 +10838,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -10556,6 +11116,16 @@ "license": "MIT", "optional": true }, + "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/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -10931,6 +11501,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, "node_modules/run-applescript": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", @@ -11073,6 +11650,19 @@ "license": "ISC", "optional": true }, + "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/schema-utils": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", @@ -11431,6 +12021,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -11721,6 +12318,13 @@ "node": "^18.17.0 || >=20.5.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/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -11730,6 +12334,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -11890,6 +12501,13 @@ "node": ">=0.10" } }, + "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/tapable": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", @@ -12062,6 +12680,20 @@ "dev": true, "license": "MIT" }, + "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": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -12079,6 +12711,56 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -12114,6 +12796,32 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tree-dump": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.3.tgz", @@ -12396,25 +13104,21 @@ } }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -12423,25 +13127,19 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", + "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" + "terser": "^5.4.0" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, - "jiti": { - "optional": true - }, "less": { "optional": true }, @@ -12462,15 +13160,423 @@ }, "terser": { "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true } } }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/vite/node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.44.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz", @@ -12483,8 +13589,7 @@ "optional": true, "os": [ "android" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-android-arm64": { "version": "4.44.1", @@ -12498,8 +13603,7 @@ "optional": true, "os": [ "android" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-darwin-arm64": { "version": "4.44.1", @@ -12513,8 +13617,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-darwin-x64": { "version": "4.44.1", @@ -12528,8 +13631,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.44.1", @@ -12543,8 +13645,7 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-freebsd-x64": { "version": "4.44.1", @@ -12558,8 +13659,7 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.44.1", @@ -12573,8 +13673,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.44.1", @@ -12588,8 +13687,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.44.1", @@ -12603,8 +13701,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.44.1", @@ -12618,8 +13715,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-loongarch64-gnu": { "version": "4.44.1", @@ -12633,8 +13729,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "version": "4.44.1", @@ -12648,8 +13743,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.44.1", @@ -12663,8 +13757,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.44.1", @@ -12678,8 +13771,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.44.1", @@ -12693,8 +13785,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.44.1", @@ -12708,8 +13799,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.44.1", @@ -12723,8 +13813,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.44.1", @@ -12738,8 +13827,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.44.1", @@ -12753,16 +13841,53 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, "license": "MIT", - "peer": true + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } }, "node_modules/vite/node_modules/postcss": { "version": "8.5.6", @@ -12784,7 +13909,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12800,7 +13924,6 @@ "integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -12835,6 +13958,85 @@ "fsevents": "~2.3.2" } }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "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/watchpack": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", @@ -12877,6 +14079,16 @@ "license": "MIT", "optional": true }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/webpack": { "version": "5.98.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", @@ -13190,6 +14402,56 @@ "node": ">=0.8.0" } }, + "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-encoding/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/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": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", @@ -13206,6 +14468,23 @@ "node": "^18.17.0 || >=20.5.0" } }, + "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/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", @@ -13388,6 +14667,23 @@ "node": ">= 6" } }, + "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/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2b9769c..713db0e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,9 @@ "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", - "serve:ssr:mform-upload": "node --no-deprecation dist/mform-upload/server/server.mjs" + "serve:ssr:mform-upload": "node --no-deprecation dist/mform-upload/server/server.mjs", + "test": "vitest", + "test:run": "vitest run" }, "private": true, "dependencies": { @@ -35,6 +37,9 @@ "@angular/compiler-cli": "^19.2.0", "@types/express": "^4.17.21", "@types/node": "^24.0.3", - "typescript": "~5.7.2" + "typescript": "~5.7.2", + "vitest": "^2.0.0", + "jsdom": "^25.0.0", + "@types/jsdom": "^21.1.7" } } diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..d3bfb4a --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vitest/config' +import { fileURLToPath } from 'node:url' +import { dirname, resolve } from 'node:path' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) +const repoRoot = resolve(__dirname, '..') + +export default defineConfig({ + server: { + fs: { + allow: ['..'] + } + }, + test: { + environment: 'jsdom', + include: [resolve(repoRoot, 'tests/frontend/**/*.spec.ts')], + globals: true + } +}) + diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..90243d4 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,7 @@ +[pytest] +filterwarnings = + ignore::DeprecationWarning:fastapi\.openapi\.models + ignore::DeprecationWarning:fastapi\.datastructures + ignore::DeprecationWarning:pydantic_core\.core_schema + ignore:The 'app' shortcut is now deprecated.*:DeprecationWarning:httpx\._client + ignore:datetime\.datetime\.utcnow.*:DeprecationWarning:openpyxl\.packaging\.core diff --git a/tests/backend/conftest.py b/tests/backend/conftest.py new file mode 100644 index 0000000..ffc4675 --- /dev/null +++ b/tests/backend/conftest.py @@ -0,0 +1,46 @@ +import os +import sys +import pytest +import pytest_asyncio +import httpx + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'backend')) +import main + + +@pytest_asyncio.fixture +async def client(): + transport = httpx.ASGITransport(app=main.app) + async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as c: + yield c + + +@pytest_asyncio.fixture(autouse=True) +async def mock_db_services(monkeypatch): + """Mock database services to avoid requiring MongoDB""" + + # Mock database connection functions + async def mock_connect_to_mongo(): + pass + + async def mock_close_mongo_connection(): + pass + + monkeypatch.setattr(main, 'connect_to_mongo', mock_connect_to_mongo) + monkeypatch.setattr(main, 'close_mongo_connection', mock_close_mongo_connection) + + # Mock database service + class MockDatabaseService: + async def get_all_forms(self): + return [] + + async def get_form_by_id(self, form_id: str): + return None # Simulate form not found + + async def get_questions_by_form_id(self, form_id: str): + return [] + + async def get_options_by_form_id(self, form_id: str): + return [] + + monkeypatch.setattr(main, 'db_service', MockDatabaseService()) diff --git a/tests/backend/test_api.py b/tests/backend/test_api.py new file mode 100644 index 0000000..48fbbce --- /dev/null +++ b/tests/backend/test_api.py @@ -0,0 +1,97 @@ +import os +import sys +import io +import pytest +import httpx + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'backend')) +import main + + +@pytest.mark.asyncio +async def test_validate_invalid_extension(client: httpx.AsyncClient): + """Test validation endpoint with invalid file extension""" + files = {'file': ('bad.txt', b'data', 'text/plain')} + resp = await client.post('/api/validate', files=files) + assert resp.status_code == 200 + body = resp.json() + assert body['valid'] is False + assert 'Only .xls/.xlsx' in body['message'] + + +@pytest.mark.asyncio +async def test_parse_missing_file_name(client: httpx.AsyncClient): + """Test parse endpoint with no file uploaded""" + resp = await client.post('/api/forms/parse', data={}) + assert resp.status_code == 400 + detail = resp.json()['detail'] + assert detail['error_type'] == 'MISSING_FILE' + + +@pytest.mark.asyncio +async def test_parse_invalid_extension(client: httpx.AsyncClient): + """Test parse endpoint with invalid file extension""" + files = {'file': ('bad.txt', io.BytesIO(b'data'), 'text/plain')} + resp = await client.post('/api/forms/parse', files=files) + assert resp.status_code == 400 + detail = resp.json()['detail'] + assert detail['error_type'] == 'INVALID_FILE_FORMAT' + + +@pytest.mark.asyncio +async def test_get_all_forms_empty(client: httpx.AsyncClient): + """Test getting all forms when database is empty (mocked)""" + resp = await client.get('/api/forms') + assert resp.status_code == 200 + body = resp.json() + assert 'forms' in body + assert 'count' in body + assert body['count'] == 0 + assert body['forms'] == [] + + +@pytest.mark.asyncio +async def test_get_form_by_id_not_found(client: httpx.AsyncClient): + """Test getting a form by ID when form doesn't exist (mocked)""" + resp = await client.get('/api/forms/507f1f77bcf86cd799439011') # Valid ObjectId format + assert resp.status_code == 404 + + +@pytest.mark.asyncio +async def test_parse_with_valid_excel_file(client: httpx.AsyncClient): + """Test parsing a valid Excel file using local test files""" + test_file_path = os.path.join(os.path.dirname(__file__), '..', 'test_xlsforms_valid', 'valid_form_1.xlsx') + + if os.path.exists(test_file_path): + with open(test_file_path, 'rb') as f: + files = {'file': ('valid_form_1.xlsx', f, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')} + resp = await client.post('/api/forms/parse', files=files) + # Should return parsed form data without saving to DB + assert resp.status_code == 200 + body = resp.json() + assert 'title' in body + assert 'groups' in body + assert 'metadata' in body + else: + # Skip test if no test file available + pytest.skip("Test Excel file not found") + + +@pytest.mark.asyncio +async def test_validate_with_valid_excel_file(client: httpx.AsyncClient): + """Test validation with a valid Excel file using local test files""" + test_file_path = os.path.join(os.path.dirname(__file__), '..', 'test_xlsforms_valid', 'valid_form_1.xlsx') + + if os.path.exists(test_file_path): + with open(test_file_path, 'rb') as f: + files = {'file': ('valid_form_1.xlsx', f, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')} + resp = await client.post('/api/validate', files=files) + assert resp.status_code == 200 + body = resp.json() + assert 'valid' in body + assert 'message' in body + assert 'sheets' in body + else: + # Skip test if no test file available + pytest.skip("Test Excel file not found") + diff --git a/tests/codeshares b/tests/codeshares new file mode 100644 index 0000000..dee8013 --- /dev/null +++ b/tests/codeshares @@ -0,0 +1,5 @@ +https://codeshare.io/5vwmgl +https://codeshare.io/5eVpNA +https://codeshare.io/GLwbe7 +https://codeshare.io/ar9xeq +https://codeshare.io/GAdOBE diff --git a/tests/frontend/form.service.spec.ts b/tests/frontend/form.service.spec.ts new file mode 100644 index 0000000..08cc8c7 --- /dev/null +++ b/tests/frontend/form.service.spec.ts @@ -0,0 +1,59 @@ +import type { HttpClient } from '@angular/common/http' +import { describe, it, expect, vi } from 'vitest' + +function of(value: T) { + return { + subscribe: (next?: (v: T) => void) => { + if (typeof next === 'function') next(value) + return { unsubscribe() {} } + } + } as any +} +import { FormService } from '../../frontend/src/app/services/form.service' + +class HttpClientStub { + post(url: string, body: any) { + return of({} as T) + } + get(url: string) { + return of({} as T) + } + delete(url: string) { + return of({} as T) + } + put(url: string, body: any) { + return of({} as T) + } +} + +describe('FormService', () => { + let service: FormService + + beforeEach(() => { + service = new FormService(new HttpClientStub() as unknown as HttpClient) + }) + + it('should call validate endpoint', async () => { + const file = new Blob(['a']) as any + const spy = vi.spyOn(HttpClientStub.prototype, 'post') + service.validateFile(file).subscribe() + expect(spy).toHaveBeenCalled() + expect(spy.mock.calls[0][0]).toContain('/api/validate') + }) + + it('should call parse endpoint', async () => { + const file = new Blob(['a']) as any + const spy = vi.spyOn(HttpClientStub.prototype, 'post') + service.parseFile(file).subscribe() + expect(spy).toHaveBeenCalled() + expect(spy.mock.calls[0][0]).toContain('/api/forms/parse') + }) + + it('should call getAllForms endpoint', async () => { + const spy = vi.spyOn(HttpClientStub.prototype, 'get') + service.getAllForms().subscribe() + expect(spy).toHaveBeenCalled() + expect(spy.mock.calls[0][0]).toContain('/api/forms') + }) +}) + diff --git a/tests/make-valid.py b/tests/make-valid.py new file mode 100644 index 0000000..350bcb2 --- /dev/null +++ b/tests/make-valid.py @@ -0,0 +1,80 @@ +import pandas as pd +import os +from faker import Faker +import random + +fake = Faker() +output_dir = "test_xlsforms_valid" # Changed directory name +os.makedirs(output_dir, exist_ok=True) + +VALID_INPUT_TYPES = [1, 2, 3, 4, 5, 6, 7] # Example: 1=Text, 2=Select One, 3=Select Many, etc. + +def generate_valid_xlsform(file_name_prefix, num_questions, num_options_per_question): + forms_data = { + "Language": [random.choice(["en", "fr", "es", "de"])], + "Title": [fake.catch_phrase() + " Form"] + } + forms_df = pd.DataFrame(forms_data) + + questions_data = [] + for i in range(num_questions): + question_order = i + 1 + question_type = random.choice(VALID_INPUT_TYPES) + questions_data.append({ + "Order": question_order, + "Title": fake.sentence(nb_words=6), + "View Sequence": i + 1, + "Input Type": question_type, + "Optional Column 1": fake.word(), # Add some optional columns + "Optional Column 2": fake.random_int(min=1, max=100) + }) + questions_df = pd.DataFrame(questions_data) + + options_data = [] + for question_idx, question in questions_df.iterrows(): + if question["Input Type"] in [2, 3]: # Select One or Select Many + # Generate 3-10 options per question + num_options_for_question = random.randint(3, 10) + for i in range(num_options_for_question): + # Generate a more robust label that won't be empty + label = fake.catch_phrase().split()[0].capitalize() + if not label or len(label) < 2: + label = f"Option{i+1}" + + options_data.append({ + "Order": question["Order"], + "Id": i + 1, + "Label": label + }) + options_df = pd.DataFrame(options_data) + + # Validate that all labels are non-empty + if not options_df.empty: + # Replace any empty or null labels with fallback values + options_df['Label'] = options_df['Label'].fillna('Default Option') + options_df.loc[options_df['Label'] == '', 'Label'] = 'Default Option' + options_df.loc[options_df['Label'].str.len() < 2, 'Label'] = 'Default Option' + + file_path = os.path.join(output_dir, f"{file_name_prefix}.xlsx") + with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_df.to_excel(writer, sheet_name="Forms", index=False) + questions_df.to_excel(writer, sheet_name="Questions Info", index=False) + options_df.to_excel(writer, sheet_name="Answer Options", index=False) + + return file_path + +# Generate 9 forms with ~400 questions each +num_files_to_generate = 100 +generated_files = [] + +for i in range(num_files_to_generate): + file_name = f"valid_form_{i+1}" + # Generate approximately 400 questions per form (with some variation) + num_questions = random.randint(380, 420) # ~400 questions with ±20 variation + + path = generate_valid_xlsform(file_name, num_questions, 10) # Max 10 options, actual will be 3-10 + generated_files.append(path) + print(f"Generated form {i+1}: {num_questions} questions") + +print(f"\nSuccessfully generated {len(generated_files)} valid test files in '{output_dir}'") +print("Each form contains approximately 400 questions with 3-10 options per select question.") diff --git a/tests/make.py b/tests/make.py new file mode 100644 index 0000000..7922405 --- /dev/null +++ b/tests/make.py @@ -0,0 +1,273 @@ +import pandas as pd +import os +from faker import Faker +import random + +fake = Faker() +output_dir = "test_xlsforms_valid" +os.makedirs(output_dir, exist_ok=True) + +forms_df = pd.DataFrame([{ + "Language": "en", + "Title": "Test Form Missing Forms Sheet" +}]) + +questions_df = pd.DataFrame([{ + "Order": 1, + "Title": "Sample Question", + "View Sequence": 1, + "Input Type": 1 +}]) + +options_df = pd.DataFrame([{ + "Order": 1, + "Id": 1, + "Label": "Option 1" +}]) + +file_path = os.path.join(output_dir, "missing_forms_sheet.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + # Skip the Forms sheet intentionally + questions_df.to_excel(writer, sheet_name="Questions Info", index=False) + options_df.to_excel(writer, sheet_name="Answer Options", index=False) + +file_path = os.path.join(output_dir, "missing_questions_sheet.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_df.to_excel(writer, sheet_name="Forms", index=False) + # Skip the Questions Info sheet intentionally + options_df.to_excel(writer, sheet_name="Answer Options", index=False) + +file_path = os.path.join(output_dir, "missing_options_sheet.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_df.to_excel(writer, sheet_name="Forms", index=False) + questions_df.to_excel(writer, sheet_name="Questions Info", index=False) + # Skip the Answer Options sheet intentionally + +forms_missing_lang = pd.DataFrame([{ + "Title": "Form with missing Language column" +}]) + +file_path = os.path.join(output_dir, "missing_language_column.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_missing_lang.to_excel(writer, sheet_name="Forms", index=False) + questions_df.to_excel(writer, sheet_name="Questions Info", index=False) + options_df.to_excel(writer, sheet_name="Answer Options", index=False) + +forms_missing_title = pd.DataFrame([{ + "Language": "en" +}]) + +file_path = os.path.join(output_dir, "missing_title_column.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_missing_title.to_excel(writer, sheet_name="Forms", index=False) + questions_df.to_excel(writer, sheet_name="Questions Info", index=False) + options_df.to_excel(writer, sheet_name="Answer Options", index=False) + +questions_missing_order = pd.DataFrame([{ + "Title": "Question without Order", + "View Sequence": 1, + "Input Type": 1 +}]) + +file_path = os.path.join(output_dir, "missing_order_column.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_df.to_excel(writer, sheet_name="Forms", index=False) + questions_missing_order.to_excel(writer, sheet_name="Questions Info", index=False) + options_df.to_excel(writer, sheet_name="Answer Options", index=False) + +questions_invalid_types = pd.DataFrame([ + {"Order": 1, "Title": "Valid Question", "View Sequence": 1, "Input Type": 1}, + {"Order": 2, "Title": "Invalid Type 99", "View Sequence": 2, "Input Type": 99}, + {"Order": 3, "Title": "Invalid Type Text", "View Sequence": 3, "Input Type": "text"}, + {"Order": 4, "Title": "Invalid Type -1", "View Sequence": 4, "Input Type": -1}, +]) + +file_path = os.path.join(output_dir, "invalid_question_types.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_df.to_excel(writer, sheet_name="Forms", index=False) + questions_invalid_types.to_excel(writer, sheet_name="Questions Info", index=False) + options_df.to_excel(writer, sheet_name="Answer Options", index=False) + +questions_invalid_order = pd.DataFrame([ + {"Order": "first", "Title": "Question with text order", "View Sequence": 1, "Input Type": 1}, + {"Order": 2.5, "Title": "Question with decimal order", "View Sequence": 2, "Input Type": 1}, + {"Order": -1, "Title": "Question with negative order", "View Sequence": 3, "Input Type": 1}, + {"Order": 0, "Title": "Question with zero order", "View Sequence": 4, "Input Type": 1}, +]) + +file_path = os.path.join(output_dir, "invalid_order_values.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_df.to_excel(writer, sheet_name="Forms", index=False) + questions_invalid_order.to_excel(writer, sheet_name="Questions Info", index=False) + options_df.to_excel(writer, sheet_name="Answer Options", index=False) + +questions_duplicate_order = pd.DataFrame([ + {"Order": 1, "Title": "First question", "View Sequence": 1, "Input Type": 1}, + {"Order": 1, "Title": "Duplicate order question", "View Sequence": 2, "Input Type": 2}, + {"Order": 2, "Title": "Third question", "View Sequence": 3, "Input Type": 1}, +]) + +file_path = os.path.join(output_dir, "duplicate_question_order.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_df.to_excel(writer, sheet_name="Forms", index=False) + questions_duplicate_order.to_excel(writer, sheet_name="Questions Info", index=False) + options_df.to_excel(writer, sheet_name="Answer Options", index=False) + +options_duplicate = pd.DataFrame([ + {"Order": 1, "Id": 1, "Label": "Option 1"}, + {"Order": 1, "Id": 1, "Label": "Duplicate Option 1"}, # Duplicate combination + {"Order": 1, "Id": 2, "Label": "Option 2"}, + {"Order": 2, "Id": 1, "Label": "Question 2 Option 1"}, +]) + +file_path = os.path.join(output_dir, "duplicate_option_ids.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_df.to_excel(writer, sheet_name="Forms", index=False) + questions_df.to_excel(writer, sheet_name="Questions Info", index=False) + options_duplicate.to_excel(writer, sheet_name="Answer Options", index=False) + +forms_empty = pd.DataFrame([{ + "Language": "", + "Title": "" +}]) + +file_path = os.path.join(output_dir, "empty_form_values.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_empty.to_excel(writer, sheet_name="Forms", index=False) + questions_df.to_excel(writer, sheet_name="Questions Info", index=False) + options_df.to_excel(writer, sheet_name="Answer Options", index=False) + +questions_missing_titles = pd.DataFrame([ + {"Order": 1, "Title": "Valid question", "View Sequence": 1, "Input Type": 1}, + {"Order": 2, "Title": "", "View Sequence": 2, "Input Type": 2}, + {"Order": 3, "Title": None, "View Sequence": 3, "Input Type": 1}, +]) + +file_path = os.path.join(output_dir, "missing_question_titles.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_df.to_excel(writer, sheet_name="Forms", index=False) + questions_missing_titles.to_excel(writer, sheet_name="Questions Info", index=False) + options_df.to_excel(writer, sheet_name="Answer Options", index=False) + +options_missing_labels = pd.DataFrame([ + {"Order": 1, "Id": 1, "Label": "Valid option"}, + {"Order": 1, "Id": 2, "Label": ""}, + {"Order": 1, "Id": 3, "Label": None}, +]) + +file_path = os.path.join(output_dir, "missing_option_labels.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_df.to_excel(writer, sheet_name="Forms", index=False) + questions_df.to_excel(writer, sheet_name="Questions Info", index=False) + options_missing_labels.to_excel(writer, sheet_name="Answer Options", index=False) + +questions_choice_no_options = pd.DataFrame([ + {"Order": 1, "Title": "Text question", "View Sequence": 1, "Input Type": 1}, + {"Order": 2, "Title": "Select one without options", "View Sequence": 2, "Input Type": 2}, + {"Order": 3, "Title": "Select multiple without options", "View Sequence": 3, "Input Type": 3}, +]) + +options_limited = pd.DataFrame([ + {"Order": 1, "Id": 1, "Label": "Option for text question (shouldn't be here)"} +]) + +file_path = os.path.join(output_dir, "choice_questions_no_options.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_df.to_excel(writer, sheet_name="Forms", index=False) + questions_choice_no_options.to_excel(writer, sheet_name="Questions Info", index=False) + options_limited.to_excel(writer, sheet_name="Answer Options", index=False) + +questions_limited = pd.DataFrame([ + {"Order": 1, "Title": "Only question", "View Sequence": 1, "Input Type": 1}, +]) + +options_orphaned = pd.DataFrame([ + {"Order": 1, "Id": 1, "Label": "Option for existing question"}, + {"Order": 2, "Id": 1, "Label": "Option for non-existent question"}, + {"Order": 3, "Id": 1, "Label": "Another orphaned option"}, +]) + +file_path = os.path.join(output_dir, "orphaned_options.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_df.to_excel(writer, sheet_name="Forms", index=False) + questions_limited.to_excel(writer, sheet_name="Questions Info", index=False) + options_orphaned.to_excel(writer, sheet_name="Answer Options", index=False) + +forms_invalid_lang = pd.DataFrame([ + {"Language": "invalid_lang", "Title": "Form with invalid language code"} +]) + +file_path = os.path.join(output_dir, "invalid_language_code.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_invalid_lang.to_excel(writer, sheet_name="Forms", index=False) + questions_df.to_excel(writer, sheet_name="Questions Info", index=False) + options_df.to_excel(writer, sheet_name="Answer Options", index=False) + +forms_long_title = pd.DataFrame([{ + "Language": "en", + "Title": "A" * 300 # Very long title (over 255 chars) +}]) + +questions_long_title = pd.DataFrame([ + {"Order": 1, "Title": "B" * 1200, "View Sequence": 1, "Input Type": 1}, # Very long question +]) + +options_long_label = pd.DataFrame([ + {"Order": 1, "Id": 1, "Label": "C" * 600}, # Very long option label +]) + +file_path = os.path.join(output_dir, "very_long_values.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_long_title.to_excel(writer, sheet_name="Forms", index=False) + questions_long_title.to_excel(writer, sheet_name="Questions Info", index=False) + options_long_label.to_excel(writer, sheet_name="Answer Options", index=False) + +forms_multiple_issues = pd.DataFrame([ + {"Language": "", "Title": ""}, # Missing both required values + {"Language": "fr", "Title": "Extra row (should cause warning)"} # Extra row +]) + +questions_multiple_issues = pd.DataFrame([ + {"Order": "bad", "Title": "", "View Sequence": -1, "Input Type": 99}, # Multiple errors + {"Order": 1, "Title": "Valid question", "View Sequence": 1, "Input Type": 2}, # Choice question + {"Order": 1, "Title": "Duplicate order", "View Sequence": 2, "Input Type": 1}, # Duplicate order +]) + +options_multiple_issues = pd.DataFrame([ + {"Order": "bad", "Id": "bad", "Label": ""}, # All bad values + {"Order": 2, "Id": 1, "Label": "Option for non-choice question"}, # Orphaned +]) + +file_path = os.path.join(output_dir, "multiple_issues_complex.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + forms_multiple_issues.to_excel(writer, sheet_name="Forms", index=False) + questions_multiple_issues.to_excel(writer, sheet_name="Questions Info", index=False) + options_multiple_issues.to_excel(writer, sheet_name="Answer Options", index=False) + +empty_df = pd.DataFrame() + +file_path = os.path.join(output_dir, "empty_sheets.xlsx") +with pd.ExcelWriter(file_path, engine="openpyxl") as writer: + empty_df.to_excel(writer, sheet_name="Forms", index=False) + empty_df.to_excel(writer, sheet_name="Questions Info", index=False) + empty_df.to_excel(writer, sheet_name="Answer Options", index=False) + +with open(os.path.join(output_dir, "fake_excel.xlsx"), 'w') as f: + f.write("This is not an Excel file") + +csv_content = "Language,Title\nen,Fake Excel File" +with open(os.path.join(output_dir, "csv_as_xlsx.xlsx"), 'w') as f: + f.write(csv_content) + +print(f"\nSuccessfully generated {len(os.listdir(output_dir))} incorrect test files in '{output_dir}'") +print("\nTest file categories created:") +print("• Missing sheets (3 files)") +print("• Missing columns (3 files)") +print("• Invalid data types (2 files)") +print("• Duplicate values (2 files)") +print("• Missing required values (3 files)") +print("• Cross-reference issues (2 files)") +print("• Language/format issues (2 files)") +print("• Complex edge cases (2 files)") +print("• Invalid file formats (2 files)") + diff --git a/tests/test_validation.py b/tests/test_validation.py new file mode 100644 index 0000000..15f7e90 --- /dev/null +++ b/tests/test_validation.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +""" +Test script to demonstrate the enhanced validation system +Usage: python test_validation.py +""" + +import asyncio +import sys +import os +import io +import json + +# Add backend to path +backend_path = os.path.join(os.path.dirname(__file__), '..', 'backend') +sys.path.insert(0, backend_path) + +try: + from fastapi import UploadFile + from services.xlsform_parser import XLSFormParser +except ImportError as e: + print(f"❌ Import error: {e}") + print("Make sure you're running this from the tests directory and have the required dependencies installed.") + sys.exit(1) + +async def test_validation_file(file_path: str): + """Test validation for a single file""" + print(f"\n🧪 Testing: {os.path.basename(file_path)}") + print("=" * 60) + + try: + # Read the file + with open(file_path, 'rb') as f: + file_content = f.read() + + # Create UploadFile object + upload_file = UploadFile( + filename=os.path.basename(file_path), + file=io.BytesIO(file_content) + ) + + # Initialize parser and validate + parser = XLSFormParser() + result = await parser.validate_file(upload_file) + + # Display results + print(f"✅ Valid: {result['valid']}") + print(f"📝 Message: {result['message']}") + + if result.get('errors'): + print(f"\n❌ Errors ({len(result['errors'])}):") + for error in result['errors']: + location = f"{error['location']}" + if error.get('row'): + location += f" (Row {error['row']})" + if error.get('column'): + location += f" (Column: {error['column']})" + print(f" • {error['type']}: {error['message']} [{location}]") + + if result.get('warnings'): + print(f"\n⚠️ Warnings ({len(result['warnings'])}):") + for warning in result['warnings']: + location = f"{warning['location']}" + if warning.get('row'): + location += f" (Row {warning['row']})" + if warning.get('column'): + location += f" (Column: {warning['column']})" + print(f" • {warning['type']}: {warning['message']} [{location}]") + + # Sheet validation details + if result.get('sheets'): + print(f"\n📊 Sheet Validation:") + for sheet in result['sheets']: + status = "✅" if sheet['exists'] and not sheet['missing_columns'] else "❌" + print(f" {status} {sheet['name']}: {sheet['row_count']} rows") + if sheet['missing_columns']: + print(f" Missing columns: {', '.join(sheet['missing_columns'])}") + + return result['valid'] + + except Exception as e: + print(f"❌ Error testing file: {str(e)}") + return False + +async def main(): + """Main test function""" + print("🚀 Enhanced XLSForm Validation Test Suite") + print("=" * 60) + + # Test directory + test_dir = os.path.join(os.path.dirname(__file__), 'test_xlsforms_incorrect') + + if not os.path.exists(test_dir): + print(f"❌ Test directory not found: {test_dir}") + print("Please run 'python make.py' first to generate test files.") + return + + # Get all test files + test_files = [f for f in os.listdir(test_dir) if f.endswith('.xlsx')] + + if not test_files: + print(f"❌ No test files found in {test_dir}") + return + + print(f"📁 Found {len(test_files)} test files") + + # Test each file + valid_count = 0 + total_count = len(test_files) + + for test_file in sorted(test_files): + file_path = os.path.join(test_dir, test_file) + is_valid = await test_validation_file(file_path) + if is_valid: + valid_count += 1 + + # Summary + print("\n" + "=" * 60) + print("📈 Test Summary") + print("=" * 60) + print(f"Total files tested: {total_count}") + print(f"Valid files: {valid_count}") + print(f"Invalid files: {total_count - valid_count}") + print(f"Success rate: {(total_count - valid_count) / total_count * 100:.1f}% (lower is better for error testing)") + + if valid_count > 0: + print(f"\n⚠️ Warning: {valid_count} files passed validation when they should have failed!") + print("This may indicate issues with the validation logic.") + else: + print(f"\n🎉 Excellent! All test files correctly failed validation.") + print("The enhanced validation system is working properly!") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/test_xlsforms_incorrect/choice_questions_no_options.xlsx b/tests/test_xlsforms_incorrect/choice_questions_no_options.xlsx new file mode 100644 index 0000000..54e5577 Binary files /dev/null and b/tests/test_xlsforms_incorrect/choice_questions_no_options.xlsx differ diff --git a/tests/test_xlsforms_incorrect/csv_as_xlsx.xlsx b/tests/test_xlsforms_incorrect/csv_as_xlsx.xlsx new file mode 100644 index 0000000..63d14f0 --- /dev/null +++ b/tests/test_xlsforms_incorrect/csv_as_xlsx.xlsx @@ -0,0 +1,2 @@ +Language,Title +en,Fake Excel File \ No newline at end of file diff --git a/tests/test_xlsforms_incorrect/duplicate_option_ids.xlsx b/tests/test_xlsforms_incorrect/duplicate_option_ids.xlsx new file mode 100644 index 0000000..a6ce416 Binary files /dev/null and b/tests/test_xlsforms_incorrect/duplicate_option_ids.xlsx differ diff --git a/tests/test_xlsforms_incorrect/duplicate_question_order.xlsx b/tests/test_xlsforms_incorrect/duplicate_question_order.xlsx new file mode 100644 index 0000000..afc59a6 Binary files /dev/null and b/tests/test_xlsforms_incorrect/duplicate_question_order.xlsx differ diff --git a/tests/test_xlsforms_incorrect/empty_form_values.xlsx b/tests/test_xlsforms_incorrect/empty_form_values.xlsx new file mode 100644 index 0000000..5bbd152 Binary files /dev/null and b/tests/test_xlsforms_incorrect/empty_form_values.xlsx differ diff --git a/tests/test_xlsforms_incorrect/empty_sheets.xlsx b/tests/test_xlsforms_incorrect/empty_sheets.xlsx new file mode 100644 index 0000000..efafb11 Binary files /dev/null and b/tests/test_xlsforms_incorrect/empty_sheets.xlsx differ diff --git a/tests/test_xlsforms_incorrect/fake_excel.xlsx b/tests/test_xlsforms_incorrect/fake_excel.xlsx new file mode 100644 index 0000000..69f990d --- /dev/null +++ b/tests/test_xlsforms_incorrect/fake_excel.xlsx @@ -0,0 +1 @@ +This is not an Excel file \ No newline at end of file diff --git a/tests/test_xlsforms_incorrect/invalid_language_code.xlsx b/tests/test_xlsforms_incorrect/invalid_language_code.xlsx new file mode 100644 index 0000000..c105644 Binary files /dev/null and b/tests/test_xlsforms_incorrect/invalid_language_code.xlsx differ diff --git a/tests/test_xlsforms_incorrect/invalid_order_values.xlsx b/tests/test_xlsforms_incorrect/invalid_order_values.xlsx new file mode 100644 index 0000000..e45b46b Binary files /dev/null and b/tests/test_xlsforms_incorrect/invalid_order_values.xlsx differ diff --git a/tests/test_xlsforms_incorrect/invalid_question_types.xlsx b/tests/test_xlsforms_incorrect/invalid_question_types.xlsx new file mode 100644 index 0000000..2775b53 Binary files /dev/null and b/tests/test_xlsforms_incorrect/invalid_question_types.xlsx differ diff --git a/tests/test_xlsforms_incorrect/missing_forms_sheet.xlsx b/tests/test_xlsforms_incorrect/missing_forms_sheet.xlsx new file mode 100644 index 0000000..37d3a8f Binary files /dev/null and b/tests/test_xlsforms_incorrect/missing_forms_sheet.xlsx differ diff --git a/tests/test_xlsforms_incorrect/missing_language_column.xlsx b/tests/test_xlsforms_incorrect/missing_language_column.xlsx new file mode 100644 index 0000000..3bc9522 Binary files /dev/null and b/tests/test_xlsforms_incorrect/missing_language_column.xlsx differ diff --git a/tests/test_xlsforms_incorrect/missing_option_labels.xlsx b/tests/test_xlsforms_incorrect/missing_option_labels.xlsx new file mode 100644 index 0000000..ce88417 Binary files /dev/null and b/tests/test_xlsforms_incorrect/missing_option_labels.xlsx differ diff --git a/tests/test_xlsforms_incorrect/missing_options_sheet.xlsx b/tests/test_xlsforms_incorrect/missing_options_sheet.xlsx new file mode 100644 index 0000000..1579f99 Binary files /dev/null and b/tests/test_xlsforms_incorrect/missing_options_sheet.xlsx differ diff --git a/tests/test_xlsforms_incorrect/missing_order_column.xlsx b/tests/test_xlsforms_incorrect/missing_order_column.xlsx new file mode 100644 index 0000000..46354bb Binary files /dev/null and b/tests/test_xlsforms_incorrect/missing_order_column.xlsx differ diff --git a/tests/test_xlsforms_incorrect/missing_question_titles.xlsx b/tests/test_xlsforms_incorrect/missing_question_titles.xlsx new file mode 100644 index 0000000..17223f6 Binary files /dev/null and b/tests/test_xlsforms_incorrect/missing_question_titles.xlsx differ diff --git a/tests/test_xlsforms_incorrect/missing_questions_sheet.xlsx b/tests/test_xlsforms_incorrect/missing_questions_sheet.xlsx new file mode 100644 index 0000000..be1d37e Binary files /dev/null and b/tests/test_xlsforms_incorrect/missing_questions_sheet.xlsx differ diff --git a/tests/test_xlsforms_incorrect/missing_title_column.xlsx b/tests/test_xlsforms_incorrect/missing_title_column.xlsx new file mode 100644 index 0000000..8a5d19b Binary files /dev/null and b/tests/test_xlsforms_incorrect/missing_title_column.xlsx differ diff --git a/tests/test_xlsforms_incorrect/multiple_issues_complex.xlsx b/tests/test_xlsforms_incorrect/multiple_issues_complex.xlsx new file mode 100644 index 0000000..a1b4a6a Binary files /dev/null and b/tests/test_xlsforms_incorrect/multiple_issues_complex.xlsx differ diff --git a/tests/test_xlsforms_incorrect/orphaned_options.xlsx b/tests/test_xlsforms_incorrect/orphaned_options.xlsx new file mode 100644 index 0000000..fe4955f Binary files /dev/null and b/tests/test_xlsforms_incorrect/orphaned_options.xlsx differ diff --git a/tests/test_xlsforms_incorrect/very_long_values.xlsx b/tests/test_xlsforms_incorrect/very_long_values.xlsx new file mode 100644 index 0000000..987e6dc Binary files /dev/null and b/tests/test_xlsforms_incorrect/very_long_values.xlsx differ diff --git a/tests/test_xlsforms_valid/valid_form_1.xlsx b/tests/test_xlsforms_valid/valid_form_1.xlsx new file mode 100644 index 0000000..b1f7d94 Binary files /dev/null and b/tests/test_xlsforms_valid/valid_form_1.xlsx differ diff --git a/tests/test_xlsforms_valid/valid_form_10.xlsx b/tests/test_xlsforms_valid/valid_form_10.xlsx new file mode 100644 index 0000000..be703a4 Binary files /dev/null and b/tests/test_xlsforms_valid/valid_form_10.xlsx differ diff --git a/tests/test_xlsforms_valid/valid_form_2.xlsx b/tests/test_xlsforms_valid/valid_form_2.xlsx new file mode 100644 index 0000000..9aef979 Binary files /dev/null and b/tests/test_xlsforms_valid/valid_form_2.xlsx differ diff --git a/tests/test_xlsforms_valid/valid_form_3.xlsx b/tests/test_xlsforms_valid/valid_form_3.xlsx new file mode 100644 index 0000000..d37c970 Binary files /dev/null and b/tests/test_xlsforms_valid/valid_form_3.xlsx differ diff --git a/tests/test_xlsforms_valid/valid_form_4.xlsx b/tests/test_xlsforms_valid/valid_form_4.xlsx new file mode 100644 index 0000000..3b41fb6 Binary files /dev/null and b/tests/test_xlsforms_valid/valid_form_4.xlsx differ diff --git a/tests/test_xlsforms_valid/valid_form_5.xlsx b/tests/test_xlsforms_valid/valid_form_5.xlsx new file mode 100644 index 0000000..76b8d2d Binary files /dev/null and b/tests/test_xlsforms_valid/valid_form_5.xlsx differ diff --git a/tests/test_xlsforms_valid/valid_form_6.xlsx b/tests/test_xlsforms_valid/valid_form_6.xlsx new file mode 100644 index 0000000..c57271b Binary files /dev/null and b/tests/test_xlsforms_valid/valid_form_6.xlsx differ diff --git a/tests/test_xlsforms_valid/valid_form_7.xlsx b/tests/test_xlsforms_valid/valid_form_7.xlsx new file mode 100644 index 0000000..af10789 Binary files /dev/null and b/tests/test_xlsforms_valid/valid_form_7.xlsx differ diff --git a/tests/test_xlsforms_valid/valid_form_8.xlsx b/tests/test_xlsforms_valid/valid_form_8.xlsx new file mode 100644 index 0000000..63648db Binary files /dev/null and b/tests/test_xlsforms_valid/valid_form_8.xlsx differ diff --git a/tests/test_xlsforms_valid/valid_form_9.xlsx b/tests/test_xlsforms_valid/valid_form_9.xlsx new file mode 100644 index 0000000..b036d77 Binary files /dev/null and b/tests/test_xlsforms_valid/valid_form_9.xlsx differ diff --git a/tests/testsheet.xls b/tests/testsheet.xls new file mode 100644 index 0000000..2065119 Binary files /dev/null and b/tests/testsheet.xls differ diff --git a/tests/testsheet2.xls b/tests/testsheet2.xls new file mode 100644 index 0000000..75a41f1 Binary files /dev/null and b/tests/testsheet2.xls differ