mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 07:14:56 +00:00
Merge pull request #789 from 123FLO321/feat/test-setup
test: add Vitest projects, CI workflow, and new frontend/backend test suites
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
# Cancel superseded runs on the same branch / PR to save CI minutes.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Get Playwright version
|
||||
id: playwright-version
|
||||
run: echo "version=$(npx playwright --version | awk '{print $2}')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Type check
|
||||
run: npm run check
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
@@ -16,8 +16,14 @@ npm run check # Svelte + TypeScript type checking
|
||||
npm run prettify # Format all files with Prettier
|
||||
npm run migrate # Run database migrations via Knex
|
||||
npm run seed # Run database seeds (migrations run automatically first)
|
||||
npm test # Run all tests (server unit + browser component projects)
|
||||
npm run test:server # Server-side unit tests only (Node)
|
||||
npm run test:client # Component tests only (headless Chromium via Playwright)
|
||||
npm run test:watch # Watch mode
|
||||
```
|
||||
|
||||
Component tests need a one-time `npx playwright install chromium`.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Dual Process Model
|
||||
|
||||
Generated
+503
-1
@@ -103,6 +103,7 @@
|
||||
"@types/mustache": "^4.2.6",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/nodemailer": "^7.0.4",
|
||||
"@vitest/browser-playwright": "^4.1.10",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"clsx": "^2.1.1",
|
||||
"concurrently": "^9.2.1",
|
||||
@@ -126,7 +127,9 @@
|
||||
"vaul-svelte": "^1.0.0-next.7",
|
||||
"vite": "^7.2.2",
|
||||
"vite-node": "^5.3.0",
|
||||
"vite-plugin-devtools-json": "^1.0.0"
|
||||
"vite-plugin-devtools-json": "^1.0.0",
|
||||
"vitest": "^4.1.10",
|
||||
"vitest-browser-svelte": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -157,6 +160,13 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@blazediff/core": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz",
|
||||
"integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz",
|
||||
@@ -2645,6 +2655,19 @@
|
||||
"vite": "^5.2.0 || ^6 || ^7"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/svelte-core": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/svelte-core/-/svelte-core-1.1.3.tgz",
|
||||
"integrity": "sha512-KkMAvXeWorxN2Yn0kdC1lfoAItxpoj4uOWzxK5leDrNxonLvS5nwBFvztrroyTszQ0Wf/EU6iLT8JhY5qcn22g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bcrypt": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||
@@ -2666,6 +2689,17 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/deep-eql": "*",
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
@@ -2716,6 +2750,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/dns2": {
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/dns2/-/dns2-2.0.10.tgz",
|
||||
@@ -2900,6 +2941,186 @@
|
||||
"@codemirror/view": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/browser": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.10.tgz",
|
||||
"integrity": "sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blazediff/core": "1.9.1",
|
||||
"@vitest/mocker": "4.1.10",
|
||||
"@vitest/utils": "4.1.10",
|
||||
"magic-string": "^0.30.21",
|
||||
"pngjs": "^7.0.0",
|
||||
"sirv": "^3.0.2",
|
||||
"tinyrainbow": "^3.1.0",
|
||||
"ws": "^8.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "4.1.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/browser-playwright": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.10.tgz",
|
||||
"integrity": "sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/browser": "4.1.10",
|
||||
"@vitest/mocker": "4.1.10",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright": "*",
|
||||
"vitest": "4.1.10"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"playwright": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/browser/node_modules/pngjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
||||
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
|
||||
"integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "4.1.10",
|
||||
"@vitest/utils": "4.1.10",
|
||||
"chai": "^6.2.2",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
|
||||
"integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.1.10",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker/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/@vitest/pretty-format": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
|
||||
"integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
|
||||
"integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.1.10",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
|
||||
"integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.1.10",
|
||||
"@vitest/utils": "4.1.10",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
|
||||
"integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
|
||||
"integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.1.10",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@zone-eu/mailsplit": {
|
||||
"version": "5.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz",
|
||||
@@ -3027,6 +3248,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"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/async-function": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
||||
@@ -3539,6 +3770,16 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -3805,6 +4046,13 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
@@ -5018,6 +5266,16 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
|
||||
"integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
@@ -8291,6 +8549,56 @@
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
|
||||
@@ -9490,6 +9798,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/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
@@ -9614,6 +9929,13 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"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/standard-as-callback": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||
@@ -9639,6 +9961,13 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
|
||||
"integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stop-iteration-iterator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||
@@ -10215,6 +10544,23 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
|
||||
"integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
@@ -10231,6 +10577,16 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyrainbow": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
|
||||
"integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tlds": {
|
||||
"version": "1.261.0",
|
||||
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz",
|
||||
@@ -10729,6 +11085,113 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",
|
||||
"integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.1.10",
|
||||
"@vitest/mocker": "4.1.10",
|
||||
"@vitest/pretty-format": "4.1.10",
|
||||
"@vitest/runner": "4.1.10",
|
||||
"@vitest/snapshot": "4.1.10",
|
||||
"@vitest/spy": "4.1.10",
|
||||
"@vitest/utils": "4.1.10",
|
||||
"es-module-lexer": "^2.0.0",
|
||||
"expect-type": "^1.3.0",
|
||||
"magic-string": "^0.30.21",
|
||||
"obug": "^2.1.1",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.3",
|
||||
"std-env": "^4.0.0-rc.1",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^1.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tinyrainbow": "^3.1.0",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.1.10",
|
||||
"@vitest/browser-preview": "4.1.10",
|
||||
"@vitest/browser-webdriverio": "4.1.10",
|
||||
"@vitest/coverage-istanbul": "4.1.10",
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
"@vitest/ui": "4.1.10",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@edge-runtime/vm": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-playwright": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-preview": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-webdriverio": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/coverage-istanbul": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/coverage-v8": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/ui": {
|
||||
"optional": true
|
||||
},
|
||||
"happy-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"jsdom": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest-browser-svelte": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/vitest-browser-svelte/-/vitest-browser-svelte-3.0.0.tgz",
|
||||
"integrity": "sha512-BKM9iBGNsEQzigWuSZFAiw6AJDqQqMttApoD8gM8LTI6vxfd423rDMN8GqUHJyGbhH3XcOo9N9u/4hu47Nxhsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@testing-library/svelte-core": "^1.1.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0",
|
||||
"vitest": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
@@ -10864,6 +11327,23 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"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/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
@@ -10887,6 +11367,28 @@
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
|
||||
|
||||
+9
-2
@@ -48,7 +48,11 @@
|
||||
"preview": "vite preview",
|
||||
"schedule": "vite-node src/lib/server/startup.ts",
|
||||
"seed": "npx knex seed:run",
|
||||
"start": "node build/main.js"
|
||||
"start": "node build/main.js",
|
||||
"test": "cross-env TZ=UTC vitest run",
|
||||
"test:client": "vitest run --project=client",
|
||||
"test:server": "cross-env TZ=UTC vitest run --project=server",
|
||||
"test:watch": "cross-env TZ=UTC vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@internationalized/date": "^3.10.0",
|
||||
@@ -69,6 +73,7 @@
|
||||
"@types/mustache": "^4.2.6",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/nodemailer": "^7.0.4",
|
||||
"@vitest/browser-playwright": "^4.1.10",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"clsx": "^2.1.1",
|
||||
"concurrently": "^9.2.1",
|
||||
@@ -92,7 +97,9 @@
|
||||
"vaul-svelte": "^1.0.0-next.7",
|
||||
"vite": "^7.2.2",
|
||||
"vite-node": "^5.3.0",
|
||||
"vite-plugin-devtools-json": "^1.0.0"
|
||||
"vite-plugin-devtools-json": "^1.0.0",
|
||||
"vitest": "^4.1.10",
|
||||
"vitest-browser-svelte": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { render } from "vitest-browser-svelte";
|
||||
import CopyButton from "./CopyButton.svelte";
|
||||
|
||||
// The clipboard is spied on rather than read back: headless Chromium does not
|
||||
// grant clipboard-read permission, and the component's contract is "calls
|
||||
// navigator.clipboard.writeText and flips to the copied state".
|
||||
describe("CopyButton", () => {
|
||||
beforeAll(() => {
|
||||
// navigator.clipboard only exists in secure contexts; provide a minimal
|
||||
// stand-in when absent so the spies below always have a target.
|
||||
if (!("clipboard" in navigator)) {
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
value: { writeText: async () => {} },
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("copies the text prop and shows the copied state on click", async () => {
|
||||
const writeText = vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue(undefined);
|
||||
|
||||
const screen = await render(CopyButton, { text: "copy-me" });
|
||||
const copiedTooltip = screen.getByText("Copied");
|
||||
|
||||
// Hidden before the click (class-based visibility)
|
||||
await expect.element(copiedTooltip).toHaveClass(/opacity-0/);
|
||||
|
||||
await screen.getByRole("button").click();
|
||||
|
||||
expect(writeText).toHaveBeenCalledExactlyOnceWith("copy-me");
|
||||
await expect.element(copiedTooltip).toHaveClass(/scale-100/);
|
||||
});
|
||||
|
||||
it("invokes the onclick callback", async () => {
|
||||
vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue(undefined);
|
||||
const onclick = vi.fn();
|
||||
|
||||
const screen = await render(CopyButton, { text: "x", onclick });
|
||||
await screen.getByRole("button").click();
|
||||
|
||||
expect(onclick).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not touch the clipboard when no text is provided", async () => {
|
||||
const writeText = vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue(undefined);
|
||||
|
||||
const screen = await render(CopyButton, {});
|
||||
await screen.getByRole("button").click();
|
||||
|
||||
expect(writeText).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { render } from "vitest-browser-svelte";
|
||||
import StatusBarCalendar from "./StatusBarCalendar.svelte";
|
||||
import type { TimestampStatusCount } from "$lib/server/types/db";
|
||||
|
||||
// The browser context is pinned to UTC (see vite.config.ts), so the timezone store
|
||||
// (which defaults to browser TZ) is deterministic; noon UTC keeps fixtures safe too.
|
||||
const NOON_UTC = 1768478400; // 2026-01-15T12:00:00Z
|
||||
|
||||
const day = (overrides: Partial<TimestampStatusCount>): TimestampStatusCount => ({
|
||||
ts: NOON_UTC,
|
||||
countOfUp: 0,
|
||||
countOfDown: 0,
|
||||
countOfDegraded: 0,
|
||||
countOfMaintenance: 0,
|
||||
avgLatency: 0,
|
||||
maxLatency: 0,
|
||||
minLatency: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("StatusBarCalendar", () => {
|
||||
it("labels the canvas with the number of days", async () => {
|
||||
const data = [day({ countOfUp: 10 }), day({ countOfUp: 10 }), day({ countOfUp: 10 })];
|
||||
const screen = await render(StatusBarCalendar, { data, monitorTag: "test-monitor", disableClick: true });
|
||||
|
||||
await expect.element(screen.getByLabelText("Status calendar showing 3-day uptime data")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows status summary, date, and latency in the tooltip of the hovered day", async () => {
|
||||
// Three bars: hovering the canvas center lands on the middle bar (index 1),
|
||||
// which is 90% down => "Major System Outage" (>= 75% threshold).
|
||||
const data = [
|
||||
day({ countOfUp: 100 }),
|
||||
day({ countOfDown: 90, countOfUp: 10, avgLatency: 250 }),
|
||||
day({ countOfUp: 90, countOfDegraded: 10 }),
|
||||
];
|
||||
const screen = await render(StatusBarCalendar, { data, monitorTag: "test-monitor", disableClick: true });
|
||||
|
||||
await screen.getByLabelText("Status calendar showing 3-day uptime data").hover();
|
||||
|
||||
await expect.element(screen.getByText("Major System Outage")).toBeInTheDocument();
|
||||
// page.data.dateAndTimeFormat.dateOnly is mocked as "yyyy-MM-dd" in vitest-setup-client.ts
|
||||
await expect.element(screen.getByText("2026-01-15", { exact: false })).toBeInTheDocument();
|
||||
await expect.element(screen.getByText("250ms")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("reports a partial outage when downtime is below the 75% threshold", async () => {
|
||||
const data = [day({ countOfUp: 100 }), day({ countOfDown: 20, countOfUp: 80 }), day({ countOfUp: 100 })];
|
||||
const screen = await render(StatusBarCalendar, { data, monitorTag: "test-monitor", disableClick: true });
|
||||
|
||||
await screen.getByLabelText("Status calendar showing 3-day uptime data").hover();
|
||||
|
||||
await expect.element(screen.getByText("Partial System Outage")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,378 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
BeginningOfDay,
|
||||
BeginningOfMinute,
|
||||
checkIfDuplicateExists,
|
||||
DurationInMinutes,
|
||||
GetDayStartTimestampUTC,
|
||||
GetDayStartWithOffset,
|
||||
GetMinuteStartNowTimestampUTC,
|
||||
GetMinuteStartTimestampUTC,
|
||||
GetNowTimestampUTC,
|
||||
GetNowTimestampUTCInMs,
|
||||
GetRequiredSecrets,
|
||||
GetWordsStartingWithDollar,
|
||||
HashString,
|
||||
IsStringURLSafe,
|
||||
IsValidHost,
|
||||
IsValidHTTPMethod,
|
||||
IsValidNameServer,
|
||||
IsValidURL,
|
||||
IsValidUptimeFormula,
|
||||
MaskString,
|
||||
ParsePercentage,
|
||||
ParseUptime,
|
||||
ReplaceAllOccurrences,
|
||||
UnparsePercentage,
|
||||
UptimeCalculator,
|
||||
ValidateEmail,
|
||||
ValidateIpAddress,
|
||||
ValidateMonitorAlerts,
|
||||
ValidateURL,
|
||||
} from "./tool";
|
||||
import type { TimestampStatusCount } from "./types/db";
|
||||
|
||||
// The npm scripts pin TZ=UTC, so local-date-based functions (GetDayStartTimestampUTC
|
||||
// mixes local date parts with Date.UTC) are deterministic here. Fixtures still use
|
||||
// noon UTC so the tests stay safe even if vitest is run directly without the pin.
|
||||
const NOON_UTC = 1768478400; // 2026-01-15T12:00:00Z
|
||||
const DAY_START = 1768435200; // 2026-01-15T00:00:00Z
|
||||
|
||||
describe("timestamp helpers", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("GetNowTimestampUTC returns the current epoch in seconds", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-15T12:00:00Z"));
|
||||
expect(GetNowTimestampUTC()).toBe(NOON_UTC);
|
||||
});
|
||||
|
||||
it("GetNowTimestampUTCInMs returns the current epoch in milliseconds", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-15T12:00:00Z"));
|
||||
expect(GetNowTimestampUTCInMs()).toBe(NOON_UTC * 1000);
|
||||
});
|
||||
|
||||
it("GetMinuteStartTimestampUTC floors a timestamp to the start of its minute", () => {
|
||||
expect(GetMinuteStartTimestampUTC(NOON_UTC + 45)).toBe(NOON_UTC);
|
||||
expect(GetMinuteStartTimestampUTC(NOON_UTC)).toBe(NOON_UTC);
|
||||
expect(GetMinuteStartTimestampUTC(NOON_UTC + 59)).toBe(NOON_UTC);
|
||||
});
|
||||
|
||||
it("GetMinuteStartNowTimestampUTC floors the current time to the minute", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-15T12:00:45Z"));
|
||||
expect(GetMinuteStartNowTimestampUTC()).toBe(NOON_UTC);
|
||||
});
|
||||
|
||||
it("GetDayStartTimestampUTC returns midnight UTC of the timestamp's day", () => {
|
||||
expect(GetDayStartTimestampUTC(NOON_UTC)).toBe(DAY_START);
|
||||
expect(GetDayStartTimestampUTC(DAY_START + 12 * 3600 + 61)).toBe(DAY_START);
|
||||
});
|
||||
|
||||
it("DurationInMinutes floors the difference to whole minutes", () => {
|
||||
expect(DurationInMinutes(0, 600)).toBe(10);
|
||||
expect(DurationInMinutes(0, 59)).toBe(0);
|
||||
expect(DurationInMinutes(DAY_START, NOON_UTC)).toBe(720);
|
||||
});
|
||||
|
||||
it("GetDayStartWithOffset shifts the UTC day start by positive offsets", () => {
|
||||
// 330 minutes = UTC+5:30 (e.g. IST)
|
||||
expect(GetDayStartWithOffset(NOON_UTC, 330)).toBe(DAY_START + 330 * 60);
|
||||
expect(GetDayStartWithOffset(NOON_UTC, 0)).toBe(DAY_START);
|
||||
});
|
||||
|
||||
it("GetDayStartWithOffset moves to the next day for negative offsets", () => {
|
||||
expect(GetDayStartWithOffset(NOON_UTC, -330)).toBe(DAY_START + 86400 - 330 * 60);
|
||||
});
|
||||
|
||||
it("BeginningOfDay returns midnight in the given timezone", () => {
|
||||
const date = new Date("2026-01-15T12:00:00Z");
|
||||
expect(BeginningOfDay({ date, timeZone: "UTC" })).toBe(DAY_START);
|
||||
// Midnight in New York (UTC-5 in January) is 05:00 UTC
|
||||
expect(BeginningOfDay({ date, timeZone: "America/New_York" })).toBe(DAY_START + 5 * 3600);
|
||||
});
|
||||
|
||||
it("BeginningOfMinute strips seconds", () => {
|
||||
const date = new Date("2026-01-15T12:00:45Z");
|
||||
expect(BeginningOfMinute({ date, timeZone: "UTC" })).toBe(NOON_UTC);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ParseUptime", () => {
|
||||
it("returns '-' when there is no data", () => {
|
||||
expect(ParseUptime(0, 0)).toBe("-");
|
||||
});
|
||||
|
||||
it("returns '0' when nothing is up", () => {
|
||||
expect(ParseUptime(0, 10)).toBe("0");
|
||||
});
|
||||
|
||||
it("returns whole numbers without decimals", () => {
|
||||
expect(ParseUptime(10, 10)).toBe("100");
|
||||
expect(ParseUptime(5, 10)).toBe("50");
|
||||
});
|
||||
|
||||
it("keeps 4 decimal places and strips trailing zeros", () => {
|
||||
expect(ParseUptime(1, 3)).toBe("33.3333");
|
||||
expect(ParseUptime(2, 3)).toBe("66.6667");
|
||||
expect(ParseUptime(1, 8)).toBe("12.5");
|
||||
expect(ParseUptime(999, 1000)).toBe("99.9");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ParsePercentage / UnparsePercentage", () => {
|
||||
it("formats numbers as percentage strings", () => {
|
||||
expect(ParsePercentage(NaN)).toBe("-");
|
||||
expect(ParsePercentage(0)).toBe("0");
|
||||
expect(ParsePercentage(100)).toBe("100");
|
||||
expect(ParsePercentage(99.9)).toBe("99.9000");
|
||||
});
|
||||
|
||||
it("parses percentage strings back to numbers", () => {
|
||||
expect(UnparsePercentage("99.9")).toBe(99.9);
|
||||
expect(UnparsePercentage("0")).toBe(0);
|
||||
expect(Number.isNaN(UnparsePercentage("-"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("IsValidUptimeFormula", () => {
|
||||
it("accepts single variables and operator chains", () => {
|
||||
expect(IsValidUptimeFormula("up")).toBe(true);
|
||||
expect(IsValidUptimeFormula("up + down")).toBe(true);
|
||||
expect(IsValidUptimeFormula("up+down+degraded+maintenance")).toBe(true);
|
||||
expect(IsValidUptimeFormula("up - down * maintenance / degraded")).toBe(true);
|
||||
});
|
||||
|
||||
it("is case-insensitive", () => {
|
||||
expect(IsValidUptimeFormula("UP + Maintenance")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects malformed formulas", () => {
|
||||
expect(IsValidUptimeFormula("")).toBe(false);
|
||||
expect(IsValidUptimeFormula(" ")).toBe(false);
|
||||
expect(IsValidUptimeFormula("up +")).toBe(false);
|
||||
expect(IsValidUptimeFormula("+ up")).toBe(false);
|
||||
expect(IsValidUptimeFormula("banana + up")).toBe(false);
|
||||
expect(IsValidUptimeFormula("up ** down")).toBe(false);
|
||||
expect(IsValidUptimeFormula("up down")).toBe(false);
|
||||
expect(IsValidUptimeFormula("42")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("UptimeCalculator", () => {
|
||||
const day = (overrides: Partial<TimestampStatusCount>): TimestampStatusCount => ({
|
||||
ts: 0,
|
||||
countOfUp: 0,
|
||||
countOfDown: 0,
|
||||
countOfDegraded: 0,
|
||||
countOfMaintenance: 0,
|
||||
avgLatency: 0,
|
||||
maxLatency: 0,
|
||||
minLatency: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it("computes uptime and latency aggregates with the default formulas", () => {
|
||||
// Defaults (global-constants): numerator "up + maintenance + degraded",
|
||||
// denominator "up + down + degraded + maintenance"
|
||||
const data = [
|
||||
day({ countOfUp: 30, countOfDown: 10, avgLatency: 100, maxLatency: 150, minLatency: 50 }),
|
||||
day({ countOfUp: 50, countOfDegraded: 10, avgLatency: 200, maxLatency: 300, minLatency: 80 }),
|
||||
];
|
||||
expect(UptimeCalculator(data)).toEqual({
|
||||
uptime: "90", // (80 up + 0 maintenance + 10 degraded) / 100 total
|
||||
avgLatency: "150ms", // (100 + 200) / 2
|
||||
maxLatency: "300ms",
|
||||
minLatency: "50ms",
|
||||
});
|
||||
});
|
||||
|
||||
it("supports a custom numerator formula", () => {
|
||||
const data = [day({ countOfUp: 30, countOfDown: 10 }), day({ countOfUp: 50, countOfDegraded: 10 })];
|
||||
expect(UptimeCalculator(data, "up").uptime).toBe("80");
|
||||
});
|
||||
|
||||
it("falls back to the default formula when the custom one is invalid", () => {
|
||||
const data = [day({ countOfUp: 30, countOfDown: 10 }), day({ countOfUp: 50, countOfDegraded: 10 })];
|
||||
expect(UptimeCalculator(data, "banana + up").uptime).toBe("90");
|
||||
});
|
||||
|
||||
it("returns '-' uptime and empty latencies for no data", () => {
|
||||
expect(UptimeCalculator([])).toEqual({
|
||||
uptime: "-",
|
||||
avgLatency: "",
|
||||
maxLatency: "",
|
||||
minLatency: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("only counts entries with a positive avgLatency toward the average", () => {
|
||||
const data = [
|
||||
day({ countOfUp: 10, avgLatency: 0 }),
|
||||
day({ countOfUp: 10, avgLatency: 100, maxLatency: 100, minLatency: 100 }),
|
||||
];
|
||||
const result = UptimeCalculator(data);
|
||||
expect(result.uptime).toBe("100");
|
||||
expect(result.avgLatency).toBe("100ms"); // (0 + 100) / 1 entry with latency > 0
|
||||
});
|
||||
});
|
||||
|
||||
describe("ValidateIpAddress", () => {
|
||||
it("classifies IPv4, IPv6, domain names, and invalid input", () => {
|
||||
expect(ValidateIpAddress("192.168.1.1")).toBe("IPv4");
|
||||
expect(ValidateIpAddress("8.8.8.8")).toBe("IPv4");
|
||||
expect(ValidateIpAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334")).toBe("IPv6");
|
||||
expect(ValidateIpAddress("example.com")).toBe("Domain Name");
|
||||
expect(ValidateIpAddress("kener.ing")).toBe("Domain Name");
|
||||
expect(ValidateIpAddress("not valid")).toBe("Invalid");
|
||||
});
|
||||
|
||||
it("documents quirks: permissive IPv4 octets, no shortened IPv6", () => {
|
||||
// The regex does not range-check octets:
|
||||
expect(ValidateIpAddress("999.999.999.999")).toBe("IPv4");
|
||||
// Only full-form IPv6 is recognized:
|
||||
expect(ValidateIpAddress("::1")).toBe("Invalid");
|
||||
});
|
||||
});
|
||||
|
||||
describe("host / nameserver / URL / method validators", () => {
|
||||
it("IsValidHost accepts dotted domains with a TLD", () => {
|
||||
expect(IsValidHost("example.com")).toBe(true);
|
||||
expect(IsValidHost("sub.domain.co.uk")).toBe(true);
|
||||
expect(IsValidHost("my-site.example.org")).toBe(true);
|
||||
expect(IsValidHost("localhost")).toBe(false);
|
||||
expect(IsValidHost("-bad.com")).toBe(false);
|
||||
expect(IsValidHost("")).toBe(false);
|
||||
});
|
||||
|
||||
it("IsValidNameServer accepts only dotted-quad IPs", () => {
|
||||
expect(IsValidNameServer("8.8.8.8")).toBe(true);
|
||||
expect(IsValidNameServer("example.com")).toBe(false);
|
||||
expect(IsValidNameServer("8.8.8")).toBe(false);
|
||||
});
|
||||
|
||||
it("IsValidURL requires http(s) and no spaces", () => {
|
||||
expect(IsValidURL("https://example.com")).toBe(true);
|
||||
expect(IsValidURL("http://example.com/path?q=1")).toBe(true);
|
||||
expect(IsValidURL("ftp://example.com")).toBe(false);
|
||||
expect(IsValidURL("https://exa mple.com")).toBe(false);
|
||||
});
|
||||
|
||||
it("ValidateURL parses with the URL constructor and allows only http(s)", () => {
|
||||
expect(ValidateURL("https://example.com")).toBe(true);
|
||||
expect(ValidateURL("http://localhost:3000")).toBe(true);
|
||||
expect(ValidateURL("ftp://example.com")).toBe(false);
|
||||
expect(ValidateURL("javascript:alert(1)")).toBe(false);
|
||||
expect(ValidateURL("not a url")).toBe(false);
|
||||
});
|
||||
|
||||
it("IsValidHTTPMethod is uppercase-only", () => {
|
||||
expect(IsValidHTTPMethod("GET")).toBe(true);
|
||||
expect(IsValidHTTPMethod("PATCH")).toBe(true);
|
||||
expect(IsValidHTTPMethod("get")).toBe(false);
|
||||
expect(IsValidHTTPMethod("FETCH")).toBe(false);
|
||||
});
|
||||
|
||||
it("IsStringURLSafe allows unreserved URL characters only", () => {
|
||||
expect(IsStringURLSafe("abc-123_~.")).toBe(true);
|
||||
expect(IsStringURLSafe("a b")).toBe(false);
|
||||
expect(IsStringURLSafe("a/b")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ValidateEmail", () => {
|
||||
it("accepts common address shapes", () => {
|
||||
expect(ValidateEmail("user@example.com")).toBe(true);
|
||||
expect(ValidateEmail("first.last@sub.example.co")).toBe(true);
|
||||
expect(ValidateEmail("user+tag@example.com")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects malformed addresses", () => {
|
||||
expect(ValidateEmail("no-at.example.com")).toBe(false);
|
||||
expect(ValidateEmail("user@")).toBe(false);
|
||||
expect(ValidateEmail("user@nodot")).toBe(false);
|
||||
expect(ValidateEmail("@example.com")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("string helpers", () => {
|
||||
it("MaskString masks everything but the last 4 characters", () => {
|
||||
expect(MaskString("secret123")).toBe("*****t123");
|
||||
expect(MaskString("abcd")).toBe("****");
|
||||
expect(MaskString("ab")).toBe("**");
|
||||
expect(MaskString("")).toBe("");
|
||||
});
|
||||
|
||||
it("HashString returns the sha256 hex digest", () => {
|
||||
expect(HashString("hello")).toBe("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824");
|
||||
});
|
||||
|
||||
it("GetWordsStartingWithDollar extracts $-prefixed words", () => {
|
||||
expect(GetWordsStartingWithDollar("run $FOO with $BAR_2")).toEqual(["$FOO", "$BAR_2"]);
|
||||
expect(GetWordsStartingWithDollar("nothing here")).toEqual([]);
|
||||
});
|
||||
|
||||
it("ReplaceAllOccurrences replaces every occurrence of a $-token", () => {
|
||||
expect(ReplaceAllOccurrences("a $X b $X", "$X", "y")).toBe("a y b y");
|
||||
});
|
||||
|
||||
it("checkIfDuplicateExists detects duplicates", () => {
|
||||
expect(checkIfDuplicateExists([1, 2, 2])).toBe(true);
|
||||
expect(checkIfDuplicateExists(["a", "b"])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GetRequiredSecrets", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("resolves $-tokens that exist in the environment", () => {
|
||||
vi.stubEnv("KENER_TEST_SECRET", "s3cret");
|
||||
expect(GetRequiredSecrets("token: $KENER_TEST_SECRET")).toEqual([
|
||||
{ find: "$KENER_TEST_SECRET", replace: "s3cret" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("ignores tokens with no matching environment variable", () => {
|
||||
expect(GetRequiredSecrets("token: $KENER_TEST_DEFINITELY_UNSET_VAR")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ValidateMonitorAlerts", () => {
|
||||
beforeEach(() => {
|
||||
// The function console.logs every rejection reason; keep test output clean.
|
||||
vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const validAlert = { triggers: [1], failureThreshold: 1, successThreshold: 2 };
|
||||
|
||||
it("accepts DOWN and/or DEGRADED alerts with triggers and integer thresholds", () => {
|
||||
expect(ValidateMonitorAlerts({ DOWN: validAlert })).toBe(true);
|
||||
expect(ValidateMonitorAlerts({ DOWN: validAlert, DEGRADED: validAlert })).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects missing/empty/unknown-key alert objects", () => {
|
||||
expect(ValidateMonitorAlerts(null)).toBe(false);
|
||||
expect(ValidateMonitorAlerts({})).toBe(false);
|
||||
expect(ValidateMonitorAlerts({ UP: validAlert } as never)).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects missing or empty triggers", () => {
|
||||
expect(ValidateMonitorAlerts({ DOWN: { failureThreshold: 1, successThreshold: 1 } })).toBe(false);
|
||||
expect(ValidateMonitorAlerts({ DOWN: { ...validAlert, triggers: [] } })).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects missing, non-integer, or non-positive thresholds", () => {
|
||||
expect(ValidateMonitorAlerts({ DOWN: { triggers: [1] } })).toBe(false);
|
||||
expect(ValidateMonitorAlerts({ DOWN: { ...validAlert, failureThreshold: 1.5 } })).toBe(false);
|
||||
expect(ValidateMonitorAlerts({ DOWN: { ...validAlert, successThreshold: 0 } })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,11 @@
|
||||
/// <reference types="vitest/config" />
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import version from "vite-plugin-package-version";
|
||||
import { defineConfig } from "vite";
|
||||
import devtoolsJson from "vite-plugin-devtools-json";
|
||||
import { playwright } from "@vitest/browser-playwright";
|
||||
import { configDefaults } from "vitest/config";
|
||||
|
||||
import * as dotenv from "dotenv";
|
||||
|
||||
@@ -56,5 +59,40 @@ export default defineConfig(({ mode }) => {
|
||||
__KENER_BUILD_ENV__: JSON.stringify(buildEnv),
|
||||
__KENER_IS_PROD__: JSON.stringify(isProduction),
|
||||
},
|
||||
test: {
|
||||
projects: [
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: "server",
|
||||
environment: "node",
|
||||
include: ["src/**/*.{test,spec}.{js,ts}"],
|
||||
exclude: [...configDefaults.exclude, "src/**/*.svelte.{test,spec}.{js,ts}"],
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: true,
|
||||
// Pre-bundle the component-test dependency graph: on a cold Vite cache
|
||||
// (every CI runner) a mid-run dep-optimization reload can flake the suite.
|
||||
optimizeDeps: {
|
||||
include: ["layerchart", "mode-watcher", "bits-ui", "d3-scale", "d3-shape"],
|
||||
},
|
||||
test: {
|
||||
name: "client",
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
provider: playwright({ contextOptions: { timezoneId: "UTC" } }),
|
||||
instances: [{ browser: "chromium" }],
|
||||
// Vitest's default browser API port (63315) can fall inside Windows
|
||||
// Hyper-V excluded TCP port ranges; pin below the ephemeral range.
|
||||
api: { port: 5180 },
|
||||
},
|
||||
include: ["src/**/*.svelte.{test,spec}.{js,ts}"],
|
||||
setupFiles: ["./vitest-setup-client.ts"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
// Shared stubs for SvelteKit runtime modules used by components under test.
|
||||
// Values are minimal but realistic; individual tests can override with their
|
||||
// own vi.mock(...) when they need different page data.
|
||||
vi.mock("$app/state", () => ({
|
||||
page: {
|
||||
data: {
|
||||
siteStatusColors: { UP: "#22c55e", DOWN: "#ef4444", DEGRADED: "#eab308", MAINTENANCE: "#3b82f6" },
|
||||
dateAndTimeFormat: { dateOnly: "yyyy-MM-dd", timeOnly: "HH:mm" },
|
||||
nowAtTz: 1768478400, // 2026-01-15T12:00:00Z
|
||||
},
|
||||
url: new URL("http://localhost/"),
|
||||
params: {},
|
||||
route: { id: null },
|
||||
status: 200,
|
||||
error: null,
|
||||
form: null,
|
||||
state: {},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("$app/paths", () => ({
|
||||
base: "",
|
||||
assets: "",
|
||||
resolve: (path: string) => path,
|
||||
}));
|
||||
Reference in New Issue
Block a user