diff --git a/.dockerignore b/.dockerignore index e9ae029..dc83466 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,30 +1,36 @@ # Dependencies node_modules npm-debug.log +yarn-debug.log +yarn-error.log -# Build outputs +# Build output dist -# Environment and config -.env -.env.* - # Version control .git .gitignore -# IDE +# Environment variables +.env +.env.local +.env.*.local + +# IDE files .vscode .idea -# Testing +# Test files coverage __tests__ +jest.config.js +jest.setup.js +*.test.ts +*.spec.ts -# Logs -*.log - -# Documentation -README.md -LICENSE -CHANGELOG.md \ No newline at end of file +# Other +*.md +.DS_Store +Dockerfile +docker-compose.yml +.dockerignore \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..65880d6 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,49 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking" + ], + "parserOptions": { + "project": "./tsconfig.json", + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + "no-console": "warn", + "@typescript-eslint/explicit-function-return-type": "warn", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/strict-boolean-expressions": "warn", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_" + } + ], + "@typescript-eslint/no-misused-promises": [ + "warn", + { + "checksVoidReturn": { + "attributes": false + } + } + ], + "@typescript-eslint/no-unsafe-assignment": "warn", + "@typescript-eslint/no-unsafe-member-access": "warn", + "@typescript-eslint/no-unsafe-call": "warn", + "@typescript-eslint/no-unsafe-return": "warn" + }, + "ignorePatterns": [ + "dist/", + "node_modules/", + "*.js", + "*.d.ts" + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index e5b3ca2..d1b619c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,38 +1,23 @@ -# Build stage -FROM node:20.10.0-alpine AS builder +# Use Node.js 20.10.0 as the base image +FROM node:20.10.0-slim +# Create app directory WORKDIR /app -# Install TypeScript globally -RUN npm install -g typescript +# Copy package files +COPY package*.json ./ -# Copy source files first -COPY . . - -# Install all dependencies (including dev dependencies) +# Install dependencies RUN npm install -# Build the project +# Copy source code +COPY . . + +# Build the application RUN npm run build -# Production stage -FROM node:20.10.0-alpine +# Expose the port your app runs on (if needed) +# EXPOSE 3000 -WORKDIR /app - -# Set Node options for better compatibility -ENV NODE_OPTIONS="--experimental-modules" -ENV NODE_ENV="production" - -# Copy package files and install production dependencies -COPY package*.json ./ -RUN npm install --omit=dev --ignore-scripts - -# Copy built files from builder stage -COPY --from=builder /app/dist ./dist - -# Expose default port -EXPOSE 3000 - -# Start the server -CMD ["node", "dist/index.js"] \ No newline at end of file +# Start the application +CMD ["npm", "start"] \ No newline at end of file diff --git a/README.md b/README.md index 38a456b..f6e37cd 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,82 @@ npx jest --config=jest.config.js # Run tests } ``` +### Media Player Control +```json +{ + "tool": "control", + "command": "media_play", // or "media_pause", "media_stop", "media_next", "media_previous" + "entity_id": "media_player.living_room", + "volume_level": 0.5, + "source": "Spotify", + "media_content_id": "spotify:playlist:xyz", + "media_content_type": "playlist" +} +``` + +### Fan Control +```json +{ + "tool": "control", + "command": "turn_on", + "entity_id": "fan.bedroom", + "percentage": 50, + "preset_mode": "auto", + "oscillating": true, + "direction": "forward" +} +``` + +### Lock Control +```json +{ + "tool": "control", + "command": "lock", // or "unlock" + "entity_id": "lock.front_door" +} +``` + +### Vacuum Control +```json +{ + "tool": "control", + "command": "start", // or "pause", "stop", "return_to_base", "clean_spot" + "entity_id": "vacuum.robot", + "fan_speed": "medium" +} +``` + +### Scene Control +```json +{ + "tool": "control", + "command": "turn_on", + "entity_id": "scene.movie_night" +} +``` + +### Script Control +```json +{ + "tool": "control", + "command": "turn_on", + "entity_id": "script.welcome_home", + "variables": { + "brightness": 100, + "color": "red" + } +} +``` + +### Camera Control +```json +{ + "tool": "control", + "command": "enable_motion_detection", // or "disable_motion_detection" + "entity_id": "camera.front_door" +} +``` + ## Natural Language Integration ### Example Commands diff --git a/src/__tests__/context.test.ts b/__tests__/context/context.test.ts similarity index 98% rename from src/__tests__/context.test.ts rename to __tests__/context/context.test.ts index 153c940..6a42ebc 100644 --- a/src/__tests__/context.test.ts +++ b/__tests__/context/context.test.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { DomainSchema } from '../schemas.js'; +import { DomainSchema } from '../../src/schemas.js'; // Define types for tool and server interface Tool { @@ -25,7 +25,7 @@ class MockLiteMCP { } // Mock the Home Assistant instance -jest.mock('../hass/index.js', () => ({ +jest.mock('../../src/hass/index.js', () => ({ get_hass: jest.fn().mockResolvedValue({ services: { light: { diff --git a/src/__tests__/hass.test.ts b/__tests__/hass/hass.test.ts similarity index 55% rename from src/__tests__/hass.test.ts rename to __tests__/hass/hass.test.ts index c52f5d1..2069920 100644 --- a/src/__tests__/hass.test.ts +++ b/__tests__/hass/hass.test.ts @@ -1,7 +1,7 @@ -import { get_hass } from '../hass/index.js'; +import { get_hass } from '../../src/hass/index.js'; // Mock the entire module -jest.mock('../hass/index.js', () => { +jest.mock('../../src/hass/index.js', () => { let mockInstance: any = null; return { @@ -25,8 +25,20 @@ jest.mock('../hass/index.js', () => { }); describe('Home Assistant Connection', () => { + // Backup the original environment + const originalEnv = { ...process.env }; + beforeEach(() => { + // Clear all mocks jest.clearAllMocks(); + + // Reset environment variables + process.env = { ...originalEnv }; + }); + + afterAll(() => { + // Restore original environment + process.env = originalEnv; }); it('should return a Home Assistant instance with services', async () => { @@ -45,4 +57,25 @@ describe('Home Assistant Connection', () => { expect(firstInstance).toBe(secondInstance); }); + + it('should use "development" as default environment', async () => { + // Unset NODE_ENV + delete process.env.NODE_ENV; + + const hass = await get_hass(); + + // You might need to add a way to check the environment in your actual implementation + // This is a placeholder and might need adjustment based on your exact implementation + expect(process.env.NODE_ENV).toBe(undefined); + }); + + it('should use process.env.NODE_ENV when set', async () => { + // Set a specific environment + process.env.NODE_ENV = 'production'; + + const hass = await get_hass(); + + // You might need to add a way to check the environment in your actual implementation + expect(process.env.NODE_ENV).toBe('production'); + }); }); \ No newline at end of file diff --git a/__tests__/schemas/devices.test.js b/__tests__/schemas/devices.test.js new file mode 100644 index 0000000..bfc9eda --- /dev/null +++ b/__tests__/schemas/devices.test.js @@ -0,0 +1,186 @@ +import { MediaPlayerSchema, FanSchema, LockSchema, VacuumSchema, SceneSchema, ScriptSchema, CameraSchema, ListMediaPlayersResponseSchema, ListFansResponseSchema, ListLocksResponseSchema, ListVacuumsResponseSchema, ListScenesResponseSchema, ListScriptsResponseSchema, ListCamerasResponseSchema, } from '../../src/schemas'; +describe('Device Schemas', () => { + describe('MediaPlayer Schema', () => { + it('should validate a valid media player entity', () => { + const mediaPlayer = { + entity_id: 'media_player.living_room', + state: 'playing', + state_attributes: { + volume_level: 0.5, + is_volume_muted: false, + media_content_id: 'spotify:playlist:xyz', + media_content_type: 'playlist', + media_title: 'My Playlist', + source: 'Spotify', + source_list: ['Spotify', 'Radio', 'TV'], + supported_features: 12345 + } + }; + expect(() => MediaPlayerSchema.parse(mediaPlayer)).not.toThrow(); + }); + it('should validate media player list response', () => { + const response = { + media_players: [{ + entity_id: 'media_player.living_room', + state: 'playing', + state_attributes: {} + }] + }; + expect(() => ListMediaPlayersResponseSchema.parse(response)).not.toThrow(); + }); + }); + describe('Fan Schema', () => { + it('should validate a valid fan entity', () => { + const fan = { + entity_id: 'fan.bedroom', + state: 'on', + state_attributes: { + percentage: 50, + preset_mode: 'auto', + preset_modes: ['auto', 'low', 'medium', 'high'], + oscillating: true, + direction: 'forward', + supported_features: 12345 + } + }; + expect(() => FanSchema.parse(fan)).not.toThrow(); + }); + it('should validate fan list response', () => { + const response = { + fans: [{ + entity_id: 'fan.bedroom', + state: 'on', + state_attributes: {} + }] + }; + expect(() => ListFansResponseSchema.parse(response)).not.toThrow(); + }); + }); + describe('Lock Schema', () => { + it('should validate a valid lock entity', () => { + const lock = { + entity_id: 'lock.front_door', + state: 'locked', + state_attributes: { + code_format: 'number', + changed_by: 'User', + locked: true, + supported_features: 12345 + } + }; + expect(() => LockSchema.parse(lock)).not.toThrow(); + }); + it('should validate lock list response', () => { + const response = { + locks: [{ + entity_id: 'lock.front_door', + state: 'locked', + state_attributes: { locked: true } + }] + }; + expect(() => ListLocksResponseSchema.parse(response)).not.toThrow(); + }); + }); + describe('Vacuum Schema', () => { + it('should validate a valid vacuum entity', () => { + const vacuum = { + entity_id: 'vacuum.robot', + state: 'cleaning', + state_attributes: { + battery_level: 80, + fan_speed: 'medium', + fan_speed_list: ['low', 'medium', 'high'], + status: 'cleaning', + supported_features: 12345 + } + }; + expect(() => VacuumSchema.parse(vacuum)).not.toThrow(); + }); + it('should validate vacuum list response', () => { + const response = { + vacuums: [{ + entity_id: 'vacuum.robot', + state: 'cleaning', + state_attributes: {} + }] + }; + expect(() => ListVacuumsResponseSchema.parse(response)).not.toThrow(); + }); + }); + describe('Scene Schema', () => { + it('should validate a valid scene entity', () => { + const scene = { + entity_id: 'scene.movie_night', + state: 'on', + state_attributes: { + entity_id: ['light.living_room', 'media_player.tv'], + supported_features: 12345 + } + }; + expect(() => SceneSchema.parse(scene)).not.toThrow(); + }); + it('should validate scene list response', () => { + const response = { + scenes: [{ + entity_id: 'scene.movie_night', + state: 'on', + state_attributes: {} + }] + }; + expect(() => ListScenesResponseSchema.parse(response)).not.toThrow(); + }); + }); + describe('Script Schema', () => { + it('should validate a valid script entity', () => { + const script = { + entity_id: 'script.welcome_home', + state: 'on', + state_attributes: { + last_triggered: '2023-12-25T12:00:00Z', + mode: 'single', + variables: { + brightness: 100, + color: 'red' + }, + supported_features: 12345 + } + }; + expect(() => ScriptSchema.parse(script)).not.toThrow(); + }); + it('should validate script list response', () => { + const response = { + scripts: [{ + entity_id: 'script.welcome_home', + state: 'on', + state_attributes: {} + }] + }; + expect(() => ListScriptsResponseSchema.parse(response)).not.toThrow(); + }); + }); + describe('Camera Schema', () => { + it('should validate a valid camera entity', () => { + const camera = { + entity_id: 'camera.front_door', + state: 'recording', + state_attributes: { + motion_detection: true, + frontend_stream_type: 'hls', + supported_features: 12345 + } + }; + expect(() => CameraSchema.parse(camera)).not.toThrow(); + }); + it('should validate camera list response', () => { + const response = { + cameras: [{ + entity_id: 'camera.front_door', + state: 'recording', + state_attributes: {} + }] + }; + expect(() => ListCamerasResponseSchema.parse(response)).not.toThrow(); + }); + }); +}); +//# sourceMappingURL=devices.test.js.map \ No newline at end of file diff --git a/__tests__/schemas/devices.test.js.map b/__tests__/schemas/devices.test.js.map new file mode 100644 index 0000000..37b57d5 --- /dev/null +++ b/__tests__/schemas/devices.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"devices.test.js","sourceRoot":"","sources":["devices.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/__tests__/schemas/devices.test.ts b/__tests__/schemas/devices.test.ts new file mode 100644 index 0000000..521cc40 --- /dev/null +++ b/__tests__/schemas/devices.test.ts @@ -0,0 +1,214 @@ +import { + MediaPlayerSchema, + FanSchema, + LockSchema, + VacuumSchema, + SceneSchema, + ScriptSchema, + CameraSchema, + ListMediaPlayersResponseSchema, + ListFansResponseSchema, + ListLocksResponseSchema, + ListVacuumsResponseSchema, + ListScenesResponseSchema, + ListScriptsResponseSchema, + ListCamerasResponseSchema, +} from '../../src/schemas.js'; + +describe('Device Schemas', () => { + describe('Media Player Schema', () => { + it('should validate a valid media player entity', () => { + const mediaPlayer = { + entity_id: 'media_player.living_room', + state: 'playing', + state_attributes: { + volume_level: 0.5, + is_volume_muted: false, + media_content_id: 'spotify:playlist:xyz', + media_content_type: 'playlist', + media_title: 'My Playlist', + source: 'Spotify', + source_list: ['Spotify', 'Radio', 'TV'], + supported_features: 12345 + } + }; + expect(() => MediaPlayerSchema.parse(mediaPlayer)).not.toThrow(); + }); + + it('should validate media player list response', () => { + const response = { + media_players: [{ + entity_id: 'media_player.living_room', + state: 'playing', + state_attributes: {} + }] + }; + expect(() => ListMediaPlayersResponseSchema.parse(response)).not.toThrow(); + }); + }); + + describe('Fan Schema', () => { + it('should validate a valid fan entity', () => { + const fan = { + entity_id: 'fan.bedroom', + state: 'on', + state_attributes: { + percentage: 50, + preset_mode: 'auto', + preset_modes: ['auto', 'low', 'medium', 'high'], + oscillating: true, + direction: 'forward', + supported_features: 12345 + } + }; + expect(() => FanSchema.parse(fan)).not.toThrow(); + }); + + it('should validate fan list response', () => { + const response = { + fans: [{ + entity_id: 'fan.bedroom', + state: 'on', + state_attributes: {} + }] + }; + expect(() => ListFansResponseSchema.parse(response)).not.toThrow(); + }); + }); + + describe('Lock Schema', () => { + it('should validate a valid lock entity', () => { + const lock = { + entity_id: 'lock.front_door', + state: 'locked', + state_attributes: { + code_format: 'number', + changed_by: 'User', + locked: true, + supported_features: 12345 + } + }; + expect(() => LockSchema.parse(lock)).not.toThrow(); + }); + + it('should validate lock list response', () => { + const response = { + locks: [{ + entity_id: 'lock.front_door', + state: 'locked', + state_attributes: { locked: true } + }] + }; + expect(() => ListLocksResponseSchema.parse(response)).not.toThrow(); + }); + }); + + describe('Vacuum Schema', () => { + it('should validate a valid vacuum entity', () => { + const vacuum = { + entity_id: 'vacuum.robot', + state: 'cleaning', + state_attributes: { + battery_level: 80, + fan_speed: 'medium', + fan_speed_list: ['low', 'medium', 'high'], + status: 'cleaning', + supported_features: 12345 + } + }; + expect(() => VacuumSchema.parse(vacuum)).not.toThrow(); + }); + + it('should validate vacuum list response', () => { + const response = { + vacuums: [{ + entity_id: 'vacuum.robot', + state: 'cleaning', + state_attributes: {} + }] + }; + expect(() => ListVacuumsResponseSchema.parse(response)).not.toThrow(); + }); + }); + + describe('Scene Schema', () => { + it('should validate a valid scene entity', () => { + const scene = { + entity_id: 'scene.movie_night', + state: 'on', + state_attributes: { + entity_id: ['light.living_room', 'media_player.tv'], + supported_features: 12345 + } + }; + expect(() => SceneSchema.parse(scene)).not.toThrow(); + }); + + it('should validate scene list response', () => { + const response = { + scenes: [{ + entity_id: 'scene.movie_night', + state: 'on', + state_attributes: {} + }] + }; + expect(() => ListScenesResponseSchema.parse(response)).not.toThrow(); + }); + }); + + describe('Script Schema', () => { + it('should validate a valid script entity', () => { + const script = { + entity_id: 'script.welcome_home', + state: 'on', + state_attributes: { + last_triggered: '2023-12-25T12:00:00Z', + mode: 'single', + variables: { + brightness: 100, + color: 'red' + }, + supported_features: 12345 + } + }; + expect(() => ScriptSchema.parse(script)).not.toThrow(); + }); + + it('should validate script list response', () => { + const response = { + scripts: [{ + entity_id: 'script.welcome_home', + state: 'on', + state_attributes: {} + }] + }; + expect(() => ListScriptsResponseSchema.parse(response)).not.toThrow(); + }); + }); + + describe('Camera Schema', () => { + it('should validate a valid camera entity', () => { + const camera = { + entity_id: 'camera.front_door', + state: 'recording', + state_attributes: { + motion_detection: true, + frontend_stream_type: 'hls', + supported_features: 12345 + } + }; + expect(() => CameraSchema.parse(camera)).not.toThrow(); + }); + + it('should validate camera list response', () => { + const response = { + cameras: [{ + entity_id: 'camera.front_door', + state: 'recording', + state_attributes: {} + }] + }; + expect(() => ListCamerasResponseSchema.parse(response)).not.toThrow(); + }); + }); +}); \ No newline at end of file diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/coverage/lcov-report/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js new file mode 100644 index 0000000..cc12130 --- /dev/null +++ b/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selecter that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/lcov-report/favicon.png b/coverage/lcov-report/favicon.png new file mode 100644 index 0000000..c1525b8 Binary files /dev/null and b/coverage/lcov-report/favicon.png differ diff --git a/coverage/lcov-report/index.html b/coverage/lcov-report/index.html new file mode 100644 index 0000000..ae7a774 --- /dev/null +++ b/coverage/lcov-report/index.html @@ -0,0 +1,116 @@ + + + + +
++ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| schemas.ts | +
+
+ |
+ 100% | +32/32 | +100% | +0/0 | +100% | +0/0 | +100% | +32/32 | +