mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:07:18 +01:00
web_ui: add the CI + pre-commit gate, and make pnpm lint actually run
Matches the Phase 1 design_system pattern the brief points at: own lint config, own tests, own coverage gate enforced in CI. The lint script was declared but unrunnable -- there was no eslint flat config, and the build tsconfig excludes tests (correctly; they must not ship in dist/), so type-aware linting had no project for them. Adds eslint.config.js and a lint-only tsconfig that widens the include without emitting. Running it surfaced one real defect: nth() interpolated a number into a template literal without String(), which the consumers' configs would have flagged. The gate also checks that the committed dist/ matches a fresh build. This is not theoretical -- the check caught the stale dist/ produced by that very String() fix on its first run. A stale dist/ in a tag ships old code to every consumer with no error anywhere, which is the one failure mode this packaging choice introduces. dist/ is excluded from the whitespace hooks: they would "fix" generated output that the next build immediately reverts. CI additionally re-runs the categorical ramp's constraint proof, so the palette cannot regress silently either. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
This commit is contained in:
parent
56d0c3d420
commit
eaa5683ff1
52
.github/workflows/web-ui-tests.yml
vendored
Normal file
52
.github/workflows/web-ui-tests.yml
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
name: web_ui tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['web_ui/**']
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths: ['web_ui/**']
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web_ui
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Run tests with the 100% coverage gate
|
||||
run: pnpm coverage
|
||||
|
||||
# The tag ships a prebuilt dist/ because Vite does not transpile
|
||||
# dependencies. A committed dist/ that does not match src/ would ship
|
||||
# stale code to every consumer with no error anywhere, so rebuild and
|
||||
# fail if the result differs from what is checked in.
|
||||
- name: dist/ is in sync with src/
|
||||
run: |
|
||||
pnpm build
|
||||
if ! git diff --exit-code -- dist; then
|
||||
echo "::error::dist/ is stale — run 'pnpm build' in web_ui and commit the result."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Categorical ramp still passes its constraints
|
||||
working-directory: .
|
||||
run: python3 unified-design-system/scripts/ramp_check.py
|
||||
67
web_ui/.pre-commit-config.yaml
Normal file
67
web_ui/.pre-commit-config.yaml
Normal file
@ -0,0 +1,67 @@
|
||||
# Pre-commit Configuration for web_ui
|
||||
# Install: pre-commit install && pre-commit install --hook-type pre-push
|
||||
# Run: pre-commit run --config web_ui/.pre-commit-config.yaml --all-files
|
||||
|
||||
# NOTE: this subproject lives inside the ~/utils monorepo, whose git root is
|
||||
# one level up. pre-commit always runs hooks from the git root, so every path
|
||||
# below is written relative to THAT, not to this directory, and the top-level
|
||||
# `files` key keeps these hooks off the sibling subprojects. CI invokes this
|
||||
# config exactly the same way. Mirrors design_system/.pre-commit-config.yaml.
|
||||
files: ^web_ui/
|
||||
# dist/ is generated by tsc and committed for consumers. The whitespace hooks
|
||||
# would "fix" it, the next build would revert them, and the dist-fresh hook
|
||||
# would fail forever -- so formatting hooks must not see it.
|
||||
exclude: ^web_ui/dist/
|
||||
|
||||
default_stages: [pre-commit]
|
||||
fail_fast: false
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
args: [--markdown-linebreak-ext=md]
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-json
|
||||
- id: check-merge-conflict
|
||||
- id: mixed-line-ending
|
||||
args: [--fix=lf]
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: subproject-scope
|
||||
name: subproject scope regex still matches
|
||||
entry: scripts/check_subproject_scope.sh web_ui
|
||||
language: script
|
||||
# always_run so the top-level `files` filter cannot skip the one hook
|
||||
# whose job is to notice that filter has stopped matching anything.
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
|
||||
- id: web-ui-lint
|
||||
name: tsc --noEmit && eslint
|
||||
entry: bash -c 'cd web_ui && pnpm lint'
|
||||
language: system
|
||||
pass_filenames: false
|
||||
|
||||
- id: web-ui-dist-fresh
|
||||
name: committed dist/ matches src/
|
||||
# The tag ships a prebuilt dist/ (Vite does not transpile deps), so a
|
||||
# stale dist/ ships old code to every consumer with no error anywhere.
|
||||
entry: >-
|
||||
bash -c 'cd web_ui && pnpm build &&
|
||||
git diff --exit-code -- dist ||
|
||||
{ echo "dist/ is stale -- pnpm build in web_ui and commit it"; exit 1; }'
|
||||
language: system
|
||||
pass_filenames: false
|
||||
require_serial: true
|
||||
|
||||
- id: web-ui-test-coverage
|
||||
name: vitest with the 100% coverage gate
|
||||
entry: bash -c 'cd web_ui && pnpm coverage'
|
||||
language: system
|
||||
pass_filenames: false
|
||||
require_serial: true
|
||||
stages: [pre-push]
|
||||
2
web_ui/dist/quantile.js
vendored
2
web_ui/dist/quantile.js
vendored
@ -19,7 +19,7 @@
|
||||
export function nth(values, index) {
|
||||
const value = values[index];
|
||||
if (value === undefined) {
|
||||
throw new RangeError(`index ${index} out of range`);
|
||||
throw new RangeError(`index ${String(index)} out of range`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
2
web_ui/dist/quantile.js.map
vendored
2
web_ui/dist/quantile.js.map
vendored
@ -1 +1 @@
|
||||
{"version":3,"file":"quantile.js","sourceRoot":"","sources":["../src/quantile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;GAIG;AACH,MAAM,UAAU,GAAG,CAAC,MAAyB,EAAE,KAAa;IAC1D,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,UAAU,CAAC,SAAS,KAAK,eAAe,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,kCAAkC;AAClC,MAAM,UAAU,OAAO,CAAC,CAAS;IAC/B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAAyB,EAAE,CAAS;IAChE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;IACxB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACtB,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1B,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1B,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1B,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC;AAC9C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,MAAyB,EAAE,KAAa;IACpE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;IACxB,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACrB,IAAI,KAAK,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IACtC,IAAI,KAAK,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IAC1C,qEAAqE;IACrE,kDAAkD;IAClD,MAAM,EAAE,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC;IAC/C,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IAClB,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1B,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1B,yDAAyD;IACzD,OAAO,CAAC,EAAE,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAChD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAa,EAAE,OAAe;IAChE,OAAO,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;AAC5E,CAAC"}
|
||||
{"version":3,"file":"quantile.js","sourceRoot":"","sources":["../src/quantile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;GAIG;AACH,MAAM,UAAU,GAAG,CAAC,MAAyB,EAAE,KAAa;IAC1D,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,UAAU,CAAC,SAAS,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,kCAAkC;AAClC,MAAM,UAAU,OAAO,CAAC,CAAS;IAC/B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAAyB,EAAE,CAAS;IAChE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;IACxB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACtB,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1B,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1B,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1B,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC;AAC9C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,MAAyB,EAAE,KAAa;IACpE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;IACxB,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACrB,IAAI,KAAK,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IACtC,IAAI,KAAK,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IAC1C,qEAAqE;IACrE,kDAAkD;IAClD,MAAM,EAAE,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC;IAC/C,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IAClB,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1B,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1B,yDAAyD;IACzD,OAAO,CAAC,EAAE,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAChD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAa,EAAE,OAAe;IAChE,OAAO,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;AAC5E,CAAC"}
|
||||
|
||||
42
web_ui/eslint.config.js
Normal file
42
web_ui/eslint.config.js
Normal file
@ -0,0 +1,42 @@
|
||||
// Type-aware flat config for the shared component layer.
|
||||
//
|
||||
// A trimmed version of awesome-mcp-explorer's: typescript-eslint's
|
||||
// *type-checked* strict + stylistic presets, so anything they flag is an error.
|
||||
// The consumers layer unicorn/sonarjs/perfectionist on top; this package keeps
|
||||
// to the presets its own devDependencies actually declare, because a config
|
||||
// referencing plugins that are not installed is a lint script that cannot run.
|
||||
import js from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
// eslint.config.js itself is not in tsconfig.lint.json, so type-aware
|
||||
// rules cannot run on it; linting the linter config buys nothing anyway.
|
||||
{ ignores: ["dist", "coverage", "eslint.config.js"] },
|
||||
{ linterOptions: { reportUnusedDisableDirectives: "error" } },
|
||||
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
{
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.lint.json"],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Every exported symbol here is consumed across a package boundary, so
|
||||
// an explicit return type is documentation, not ceremony.
|
||||
"@typescript-eslint/explicit-function-return-type": "error",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
// Tests reach for non-null assertions on queries that cannot return null
|
||||
// in a passing test; asserting them again would only add noise.
|
||||
files: ["**/*.test.{ts,tsx}"],
|
||||
rules: { "@typescript-eslint/no-non-null-assertion": "off" },
|
||||
},
|
||||
);
|
||||
@ -25,7 +25,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b && cp src/tokens.css src/range-slider.css dist/",
|
||||
"lint": "tsc --noEmit && eslint src",
|
||||
"lint": "tsc --noEmit -p tsconfig.lint.json && eslint .",
|
||||
"test": "vitest run",
|
||||
"coverage": "vitest run --coverage"
|
||||
},
|
||||
@ -46,6 +46,7 @@
|
||||
"react-dom": "^19.2.7",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.64.0",
|
||||
"vitest": "^4.1.10"
|
||||
"vitest": "^4.1.10",
|
||||
"@types/node": "^26.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,9 @@ importers:
|
||||
'@testing-library/user-event':
|
||||
specifier: ^14.6.1
|
||||
version: 14.6.4(@testing-library/dom@10.4.1)
|
||||
'@types/node':
|
||||
specifier: ^26.1.1
|
||||
version: 26.2.0
|
||||
'@types/react':
|
||||
specifier: ^19.2.17
|
||||
version: 19.2.18
|
||||
@ -49,7 +52,7 @@ importers:
|
||||
version: 8.67.0(eslint@10.8.1)(typescript@6.0.3)
|
||||
vitest:
|
||||
specifier: ^4.1.10
|
||||
version: 4.1.10(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(vite@8.2.1)
|
||||
version: 4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(vite@8.2.1(@types/node@26.2.0))
|
||||
|
||||
packages:
|
||||
|
||||
@ -361,6 +364,9 @@ packages:
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/node@26.2.0':
|
||||
resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==}
|
||||
|
||||
'@types/react-dom@19.2.4':
|
||||
resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==}
|
||||
peerDependencies:
|
||||
@ -1040,6 +1046,9 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@8.3.0:
|
||||
resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
|
||||
|
||||
undici@7.29.0:
|
||||
resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
@ -1406,6 +1415,10 @@ snapshots:
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/node@26.2.0':
|
||||
dependencies:
|
||||
undici-types: 8.3.0
|
||||
|
||||
'@types/react-dom@19.2.4(@types/react@19.2.18)':
|
||||
dependencies:
|
||||
'@types/react': 19.2.18
|
||||
@ -1517,7 +1530,7 @@ snapshots:
|
||||
obug: 2.1.4
|
||||
std-env: 4.2.0
|
||||
tinyrainbow: 3.1.1
|
||||
vitest: 4.1.10(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(vite@8.2.1)
|
||||
vitest: 4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(vite@8.2.1(@types/node@26.2.0))
|
||||
|
||||
'@vitest/expect@4.1.10':
|
||||
dependencies:
|
||||
@ -1528,13 +1541,13 @@ snapshots:
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.1.1
|
||||
|
||||
'@vitest/mocker@4.1.10(vite@8.2.1)':
|
||||
'@vitest/mocker@4.1.10(vite@8.2.1(@types/node@26.2.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.1.10
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 8.2.1
|
||||
vite: 8.2.1(@types/node@26.2.0)
|
||||
|
||||
'@vitest/pretty-format@4.1.10':
|
||||
dependencies:
|
||||
@ -2086,13 +2099,15 @@ snapshots:
|
||||
|
||||
typescript@6.0.3: {}
|
||||
|
||||
undici-types@8.3.0: {}
|
||||
|
||||
undici@7.29.0: {}
|
||||
|
||||
uri-js@4.4.1:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
vite@8.2.1:
|
||||
vite@8.2.1(@types/node@26.2.0):
|
||||
dependencies:
|
||||
lightningcss: 1.33.0
|
||||
picomatch: 4.0.5
|
||||
@ -2100,12 +2115,13 @@ snapshots:
|
||||
rolldown: 1.2.4
|
||||
tinyglobby: 0.2.17
|
||||
optionalDependencies:
|
||||
'@types/node': 26.2.0
|
||||
fsevents: 2.3.3
|
||||
|
||||
vitest@4.1.10(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(vite@8.2.1):
|
||||
vitest@4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(vite@8.2.1(@types/node@26.2.0)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.10
|
||||
'@vitest/mocker': 4.1.10(vite@8.2.1)
|
||||
'@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@26.2.0))
|
||||
'@vitest/pretty-format': 4.1.10
|
||||
'@vitest/runner': 4.1.10
|
||||
'@vitest/snapshot': 4.1.10
|
||||
@ -2122,9 +2138,10 @@ snapshots:
|
||||
tinyexec: 1.3.0
|
||||
tinyglobby: 0.2.17
|
||||
tinyrainbow: 3.1.1
|
||||
vite: 8.2.1
|
||||
vite: 8.2.1(@types/node@26.2.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 26.2.0
|
||||
'@vitest/coverage-v8': 4.1.10(vitest@4.1.10)
|
||||
jsdom: 29.1.1
|
||||
transitivePeerDependencies:
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
export function nth(values: readonly number[], index: number): number {
|
||||
const value = values[index];
|
||||
if (value === undefined) {
|
||||
throw new RangeError(`index ${index} out of range`);
|
||||
throw new RangeError(`index ${String(index)} out of range`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@ -136,7 +136,7 @@ describe("RangeSlider", () => {
|
||||
["ArrowUp", 10],
|
||||
])("%s steps the low thumb up", (key, expected) => {
|
||||
const { onChange } = renderSlider();
|
||||
fireEvent.keyDown(thumbs()[0] as HTMLElement, { key });
|
||||
fireEvent.keyDown(thumbs()[0]!, { key });
|
||||
expect(onChange).toHaveBeenCalledWith(expected, 90);
|
||||
});
|
||||
|
||||
@ -145,58 +145,61 @@ describe("RangeSlider", () => {
|
||||
["ArrowDown", 80],
|
||||
])("%s steps the high thumb down", (key, expected) => {
|
||||
const { onChange } = renderSlider();
|
||||
fireEvent.keyDown(thumbs()[1] as HTMLElement, { key });
|
||||
fireEvent.keyDown(thumbs()[1]!, { key });
|
||||
expect(onChange).toHaveBeenCalledWith(0, expected);
|
||||
});
|
||||
|
||||
it("PageUp jumps ten samples", () => {
|
||||
const { onChange } = renderSlider({ lo: 0 });
|
||||
fireEvent.keyDown(thumbs()[0] as HTMLElement, { key: "PageUp" });
|
||||
fireEvent.keyDown(thumbs()[0]!, { key: "PageUp" });
|
||||
expect(onChange).toHaveBeenCalledWith(90, 90);
|
||||
});
|
||||
|
||||
it("PageDown jumps ten samples down", () => {
|
||||
const { onChange } = renderSlider({ hi: 90 });
|
||||
fireEvent.keyDown(thumbs()[1] as HTMLElement, { key: "PageDown" });
|
||||
fireEvent.keyDown(thumbs()[1]!, { key: "PageDown" });
|
||||
expect(onChange).toHaveBeenCalledWith(0, 0);
|
||||
});
|
||||
|
||||
it("Home sends a thumb to the floor", () => {
|
||||
const { onChange } = renderSlider({ lo: 50 });
|
||||
fireEvent.keyDown(thumbs()[0] as HTMLElement, { key: "Home" });
|
||||
fireEvent.keyDown(thumbs()[0]!, { key: "Home" });
|
||||
expect(onChange).toHaveBeenCalledWith(0, 90);
|
||||
});
|
||||
|
||||
it("End sends a thumb to the ceiling", () => {
|
||||
const { onChange } = renderSlider({ hi: 50 });
|
||||
fireEvent.keyDown(thumbs()[1] as HTMLElement, { key: "End" });
|
||||
fireEvent.keyDown(thumbs()[1]!, { key: "End" });
|
||||
expect(onChange).toHaveBeenCalledWith(0, 90);
|
||||
});
|
||||
|
||||
it("ignores keys it does not handle", () => {
|
||||
const { onChange } = renderSlider();
|
||||
fireEvent.keyDown(thumbs()[0] as HTMLElement, { key: "a" });
|
||||
fireEvent.keyDown(thumbs()[0]!, { key: "a" });
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not let the low thumb cross the high one", () => {
|
||||
const { onChange } = renderSlider({ hi: 10, lo: 10 });
|
||||
fireEvent.keyDown(thumbs()[0] as HTMLElement, { key: "ArrowRight" });
|
||||
fireEvent.keyDown(thumbs()[0]!, { key: "ArrowRight" });
|
||||
expect(onChange).toHaveBeenCalledWith(10, 10);
|
||||
});
|
||||
|
||||
it("does not let the high thumb cross the low one", () => {
|
||||
const { onChange } = renderSlider({ hi: 10, lo: 10 });
|
||||
fireEvent.keyDown(thumbs()[1] as HTMLElement, { key: "ArrowLeft" });
|
||||
fireEvent.keyDown(thumbs()[1]!, { key: "ArrowLeft" });
|
||||
expect(onChange).toHaveBeenCalledWith(10, 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pointer", () => {
|
||||
function track(): HTMLElement {
|
||||
// eslint-disable-next-line testing-library/no-node-access -- the track is
|
||||
// presentational; there is no role or text to query it by.
|
||||
return document.querySelector(".slider-track") as HTMLElement;
|
||||
// Queried by class rather than by role: the track is presentational, so
|
||||
// it has neither a role nor text to find it by. (The consumers run
|
||||
// eslint-plugin-testing-library, which flags this; this package does not
|
||||
// install that plugin, so a disable comment here would itself be an
|
||||
// unknown-rule error.)
|
||||
return document.querySelector(".slider-track")!;
|
||||
}
|
||||
|
||||
it("grabs the nearer thumb when pressed anywhere on the track", () => {
|
||||
|
||||
14
web_ui/tsconfig.lint.json
Normal file
14
web_ui/tsconfig.lint.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
// tsconfig.json deliberately excludes tests -- they must not land in the
|
||||
// published dist/. Type-aware linting still needs them in a project, so this
|
||||
// config widens the include for eslint (and `tsc --noEmit`) only. It emits
|
||||
// nothing and clears rootDir, so it cannot affect the shipped build.
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"rootDir": ".",
|
||||
"types": ["vitest/globals", "node"]
|
||||
},
|
||||
"include": ["src", "*.config.ts"],
|
||||
"exclude": []
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user