Merge pull request 'test: add Vitest suite with 86% coverage threshold' (#16) from feat/add-tests into main
Reviewed-on: https://git.home.ms/jarianc/MovieMapper/pulls/16
This commit is contained in:
commit
7102dbe78b
@ -13,7 +13,7 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: gitea-job-image
|
image: node:20-bookworm
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
run: |
|
run: |
|
||||||
@ -21,16 +21,6 @@ jobs:
|
|||||||
git clone --depth 1 $GITEA_URL/$GITHUB_REPOSITORY $GITHUB_WORKSPACE
|
git clone --depth 1 $GITEA_URL/$GITHUB_REPOSITORY $GITHUB_WORKSPACE
|
||||||
git -C $GITHUB_WORKSPACE checkout $GITHUB_SHA 2>/dev/null || true
|
git -C $GITHUB_WORKSPACE checkout $GITHUB_SHA 2>/dev/null || true
|
||||||
|
|
||||||
- name: Run ruff (Python lint)
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [[ -f pyproject.toml ]]; then
|
|
||||||
pip3 install ruff
|
|
||||||
ruff check .
|
|
||||||
else
|
|
||||||
echo "No Python project detected, skipping ruff"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run npm lint (JS/TS)
|
- name: Run npm lint (JS/TS)
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
@ -44,7 +34,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: gitea-job-image
|
image: node:20-bookworm
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
run: |
|
run: |
|
||||||
@ -52,36 +42,11 @@ jobs:
|
|||||||
git clone --depth 1 $GITEA_URL/$GITHUB_REPOSITORY $GITHUB_WORKSPACE
|
git clone --depth 1 $GITEA_URL/$GITHUB_REPOSITORY $GITHUB_WORKSPACE
|
||||||
git -C $GITHUB_WORKSPACE checkout $GITHUB_SHA 2>/dev/null || true
|
git -C $GITHUB_WORKSPACE checkout $GITHUB_SHA 2>/dev/null || true
|
||||||
|
|
||||||
- name: Run pytest (Python)
|
- name: Install dependencies
|
||||||
if: always()
|
run: npm ci
|
||||||
run: |
|
|
||||||
if [[ -f pyproject.toml ]]; then
|
|
||||||
python3 -m pip install --upgrade pip
|
|
||||||
pip3 install -e ".[dev]" 2>/dev/null || pip3 install -e . 2>/dev/null || true
|
|
||||||
pip3 install pytest
|
|
||||||
pytest tests/ -v --tb=short 2>/dev/null || true
|
|
||||||
else
|
|
||||||
echo "No Python project detected, skipping pytest"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run npm test (JS/TS)
|
- name: Run tests with coverage
|
||||||
if: always()
|
run: npm test
|
||||||
run: |
|
|
||||||
if [[ -f package.json ]]; then
|
|
||||||
npm ci
|
|
||||||
npm run test --if-present || true
|
|
||||||
else
|
|
||||||
echo "No Node.js project detected, skipping npm test"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run Go tests
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [[ -f go.mod ]]; then
|
|
||||||
go test ./...
|
|
||||||
else
|
|
||||||
echo "No Go project detected, skipping go test"
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -104,7 +69,7 @@ jobs:
|
|||||||
security:
|
security:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: gitea-job-image
|
image: node:20-bookworm
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
run: |
|
run: |
|
||||||
@ -112,16 +77,6 @@ jobs:
|
|||||||
git clone --depth 1 $GITEA_URL/$GITHUB_REPOSITORY $GITHUB_WORKSPACE
|
git clone --depth 1 $GITEA_URL/$GITHUB_REPOSITORY $GITHUB_WORKSPACE
|
||||||
git -C $GITHUB_WORKSPACE checkout $GITHUB_SHA 2>/dev/null || true
|
git -C $GITHUB_WORKSPACE checkout $GITHUB_SHA 2>/dev/null || true
|
||||||
|
|
||||||
- name: Run bandit (Python SAST)
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [[ -f pyproject.toml ]]; then
|
|
||||||
pip3 install bandit
|
|
||||||
bandit -r . --severity-level high --confidence-level high --exclude tests/,test_*
|
|
||||||
else
|
|
||||||
echo "No Python project detected, skipping bandit"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run npm audit (JS/TS)
|
- name: Run npm audit (JS/TS)
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
@ -135,8 +90,6 @@ jobs:
|
|||||||
build-result:
|
build-result:
|
||||||
needs: [lint, test, docker-build, security]
|
needs: [lint, test, docker-build, security]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
|
||||||
image: gitea-job-image
|
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- name: Summary
|
- name: Summary
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -50,3 +50,6 @@ dist/
|
|||||||
# Temporary files
|
# Temporary files
|
||||||
*.tmp
|
*.tmp
|
||||||
*.temp
|
*.temp
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
3067
package-lock.json
generated
3067
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,12 +5,17 @@
|
|||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "vitest run --coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.13.5",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"electron": "^40.4.1",
|
"electron": "^40.4.1",
|
||||||
"fluent-ffmpeg": "^2.1.3"
|
"fluent-ffmpeg": "^2.1.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitest/coverage-v8": "^3.2.6",
|
||||||
|
"jsdom": "^25.0.1",
|
||||||
|
"vitest": "^3.2.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
190
tests/AppState.test.js
Normal file
190
tests/AppState.test.js
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import AppState from '../utils/renderer/AppState';
|
||||||
|
|
||||||
|
describe('AppState', () => {
|
||||||
|
let state;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
state = new AppState();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('initializes with default values', () => {
|
||||||
|
expect(state.getCurrentDirectory()).toBeNull();
|
||||||
|
expect(state.getCurrentFiles()).toEqual([]);
|
||||||
|
expect(state.getCurrentShow()).toBeNull();
|
||||||
|
expect(state.getCurrentSeasons()).toEqual([]);
|
||||||
|
expect(state.getCurrentEpisodes()).toEqual([]);
|
||||||
|
expect(state.getSelectedSeasonEpisodeCount()).toBe(0);
|
||||||
|
expect(state.isUpdatingEpisodeNumbers).toBe(false);
|
||||||
|
expect(state.getNavigationStack()).toEqual([]);
|
||||||
|
expect(state.getNavigationDepth()).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('currentDirectory', () => {
|
||||||
|
it('sets and gets directory', () => {
|
||||||
|
state.setCurrentDirectory('/path/to/dir');
|
||||||
|
expect(state.getCurrentDirectory()).toBe('/path/to/dir');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('overwrites previous directory', () => {
|
||||||
|
state.setCurrentDirectory('/path/one');
|
||||||
|
state.setCurrentDirectory('/path/two');
|
||||||
|
expect(state.getCurrentDirectory()).toBe('/path/two');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('currentFiles', () => {
|
||||||
|
it('sets and gets files array', () => {
|
||||||
|
const files = [{ name: 'file1.mp4' }, { name: 'file2.mp4' }];
|
||||||
|
state.setCurrentFiles(files);
|
||||||
|
expect(state.getCurrentFiles()).toEqual(files);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty array', () => {
|
||||||
|
state.setCurrentFiles([]);
|
||||||
|
expect(state.getCurrentFiles()).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('currentShow', () => {
|
||||||
|
it('sets and gets show object', () => {
|
||||||
|
const show = { id: 'series-12345', name: 'Test Show' };
|
||||||
|
state.setCurrentShow(show);
|
||||||
|
expect(state.getCurrentShow()).toEqual(show);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('currentSeasons', () => {
|
||||||
|
it('sets and gets seasons array', () => {
|
||||||
|
const seasons = [{ number: 1 }, { number: 2 }];
|
||||||
|
state.setCurrentSeasons(seasons);
|
||||||
|
expect(state.getCurrentSeasons()).toEqual(seasons);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('currentEpisodes', () => {
|
||||||
|
it('sets and gets episodes array', () => {
|
||||||
|
const episodes = [{ number: 1 }, { number: 2 }];
|
||||||
|
state.setCurrentEpisodes(episodes);
|
||||||
|
expect(state.getCurrentEpisodes()).toEqual(episodes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('selectedSeasonEpisodeCount', () => {
|
||||||
|
it('sets and gets episode count', () => {
|
||||||
|
state.setSelectedSeasonEpisodeCount(13);
|
||||||
|
expect(state.getSelectedSeasonEpisodeCount()).toBe(13);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updatingEpisodeNumbers', () => {
|
||||||
|
it('sets and gets updating flag', () => {
|
||||||
|
state.setUpdatingEpisodeNumbers(true);
|
||||||
|
expect(state.isUpdatingEpisodeNumbers).toBe(true);
|
||||||
|
state.setUpdatingEpisodeNumbers(false);
|
||||||
|
expect(state.isUpdatingEpisodeNumbers).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('reset', () => {
|
||||||
|
it('resets all state to defaults', () => {
|
||||||
|
state.setCurrentDirectory('/path/to/dir');
|
||||||
|
state.setCurrentFiles([{ name: 'file.mp4' }]);
|
||||||
|
state.setCurrentShow({ name: 'Show' });
|
||||||
|
state.setCurrentSeasons([{ number: 1 }]);
|
||||||
|
state.setCurrentEpisodes([{ number: 1 }]);
|
||||||
|
state.setSelectedSeasonEpisodeCount(10);
|
||||||
|
state.setUpdatingEpisodeNumbers(true);
|
||||||
|
state.addToNavigationStack('/path/to/dir');
|
||||||
|
|
||||||
|
state.reset();
|
||||||
|
|
||||||
|
expect(state.getCurrentDirectory()).toBeNull();
|
||||||
|
expect(state.getCurrentFiles()).toEqual([]);
|
||||||
|
expect(state.getCurrentShow()).toBeNull();
|
||||||
|
expect(state.getCurrentSeasons()).toEqual([]);
|
||||||
|
expect(state.getCurrentEpisodes()).toEqual([]);
|
||||||
|
expect(state.getSelectedSeasonEpisodeCount()).toBe(0);
|
||||||
|
expect(state.isUpdatingEpisodeNumbers).toBe(false);
|
||||||
|
expect(state.getNavigationStack()).toEqual([]);
|
||||||
|
expect(state.getNavigationDepth()).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('navigationStack', () => {
|
||||||
|
it('adds directories to stack', () => {
|
||||||
|
state.addToNavigationStack('/home');
|
||||||
|
expect(state.getNavigationStack()).toEqual(['/home']);
|
||||||
|
expect(state.getNavigationDepth()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports multiple navigation levels', () => {
|
||||||
|
state.addToNavigationStack('/home');
|
||||||
|
state.addToNavigationStack('/home/movies');
|
||||||
|
state.addToNavigationStack('/home/movies/show');
|
||||||
|
expect(state.getNavigationStack().length).toBe(3);
|
||||||
|
expect(state.getNavigationDepth()).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns current directory from stack', () => {
|
||||||
|
state.addToNavigationStack('/home');
|
||||||
|
state.addToNavigationStack('/home/movies');
|
||||||
|
expect(state.getCurrentDirectoryFromStack()).toBe('/home/movies');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when stack is empty', () => {
|
||||||
|
expect(state.getCurrentDirectoryFromStack()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('truncates forward history on new navigation', () => {
|
||||||
|
state.addToNavigationStack('/home');
|
||||||
|
state.addToNavigationStack('/home/movies');
|
||||||
|
state.goBack();
|
||||||
|
state.addToNavigationStack('/home/photos');
|
||||||
|
expect(state.getNavigationStack()).toEqual(['/home', '/home/photos']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('goes back to previous directory', () => {
|
||||||
|
state.addToNavigationStack('/home');
|
||||||
|
state.addToNavigationStack('/home/movies');
|
||||||
|
const result = state.goBack();
|
||||||
|
expect(result).toBe('/home');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when cannot go back', () => {
|
||||||
|
state.addToNavigationStack('/home');
|
||||||
|
const result = state.goBack();
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null on empty stack goBack', () => {
|
||||||
|
const result = state.goBack();
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('canGoBack returns correct state', () => {
|
||||||
|
expect(state.canGoBack()).toBe(false);
|
||||||
|
state.addToNavigationStack('/home');
|
||||||
|
expect(state.canGoBack()).toBe(false);
|
||||||
|
state.addToNavigationStack('/home/movies');
|
||||||
|
expect(state.canGoBack()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('canGoBack becomes false after going back to root', () => {
|
||||||
|
state.addToNavigationStack('/home');
|
||||||
|
state.addToNavigationStack('/home/movies');
|
||||||
|
state.goBack();
|
||||||
|
expect(state.canGoBack()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles deep navigation', () => {
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
state.addToNavigationStack(`/level${i}`);
|
||||||
|
}
|
||||||
|
expect(state.getNavigationDepth()).toBe(4);
|
||||||
|
expect(state.getNavigationStack().length).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
283
tests/EpisodeManager.test.js
Normal file
283
tests/EpisodeManager.test.js
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import EpisodeManager from '../utils/renderer/EpisodeManager';
|
||||||
|
|
||||||
|
describe('EpisodeManager', () => {
|
||||||
|
let em;
|
||||||
|
let fileListEl;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
em = new EpisodeManager();
|
||||||
|
fileListEl = document.createElement('div');
|
||||||
|
fileListEl.id = 'file-list';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('initializes with isUpdatingEpisodeNumbers false', () => {
|
||||||
|
expect(em.getIsUpdatingEpisodeNumbers()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateEpisodeNumbers', () => {
|
||||||
|
it('updates episode numbers sequentially', () => {
|
||||||
|
const item1 = document.createElement('div');
|
||||||
|
item1.className = 'file-item';
|
||||||
|
const ep1 = document.createElement('span');
|
||||||
|
ep1.className = 'episode-number';
|
||||||
|
ep1.dataset.episodeStart = '';
|
||||||
|
ep1.dataset.episodeEnd = '';
|
||||||
|
item1.appendChild(ep1);
|
||||||
|
|
||||||
|
const item2 = document.createElement('div');
|
||||||
|
item2.className = 'file-item';
|
||||||
|
const ep2 = document.createElement('span');
|
||||||
|
ep2.className = 'episode-number';
|
||||||
|
ep2.dataset.episodeStart = '';
|
||||||
|
ep2.dataset.episodeEnd = '';
|
||||||
|
item2.appendChild(ep2);
|
||||||
|
|
||||||
|
fileListEl.appendChild(item1);
|
||||||
|
fileListEl.appendChild(item2);
|
||||||
|
|
||||||
|
em.updateEpisodeNumbers(fileListEl);
|
||||||
|
expect(ep1.textContent).toBe('1');
|
||||||
|
expect(ep2.textContent).toBe('2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips re-entrant calls', () => {
|
||||||
|
em.setIsUpdatingEpisodeNumbers(true);
|
||||||
|
em.updateEpisodeNumbers(fileListEl);
|
||||||
|
expect(em.getIsUpdatingEpisodeNumbers()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects stored episode ranges', () => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'file-item';
|
||||||
|
const ep = document.createElement('span');
|
||||||
|
ep.className = 'episode-number';
|
||||||
|
ep.dataset.episodeStart = '5';
|
||||||
|
ep.dataset.episodeEnd = '7';
|
||||||
|
item.appendChild(ep);
|
||||||
|
fileListEl.appendChild(item);
|
||||||
|
|
||||||
|
em.updateEpisodeNumbers(fileListEl);
|
||||||
|
expect(ep.textContent).toBe('5-7');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setIsUpdatingEpisodeNumbers', () => {
|
||||||
|
it('sets the flag', () => {
|
||||||
|
em.setIsUpdatingEpisodeNumbers(true);
|
||||||
|
expect(em.getIsUpdatingEpisodeNumbers()).toBe(true);
|
||||||
|
em.setIsUpdatingEpisodeNumbers(false);
|
||||||
|
expect(em.getIsUpdatingEpisodeNumbers()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('highlightEpisodes', () => {
|
||||||
|
it('highlights episodes in range', () => {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.id = 'episodes-container';
|
||||||
|
|
||||||
|
const ep1 = document.createElement('div');
|
||||||
|
ep1.className = 'episode-item';
|
||||||
|
ep1.textContent = 'E1 Pilot';
|
||||||
|
|
||||||
|
const ep2 = document.createElement('div');
|
||||||
|
ep2.className = 'episode-item';
|
||||||
|
ep2.textContent = 'E2 Second';
|
||||||
|
|
||||||
|
const ep3 = document.createElement('div');
|
||||||
|
ep3.className = 'episode-item';
|
||||||
|
ep3.textContent = 'E3 Third';
|
||||||
|
|
||||||
|
container.appendChild(ep1);
|
||||||
|
container.appendChild(ep2);
|
||||||
|
container.appendChild(ep3);
|
||||||
|
|
||||||
|
em.highlightEpisodes(1, 2, container);
|
||||||
|
expect(ep1.classList.contains('highlighted-episode')).toBe(true);
|
||||||
|
expect(ep2.classList.contains('highlighted-episode')).toBe(true);
|
||||||
|
expect(ep3.classList.contains('highlighted-episode')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles null container', () => {
|
||||||
|
expect(() => em.highlightEpisodes(1, 2, null)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeEpisodeHighlights', () => {
|
||||||
|
it('removes highlights', () => {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const ep = document.createElement('div');
|
||||||
|
ep.className = 'episode-item highlighted-episode';
|
||||||
|
container.appendChild(ep);
|
||||||
|
|
||||||
|
em.removeEpisodeHighlights(container);
|
||||||
|
expect(ep.classList.contains('highlighted-episode')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles null container', () => {
|
||||||
|
expect(() => em.removeEpisodeHighlights(null)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getEpisodeRange', () => {
|
||||||
|
it('returns range from data attributes', () => {
|
||||||
|
const epEl = document.createElement('span');
|
||||||
|
epEl.dataset.episodeStart = '3';
|
||||||
|
epEl.dataset.episodeEnd = '5';
|
||||||
|
const range = em.getEpisodeRange(epEl);
|
||||||
|
expect(range).toEqual({ start: 3, end: 5 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to 1 when no data', () => {
|
||||||
|
const epEl = document.createElement('span');
|
||||||
|
const range = em.getEpisodeRange(epEl);
|
||||||
|
expect(range).toEqual({ start: 1, end: 1 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('calculateTotalEpisodeCount', () => {
|
||||||
|
it('sums episode ranges', () => {
|
||||||
|
const item1 = document.createElement('div');
|
||||||
|
item1.className = 'file-item';
|
||||||
|
const ep1 = document.createElement('span');
|
||||||
|
ep1.className = 'episode-number';
|
||||||
|
ep1.dataset.episodeStart = '1';
|
||||||
|
ep1.dataset.episodeEnd = '1';
|
||||||
|
item1.appendChild(ep1);
|
||||||
|
|
||||||
|
const item2 = document.createElement('div');
|
||||||
|
item2.className = 'file-item';
|
||||||
|
const ep2 = document.createElement('span');
|
||||||
|
ep2.className = 'episode-number';
|
||||||
|
ep2.dataset.episodeStart = '2';
|
||||||
|
ep2.dataset.episodeEnd = '3';
|
||||||
|
item2.appendChild(ep2);
|
||||||
|
|
||||||
|
fileListEl.appendChild(item1);
|
||||||
|
fileListEl.appendChild(item2);
|
||||||
|
|
||||||
|
expect(em.calculateTotalEpisodeCount(fileListEl)).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 0 for empty list', () => {
|
||||||
|
expect(em.calculateTotalEpisodeCount(fileListEl)).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLastEpisodeEnd', () => {
|
||||||
|
it('returns highest episode end', () => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'file-item';
|
||||||
|
const ep = document.createElement('span');
|
||||||
|
ep.className = 'episode-number';
|
||||||
|
ep.dataset.episodeEnd = '13';
|
||||||
|
item.appendChild(ep);
|
||||||
|
fileListEl.appendChild(item);
|
||||||
|
|
||||||
|
expect(em.getLastEpisodeEnd(fileListEl)).toBe(13);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 0 for empty list', () => {
|
||||||
|
expect(em.getLastEpisodeEnd(fileListEl)).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleEpisodeArrowClick', () => {
|
||||||
|
it('increases range with right arrow', () => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'file-item';
|
||||||
|
item.dataset.filePath = '/path/file.mp4';
|
||||||
|
const ep = document.createElement('span');
|
||||||
|
ep.className = 'episode-number';
|
||||||
|
ep.dataset.episodeStart = '1';
|
||||||
|
ep.dataset.episodeEnd = '1';
|
||||||
|
item.appendChild(ep);
|
||||||
|
|
||||||
|
const arrow = document.createElement('button');
|
||||||
|
arrow.className = 'episode-arrow-right';
|
||||||
|
item.appendChild(arrow);
|
||||||
|
|
||||||
|
fileListEl.appendChild(item);
|
||||||
|
|
||||||
|
em.handleEpisodeArrowClick(arrow, 'right', fileListEl);
|
||||||
|
expect(ep.dataset.episodeEnd).toBe('2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decreases range with left arrow', () => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'file-item';
|
||||||
|
item.dataset.filePath = '/path/file.mp4';
|
||||||
|
const ep = document.createElement('span');
|
||||||
|
ep.className = 'episode-number';
|
||||||
|
ep.dataset.episodeStart = '3';
|
||||||
|
ep.dataset.episodeEnd = '3';
|
||||||
|
item.appendChild(ep);
|
||||||
|
|
||||||
|
const arrow = document.createElement('button');
|
||||||
|
arrow.className = 'episode-arrow-left';
|
||||||
|
item.appendChild(arrow);
|
||||||
|
|
||||||
|
fileListEl.appendChild(item);
|
||||||
|
|
||||||
|
em.handleEpisodeArrowClick(arrow, 'left', fileListEl);
|
||||||
|
expect(ep.dataset.episodeStart).toBe('2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not decrease below 1', () => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'file-item';
|
||||||
|
item.dataset.filePath = '/path/file.mp4';
|
||||||
|
const ep = document.createElement('span');
|
||||||
|
ep.className = 'episode-number';
|
||||||
|
ep.dataset.episodeStart = '1';
|
||||||
|
ep.dataset.episodeEnd = '1';
|
||||||
|
item.appendChild(ep);
|
||||||
|
|
||||||
|
const arrow = document.createElement('button');
|
||||||
|
arrow.className = 'episode-arrow-left';
|
||||||
|
item.appendChild(arrow);
|
||||||
|
|
||||||
|
fileListEl.appendChild(item);
|
||||||
|
|
||||||
|
em.handleEpisodeArrowClick(arrow, 'left', fileListEl);
|
||||||
|
expect(ep.dataset.episodeStart).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns early when no episode element', () => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'file-item';
|
||||||
|
|
||||||
|
const arrow = document.createElement('button');
|
||||||
|
arrow.className = 'episode-arrow-right';
|
||||||
|
item.appendChild(arrow);
|
||||||
|
|
||||||
|
fileListEl.appendChild(item);
|
||||||
|
|
||||||
|
expect(() => em.handleEpisodeArrowClick(arrow, 'right', fileListEl)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('makeEpisodeRangeEditable', () => {
|
||||||
|
it('makes element editable', () => {
|
||||||
|
const el = document.createElement('span');
|
||||||
|
el.textContent = '1';
|
||||||
|
el.dataset.episodeStart = '1';
|
||||||
|
el.dataset.episodeEnd = '1';
|
||||||
|
|
||||||
|
em.makeEpisodeRangeEditable(el);
|
||||||
|
expect(el.contentEditable).toBe('true');
|
||||||
|
expect(el.classList.contains('editing')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns early if already editable', () => {
|
||||||
|
const el = document.createElement('span');
|
||||||
|
el.contentEditable = 'true';
|
||||||
|
const focusSpy = vi.spyOn(el, 'focus');
|
||||||
|
|
||||||
|
em.makeEpisodeRangeEditable(el);
|
||||||
|
expect(focusSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
196
tests/FileListManager.test.js
Normal file
196
tests/FileListManager.test.js
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import FileListManager from '../utils/renderer/FileListManager';
|
||||||
|
|
||||||
|
describe('FileListManager', () => {
|
||||||
|
let fileListEl;
|
||||||
|
let flm;
|
||||||
|
let folderClickSpy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fileListEl = document.createElement('div');
|
||||||
|
fileListEl.id = 'file-list';
|
||||||
|
folderClickSpy = vi.fn();
|
||||||
|
flm = new FileListManager(fileListEl, folderClickSpy);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('initializes with null draggedItem', () => {
|
||||||
|
expect(flm.draggedItem).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts null folder click callback', () => {
|
||||||
|
const manager = new FileListManager(fileListEl, null);
|
||||||
|
expect(manager.onFolderClick).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('displayFiles', () => {
|
||||||
|
it('shows empty message when no files', () => {
|
||||||
|
flm.displayFiles([]);
|
||||||
|
expect(fileListEl.textContent).toContain('No media files found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays folders and media files', () => {
|
||||||
|
const files = [
|
||||||
|
{ isFolder: true, path: '/path/folder', name: 'Extras' },
|
||||||
|
{ isFolder: false, path: '/path/file.mp4', name: 'episode.mp4', duration: '45:00', quality: '1080p', fps: '30fps' },
|
||||||
|
];
|
||||||
|
flm.displayFiles(files);
|
||||||
|
const items = fileListEl.querySelectorAll('.file-item');
|
||||||
|
expect(items.length).toBe(2);
|
||||||
|
expect(items[0].classList.contains('folder-item')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts folders before files', () => {
|
||||||
|
const files = [
|
||||||
|
{ isFolder: false, path: '/path/file.mp4', name: 'episode.mp4', duration: '45:00' },
|
||||||
|
{ isFolder: true, path: '/path/folder', name: 'Extras' },
|
||||||
|
];
|
||||||
|
flm.displayFiles(files);
|
||||||
|
const items = fileListEl.querySelectorAll('.file-item');
|
||||||
|
expect(items[0].classList.contains('folder-item')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates folder element with click handler', () => {
|
||||||
|
const files = [{ isFolder: true, path: '/path/folder', name: 'Extras' }];
|
||||||
|
flm.displayFiles(files);
|
||||||
|
const folderItem = fileListEl.querySelector('.folder-item');
|
||||||
|
expect(folderItem).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates media file with episode number', () => {
|
||||||
|
const files = [
|
||||||
|
{ isFolder: false, path: '/path/file.mp4', name: 'episode.mp4', duration: '45:00', quality: '1080p', fps: '30fps' },
|
||||||
|
];
|
||||||
|
flm.displayFiles(files);
|
||||||
|
const episodeEl = fileListEl.querySelector('.episode-number');
|
||||||
|
expect(episodeEl).not.toBeNull();
|
||||||
|
expect(episodeEl.textContent).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks problematic files with warning icon', () => {
|
||||||
|
const files = [
|
||||||
|
{ isFolder: false, path: '/path/bad.mp4', name: 'bad.mp4', duration: '00:00', isProblematic: true },
|
||||||
|
];
|
||||||
|
flm.displayFiles(files);
|
||||||
|
const warnLabel = fileListEl.querySelector('.problematic-label');
|
||||||
|
expect(warnLabel).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateEpisodeNumbers', () => {
|
||||||
|
it('updates episode numbers sequentially', () => {
|
||||||
|
const files = [
|
||||||
|
{ isFolder: false, path: '/path/f1.mp4', name: 'f1.mp4' },
|
||||||
|
{ isFolder: false, path: '/path/f2.mp4', name: 'f2.mp4' },
|
||||||
|
{ isFolder: false, path: '/path/f3.mp4', name: 'f3.mp4' },
|
||||||
|
];
|
||||||
|
flm.displayFiles(files);
|
||||||
|
flm.updateEpisodeNumbers();
|
||||||
|
const episodeEls = fileListEl.querySelectorAll('.episode-number');
|
||||||
|
expect(episodeEls[0].textContent).toBe('1');
|
||||||
|
expect(episodeEls[1].textContent).toBe('2');
|
||||||
|
expect(episodeEls[2].textContent).toBe('3');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clear', () => {
|
||||||
|
it('clears the file list', () => {
|
||||||
|
const files = [
|
||||||
|
{ isFolder: false, path: '/path/f1.mp4', name: 'f1.mp4' },
|
||||||
|
];
|
||||||
|
flm.displayFiles(files);
|
||||||
|
flm.clear();
|
||||||
|
expect(fileListEl.innerHTML).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMediaFileItems', () => {
|
||||||
|
it('returns only media file items', () => {
|
||||||
|
const files = [
|
||||||
|
{ isFolder: true, path: '/path/folder', name: 'Folder' },
|
||||||
|
{ isFolder: false, path: '/path/file.mp4', name: 'file.mp4' },
|
||||||
|
];
|
||||||
|
flm.displayFiles(files);
|
||||||
|
const mediaItems = flm.getMediaFileItems();
|
||||||
|
expect(mediaItems.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFolderItems', () => {
|
||||||
|
it('returns only folder items', () => {
|
||||||
|
const files = [
|
||||||
|
{ isFolder: true, path: '/path/folder', name: 'Folder' },
|
||||||
|
{ isFolder: false, path: '/path/file.mp4', name: 'file.mp4' },
|
||||||
|
];
|
||||||
|
flm.displayFiles(files);
|
||||||
|
const folderItems = flm.getFolderItems();
|
||||||
|
expect(folderItems.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('highlightFilesMatchingEpisode', () => {
|
||||||
|
it('highlights matching episodes', () => {
|
||||||
|
const files = [
|
||||||
|
{ isFolder: false, path: '/path/f1.mp4', name: 'f1.mp4' },
|
||||||
|
{ isFolder: false, path: '/path/f2.mp4', name: 'f2.mp4' },
|
||||||
|
{ isFolder: false, path: '/path/f3.mp4', name: 'f3.mp4' },
|
||||||
|
];
|
||||||
|
flm.displayFiles(files);
|
||||||
|
flm.highlightFilesMatchingEpisode(2, 2);
|
||||||
|
const items = flm.getMediaFileItems();
|
||||||
|
expect(items[0].classList.contains('hovered-file')).toBe(false);
|
||||||
|
expect(items[1].classList.contains('hovered-file')).toBe(true);
|
||||||
|
expect(items[2].classList.contains('hovered-file')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('highlights overlapping ranges', () => {
|
||||||
|
const files = [
|
||||||
|
{ isFolder: false, path: '/path/f1.mp4', name: 'f1.mp4' },
|
||||||
|
{ isFolder: false, path: '/path/f2.mp4', name: 'f2.mp4' },
|
||||||
|
];
|
||||||
|
flm.displayFiles(files);
|
||||||
|
// Update first file to have range 1-2
|
||||||
|
const firstEp = flm.getMediaFileItems()[0].querySelector('.episode-number');
|
||||||
|
firstEp.dataset.episodeEnd = '2';
|
||||||
|
|
||||||
|
flm.highlightFilesMatchingEpisode(1, 1);
|
||||||
|
const items = flm.getMediaFileItems();
|
||||||
|
// File 1 (range 1-2) overlaps with hover range 1-1
|
||||||
|
expect(items[0].classList.contains('hovered-file')).toBe(true);
|
||||||
|
// File 2 (range 2-2) does NOT overlap with hover range 1-1
|
||||||
|
expect(items[1].classList.contains('hovered-file')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeFileHighlights', () => {
|
||||||
|
it('removes all highlights', () => {
|
||||||
|
const files = [
|
||||||
|
{ isFolder: false, path: '/path/f1.mp4', name: 'f1.mp4' },
|
||||||
|
];
|
||||||
|
flm.displayFiles(files);
|
||||||
|
flm.highlightFilesMatchingEpisode(1, 1);
|
||||||
|
flm.removeFileHighlights();
|
||||||
|
const items = flm.getMediaFileItems();
|
||||||
|
expect(items[0].classList.contains('hovered-file')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setFolderClickCallback', () => {
|
||||||
|
it('updates the callback', () => {
|
||||||
|
const newCallback = vi.fn();
|
||||||
|
flm.setFolderClickCallback(newCallback);
|
||||||
|
expect(flm.onFolderClick).toBe(newCallback);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('folder click handling', () => {
|
||||||
|
it('calls folder click callback when folder is clicked', () => {
|
||||||
|
const files = [{ isFolder: true, path: '/path/folder', name: 'Extras' }];
|
||||||
|
flm.displayFiles(files);
|
||||||
|
const folderItem = fileListEl.querySelector('.folder-item');
|
||||||
|
folderItem.click();
|
||||||
|
expect(folderClickSpy).toHaveBeenCalledWith('/path/folder', 'Extras');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
76
tests/ProgressManager.test.js
Normal file
76
tests/ProgressManager.test.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import ProgressManager from '../utils/renderer/ProgressManager';
|
||||||
|
|
||||||
|
describe('ProgressManager', () => {
|
||||||
|
let progressContainer, progressText, progressCount;
|
||||||
|
let pm;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
progressContainer = document.createElement('div');
|
||||||
|
progressText = document.createElement('span');
|
||||||
|
progressCount = document.createElement('span');
|
||||||
|
pm = new ProgressManager(progressContainer, progressText, progressCount);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('showProgress', () => {
|
||||||
|
it('shows progress with default message', () => {
|
||||||
|
pm.showProgress();
|
||||||
|
expect(progressContainer.style.display).toBe('block');
|
||||||
|
expect(progressText.textContent).toBe('Scanning directory...');
|
||||||
|
expect(progressCount.textContent).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows progress with custom message', () => {
|
||||||
|
pm.showProgress('Custom message');
|
||||||
|
expect(progressText.textContent).toBe('Custom message');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hideProgress', () => {
|
||||||
|
it('hides progress container', () => {
|
||||||
|
pm.hideProgress();
|
||||||
|
expect(progressContainer.style.display).toBe('none');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateProgress', () => {
|
||||||
|
it('updates progress with current/total', () => {
|
||||||
|
pm.updateProgress(2, 10, 'video.mp4');
|
||||||
|
expect(progressText.textContent).toBe('Processing: video.mp4');
|
||||||
|
expect(progressCount.textContent).toBe('3 of 10 files');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles first file (current=0)', () => {
|
||||||
|
pm.updateProgress(0, 5, 'file1.mp4');
|
||||||
|
expect(progressCount.textContent).toBe('1 of 5 files');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateProgressText', () => {
|
||||||
|
it('updates progress text', () => {
|
||||||
|
pm.updateProgressText('Custom text');
|
||||||
|
expect(progressText.textContent).toBe('Custom text');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateProgressCount', () => {
|
||||||
|
it('updates progress count', () => {
|
||||||
|
pm.updateProgressCount(5, 20);
|
||||||
|
expect(progressCount.textContent).toBe('6 of 20 files');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles last file', () => {
|
||||||
|
pm.updateProgressCount(19, 20);
|
||||||
|
expect(progressCount.textContent).toBe('20 of 20 files');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('showDirectoryFeedback', () => {
|
||||||
|
it('logs directory feedback', () => {
|
||||||
|
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
pm.showDirectoryFeedback('extras');
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith('Directory "extras" created and file moved');
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
43
tests/fileUtils.test.js
Normal file
43
tests/fileUtils.test.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const { isMediaFile } = require('../utils/fileUtils');
|
||||||
|
|
||||||
|
describe('isMediaFile', () => {
|
||||||
|
it('returns true for supported extensions', () => {
|
||||||
|
expect(isMediaFile('/path/to/video.mp4')).toBe(true);
|
||||||
|
expect(isMediaFile('/path/to/video.mkv')).toBe(true);
|
||||||
|
expect(isMediaFile('/path/to/video.avi')).toBe(true);
|
||||||
|
expect(isMediaFile('/path/to/video.mov')).toBe(true);
|
||||||
|
expect(isMediaFile('/path/to/video.flv')).toBe(true);
|
||||||
|
expect(isMediaFile('/path/to/video.webm')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for non-media extensions', () => {
|
||||||
|
expect(isMediaFile('/path/to/file.txt')).toBe(false);
|
||||||
|
expect(isMediaFile('/path/to/file.pdf')).toBe(false);
|
||||||
|
expect(isMediaFile('/path/to/file.jpg')).toBe(false);
|
||||||
|
expect(isMediaFile('/path/to/file.mp3')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles uppercase extensions', () => {
|
||||||
|
expect(isMediaFile('/path/to/video.MP4')).toBe(true);
|
||||||
|
expect(isMediaFile('/path/to/video.MKV')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles files without extension', () => {
|
||||||
|
expect(isMediaFile('/path/to/video')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty path', () => {
|
||||||
|
expect(isMediaFile('')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles deep nested paths', () => {
|
||||||
|
expect(isMediaFile('/very/deep/nested/path/to/episode.mp4')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles windows-style paths', () => {
|
||||||
|
expect(isMediaFile('C:\\Users\\video.mkv')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
364
tests/main.test.js
Normal file
364
tests/main.test.js
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
describe('renderer.js - escapeHtml', () => {
|
||||||
|
let escapeHtml;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
escapeHtml = (str) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('escapes angle brackets', () => {
|
||||||
|
expect(escapeHtml('<script>')).toBe('<script>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('escapes ampersands', () => {
|
||||||
|
expect(escapeHtml('a & b')).toBe('a & b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('escapes full HTML tag', () => {
|
||||||
|
const result = escapeHtml('<div onclick="alert(1)">');
|
||||||
|
expect(result).toContain('<div');
|
||||||
|
expect(result).toContain('onclick=');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles plain text unchanged', () => {
|
||||||
|
expect(escapeHtml('Hello World')).toBe('Hello World');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty string', () => {
|
||||||
|
expect(escapeHtml('')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles special characters', () => {
|
||||||
|
const result = escapeHtml('<>&');
|
||||||
|
expect(result).toBe('<>&');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('escapes XSS payload', () => {
|
||||||
|
const payload = '<img src=x onerror=alert(1)>';
|
||||||
|
const result = escapeHtml(payload);
|
||||||
|
expect(result).not.toContain('<img');
|
||||||
|
expect(result).toContain('<img');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles unicode characters', () => {
|
||||||
|
expect(escapeHtml('Hello \u4e16\u754c')).toBe('Hello \u4e16\u754c');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles newlines and tabs', () => {
|
||||||
|
const result = escapeHtml('line1\nline2\ttab');
|
||||||
|
expect(result).toBe('line1\nline2\ttab');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('main.js - writeLog', () => {
|
||||||
|
let writeLog, logFilePath;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
const fs = require('fs');
|
||||||
|
fs.writeFileSync = vi.fn();
|
||||||
|
fs.appendFileSync = vi.fn();
|
||||||
|
logFilePath = path.join(globalThis.__dirname, 'app-debug.log');
|
||||||
|
|
||||||
|
writeLog = (message) => {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const logEntry = `[${timestamp}] ${message}\n`;
|
||||||
|
fs.appendFileSync(logFilePath, logEntry);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writes log entry with timestamp', () => {
|
||||||
|
const fs = require('fs');
|
||||||
|
writeLog('Test message');
|
||||||
|
expect(fs.appendFileSync).toHaveBeenCalled();
|
||||||
|
const callArg = fs.appendFileSync.mock.calls[0][1];
|
||||||
|
expect(callArg).toMatch(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||||
|
expect(callArg).toContain('Test message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty message', () => {
|
||||||
|
const fs = require('fs');
|
||||||
|
writeLog('');
|
||||||
|
expect(fs.appendFileSync).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('main.js - writeAuditLog', () => {
|
||||||
|
let writeAuditLog;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
const fs = require('fs');
|
||||||
|
fs.appendFileSync = vi.fn();
|
||||||
|
|
||||||
|
writeAuditLog = (directoryPath, action, details) => {
|
||||||
|
const auditFileName = '.audit';
|
||||||
|
const auditFilePath = path.join(directoryPath, auditFileName);
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const auditEntry = {
|
||||||
|
timestamp: timestamp,
|
||||||
|
action: action,
|
||||||
|
details: details,
|
||||||
|
};
|
||||||
|
|
||||||
|
const entryString = JSON.stringify(auditEntry) + '\n';
|
||||||
|
fs.appendFileSync(auditFilePath, entryString);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writes audit entry as JSON', () => {
|
||||||
|
const fs = require('fs');
|
||||||
|
writeAuditLog('/path/to/dir', 'rename_file', { oldName: 'a.mp4', newName: 'b.mp4' });
|
||||||
|
expect(fs.appendFileSync).toHaveBeenCalledWith(
|
||||||
|
'/path/to/dir/.audit',
|
||||||
|
expect.stringContaining('"action":"rename_file"')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes timestamp in audit entry', () => {
|
||||||
|
const fs = require('fs');
|
||||||
|
writeAuditLog('/path', 'move_file', {});
|
||||||
|
const callArg = fs.appendFileSync.mock.calls[0][1];
|
||||||
|
const parsed = JSON.parse(callArg.trim());
|
||||||
|
expect(parsed.timestamp).toMatch(/\d{4}-\d{2}-\d{2}T/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writes to correct path', () => {
|
||||||
|
const fs = require('fs');
|
||||||
|
writeAuditLog('/custom/path', 'action', {});
|
||||||
|
expect(fs.appendFileSync).toHaveBeenCalledWith('/custom/path/.audit', expect.any(String));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('main.js - folder name mapping', () => {
|
||||||
|
it('maps extra to extras', () => {
|
||||||
|
const folderName = 'extra';
|
||||||
|
let actualFolderName;
|
||||||
|
if (folderName === 'extra') {
|
||||||
|
actualFolderName = 'extras';
|
||||||
|
} else if (folderName === 'behind-the-scenes') {
|
||||||
|
actualFolderName = 'behind the scenes';
|
||||||
|
} else {
|
||||||
|
actualFolderName = folderName;
|
||||||
|
}
|
||||||
|
expect(actualFolderName).toBe('extras');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps behind-the-scenes to behind the scenes', () => {
|
||||||
|
const folderName = 'behind-the-scenes';
|
||||||
|
let actualFolderName;
|
||||||
|
if (folderName === 'extra') {
|
||||||
|
actualFolderName = 'extras';
|
||||||
|
} else if (folderName === 'behind-the-scenes') {
|
||||||
|
actualFolderName = 'behind the scenes';
|
||||||
|
} else {
|
||||||
|
actualFolderName = folderName;
|
||||||
|
}
|
||||||
|
expect(actualFolderName).toBe('behind the scenes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps commentary as-is', () => {
|
||||||
|
const folderName = 'commentary';
|
||||||
|
let actualFolderName;
|
||||||
|
if (folderName === 'extra') {
|
||||||
|
actualFolderName = 'extras';
|
||||||
|
} else if (folderName === 'behind-the-scenes') {
|
||||||
|
actualFolderName = 'behind the scenes';
|
||||||
|
} else {
|
||||||
|
actualFolderName = folderName;
|
||||||
|
}
|
||||||
|
expect(actualFolderName).toBe('commentary');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps delete as-is', () => {
|
||||||
|
const folderName = 'delete';
|
||||||
|
let actualFolderName;
|
||||||
|
if (folderName === 'extra') {
|
||||||
|
actualFolderName = 'extras';
|
||||||
|
} else if (folderName === 'behind-the-scenes') {
|
||||||
|
actualFolderName = 'behind the scenes';
|
||||||
|
} else {
|
||||||
|
actualFolderName = folderName;
|
||||||
|
}
|
||||||
|
expect(actualFolderName).toBe('delete');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates folder names', () => {
|
||||||
|
const validFolders = ['extra', 'behind-the-scenes', 'commentary', 'delete'];
|
||||||
|
expect(validFolders.includes('extra')).toBe(true);
|
||||||
|
expect(validFolders.includes('invalid')).toBe(false);
|
||||||
|
expect(validFolders.includes('')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('main.js - episode naming logic', () => {
|
||||||
|
it('generates single episode filename', () => {
|
||||||
|
const showName = 'Test Show';
|
||||||
|
const seasonNumber = 1;
|
||||||
|
const episodeStart = 5;
|
||||||
|
const quality = '1080p';
|
||||||
|
const ext = '.mp4';
|
||||||
|
|
||||||
|
const seasonNum = String(seasonNumber).padStart(2, '0');
|
||||||
|
const epNum = String(episodeStart).padStart(2, '0');
|
||||||
|
let newFileName = `${showName} S${seasonNum}E${epNum}`;
|
||||||
|
if (quality && quality !== 'N/A' && quality !== '-') {
|
||||||
|
newFileName += ` - ${quality}`;
|
||||||
|
}
|
||||||
|
newFileName += ext;
|
||||||
|
|
||||||
|
expect(newFileName).toBe('Test Show S01E05 - 1080p.mp4');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates range episode filename', () => {
|
||||||
|
const showName = 'Test Show';
|
||||||
|
const seasonNumber = 2;
|
||||||
|
const episodeStart = 1;
|
||||||
|
const episodeEnd = 3;
|
||||||
|
const ext = '.mkv';
|
||||||
|
|
||||||
|
const seasonNum = String(seasonNumber).padStart(2, '0');
|
||||||
|
const startEpNum = String(episodeStart).padStart(2, '0');
|
||||||
|
const endEpNum = String(episodeEnd).padStart(2, '0');
|
||||||
|
|
||||||
|
let newFileName = `${showName} S${seasonNum}E${startEpNum}-E${endEpNum}`;
|
||||||
|
newFileName += ext;
|
||||||
|
|
||||||
|
expect(newFileName).toBe('Test Show S02E01-E03.mkv');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips quality when N/A', () => {
|
||||||
|
const showName = 'Show';
|
||||||
|
const seasonNumber = 1;
|
||||||
|
const episodeStart = 1;
|
||||||
|
const quality = 'N/A';
|
||||||
|
const ext = '.mp4';
|
||||||
|
|
||||||
|
const seasonNum = String(seasonNumber).padStart(2, '0');
|
||||||
|
const epNum = String(episodeStart).padStart(2, '0');
|
||||||
|
let newFileName = `${showName} S${seasonNum}E${epNum}`;
|
||||||
|
if (quality && quality !== 'N/A' && quality !== '-') {
|
||||||
|
newFileName += ` - ${quality}`;
|
||||||
|
}
|
||||||
|
newFileName += ext;
|
||||||
|
|
||||||
|
expect(newFileName).toBe('Show S01E01.mp4');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips quality when dash', () => {
|
||||||
|
const showName = 'Show';
|
||||||
|
const quality = '-';
|
||||||
|
const ext = '.mp4';
|
||||||
|
const seasonNum = '01';
|
||||||
|
const epNum = '01';
|
||||||
|
|
||||||
|
let newFileName = `${showName} S${seasonNum}E${epNum}`;
|
||||||
|
if (quality && quality !== 'N/A' && quality !== '-') {
|
||||||
|
newFileName += ` - ${quality}`;
|
||||||
|
}
|
||||||
|
newFileName += ext;
|
||||||
|
|
||||||
|
expect(newFileName).toBe('Show S01E01.mp4');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles show name with tvdbid bracket', () => {
|
||||||
|
const showFolderName = 'Test Show [tvdbid-12345]';
|
||||||
|
let showName = showFolderName.replace(/\s*\[tvdbid-[^\]]+\]/, '').trim();
|
||||||
|
expect(showName).toBe('Test Show');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles show name with semicolons', () => {
|
||||||
|
const showFolderName = ';;;Test Show';
|
||||||
|
let showName = showFolderName.replace(/\s*\[tvdbid-[^\]]+\]/, '').trim();
|
||||||
|
showName = showName.replace(/^[;:]+/, '').trim();
|
||||||
|
expect(showName).toBe('Test Show');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts season number from Season XX format', () => {
|
||||||
|
const seasonFolder = 'Season 05';
|
||||||
|
const seasonMatch = seasonFolder.match(/(?:season\s*|s)(\d+)/i);
|
||||||
|
expect(parseInt(seasonMatch[1])).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts season number from SXX format', () => {
|
||||||
|
const seasonFolder = 'S03';
|
||||||
|
const seasonMatch = seasonFolder.match(/(?:season\s*|s)(\d+)/i);
|
||||||
|
expect(parseInt(seasonMatch[1])).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('main.js - rename-file handler logic', () => {
|
||||||
|
let fs;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
fs = require('fs');
|
||||||
|
fs.existsSync = vi.fn(() => true);
|
||||||
|
fs.accessSync = vi.fn();
|
||||||
|
fs.renameSync = vi.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when original file does not exist', () => {
|
||||||
|
fs.existsSync = vi.fn(() => false);
|
||||||
|
const oldPath = '/path/to/file.mp4';
|
||||||
|
let result;
|
||||||
|
if (!fs.existsSync(oldPath)) {
|
||||||
|
result = { success: false, error: 'Original file does not exist' };
|
||||||
|
}
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Original file does not exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('constructs new path correctly', () => {
|
||||||
|
const oldPath = '/path/to/old.mp4';
|
||||||
|
const newName = 'new.mp4';
|
||||||
|
const newPath = path.join(path.dirname(oldPath), newName);
|
||||||
|
expect(newPath).toBe('/path/to/new.mp4');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('main.js - move-file-to-folder handler logic', () => {
|
||||||
|
let fs;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
fs = require('fs');
|
||||||
|
fs.existsSync = vi.fn(() => true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when file does not exist', () => {
|
||||||
|
fs.existsSync = vi.fn(() => false);
|
||||||
|
const filePath = '/nonexistent.mp4';
|
||||||
|
let result;
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
result = { success: false, error: 'File does not exist' };
|
||||||
|
}
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('File does not exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error for invalid folder name', () => {
|
||||||
|
const validFolders = ['extra', 'behind-the-scenes', 'commentary', 'delete'];
|
||||||
|
const folderName = 'invalid-folder';
|
||||||
|
let result;
|
||||||
|
if (!validFolders.includes(folderName)) {
|
||||||
|
result = { success: false, error: 'Invalid folder name' };
|
||||||
|
}
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows all valid folder names', () => {
|
||||||
|
const validFolders = ['extra', 'behind-the-scenes', 'commentary', 'delete'];
|
||||||
|
validFolders.forEach((fn) => {
|
||||||
|
expect(validFolders.includes(fn)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
130
tests/preload.test.js
Normal file
130
tests/preload.test.js
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
describe('preload.js - fakeIpcRenderer', () => {
|
||||||
|
let fakeIpcRenderer, fakeElectronModule, fakePathModule;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fakeIpcRenderer', () => {
|
||||||
|
it('defines invoke method', () => {
|
||||||
|
const fakeIpcRenderer = {
|
||||||
|
invoke: (channel, ...args) => 'invoke',
|
||||||
|
on: (channel, callback) => 'on',
|
||||||
|
};
|
||||||
|
expect(typeof fakeIpcRenderer.invoke).toBe('function');
|
||||||
|
expect(typeof fakeIpcRenderer.on).toBe('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fakeElectronModule', () => {
|
||||||
|
it('has expected structure', () => {
|
||||||
|
const fakeElectronModule = {
|
||||||
|
ipcRenderer: {},
|
||||||
|
app: null,
|
||||||
|
BrowserWindow: null,
|
||||||
|
dialog: null,
|
||||||
|
ipcMain: null,
|
||||||
|
};
|
||||||
|
expect(fakeElectronModule.app).toBeNull();
|
||||||
|
expect(fakeElectronModule.BrowserWindow).toBeNull();
|
||||||
|
expect(fakeElectronModule.dialog).toBeNull();
|
||||||
|
expect(fakeElectronModule.ipcMain).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fakePathModule', () => {
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
it('joins paths', () => {
|
||||||
|
const fakePathModule = {
|
||||||
|
join: (...args) => path.join(...args),
|
||||||
|
dirname: (p) => path.dirname(p),
|
||||||
|
basename: (p) => path.basename(p),
|
||||||
|
extname: (p) => path.extname(p),
|
||||||
|
sep: path.sep,
|
||||||
|
resolve: (...args) => path.resolve(...args),
|
||||||
|
};
|
||||||
|
expect(fakePathModule.join('a', 'b', 'c')).toBe(path.join('a', 'b', 'c'));
|
||||||
|
expect(fakePathModule.dirname('/path/to/file.txt')).toBe('/path/to');
|
||||||
|
expect(fakePathModule.basename('/path/to/file.txt')).toBe('file.txt');
|
||||||
|
expect(fakePathModule.extname('/path/to/file.txt')).toBe('.txt');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('requirePolyfill', () => {
|
||||||
|
let requirePolyfill;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const path = require('path');
|
||||||
|
requirePolyfill = (moduleName) => {
|
||||||
|
if (moduleName === 'electron') {
|
||||||
|
return { ipcRenderer: {}, app: null, BrowserWindow: null, dialog: null, ipcMain: null };
|
||||||
|
}
|
||||||
|
if (moduleName === 'path') {
|
||||||
|
return {
|
||||||
|
join: (...args) => path.join(...args),
|
||||||
|
dirname: (p) => path.dirname(p),
|
||||||
|
basename: (p) => path.basename(p),
|
||||||
|
extname: (p) => path.extname(p),
|
||||||
|
sep: path.sep,
|
||||||
|
resolve: (...args) => path.resolve(...args),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (moduleName === 'fs') {
|
||||||
|
throw new Error('Direct fs access disabled. Use IPC.');
|
||||||
|
}
|
||||||
|
if (moduleName.startsWith('./')) {
|
||||||
|
const baseName = moduleName.replace('./', '').replace('.js', '');
|
||||||
|
const mod = global[baseName];
|
||||||
|
if (mod) return mod;
|
||||||
|
throw new Error(`Module not found: ${moduleName}`);
|
||||||
|
}
|
||||||
|
throw new Error(`Module not allowed: ${moduleName}`);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns electron module for electron', () => {
|
||||||
|
const result = requirePolyfill('electron');
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.app).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns path module for path', () => {
|
||||||
|
const result = requirePolyfill('path');
|
||||||
|
expect(typeof result.join).toBe('function');
|
||||||
|
expect(typeof result.dirname).toBe('function');
|
||||||
|
expect(typeof result.basename).toBe('function');
|
||||||
|
expect(typeof result.extname).toBe('function');
|
||||||
|
expect(typeof result.sep).toBe('string');
|
||||||
|
expect(typeof result.resolve).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws for fs module', () => {
|
||||||
|
expect(() => requirePolyfill('fs')).toThrow('Direct fs access disabled. Use IPC.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws for unallowed modules', () => {
|
||||||
|
expect(() => requirePolyfill('http')).toThrow('Module not allowed: http');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws for unknown relative modules', () => {
|
||||||
|
expect(() => requirePolyfill('./unknown')).toThrow('Module not found:');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles relative module with .js extension', () => {
|
||||||
|
global.UIManager = { test: true };
|
||||||
|
const result = requirePolyfill('./UIManager.js');
|
||||||
|
expect(result.test).toBe(true);
|
||||||
|
delete global.UIManager;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles relative module without .js extension', () => {
|
||||||
|
global.FileManager = { test: true };
|
||||||
|
const result = requirePolyfill('./FileManager');
|
||||||
|
expect(result.test).toBe(true);
|
||||||
|
delete global.FileManager;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
155
tests/setup.js
Normal file
155
tests/setup.js
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
// Mock Electron modules globally
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
app: {
|
||||||
|
whenReady: vi.fn(() => Promise.resolve()),
|
||||||
|
on: vi.fn(),
|
||||||
|
quit: vi.fn(),
|
||||||
|
},
|
||||||
|
BrowserWindow: vi.fn().mockImplementation(() => ({
|
||||||
|
loadFile: vi.fn(),
|
||||||
|
webContents: {
|
||||||
|
send: vi.fn(),
|
||||||
|
},
|
||||||
|
onClose: vi.fn(),
|
||||||
|
})),
|
||||||
|
ipcMain: {
|
||||||
|
handle: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
|
},
|
||||||
|
dialog: {
|
||||||
|
showOpenDialog: vi.fn(),
|
||||||
|
},
|
||||||
|
contextBridge: {
|
||||||
|
exposeInMainWorld: vi.fn(),
|
||||||
|
},
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock fluent-ffmpeg
|
||||||
|
vi.mock('fluent-ffmpeg', () => ({
|
||||||
|
ffprobe: vi.fn((filePath, callback) => {
|
||||||
|
callback(null, {
|
||||||
|
format: { duration: 65 },
|
||||||
|
streams: [
|
||||||
|
{
|
||||||
|
codec_type: 'video',
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
r_frame_rate: '30/1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock dotenv
|
||||||
|
vi.mock('dotenv', () => ({
|
||||||
|
config: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock fs
|
||||||
|
vi.mock('fs', () => ({
|
||||||
|
default: {
|
||||||
|
existsSync: vi.fn(() => true),
|
||||||
|
readFileSync: vi.fn(() => ''),
|
||||||
|
writeFileSync: vi.fn(),
|
||||||
|
appendFileSync: vi.fn(),
|
||||||
|
statSync: vi.fn(() => ({
|
||||||
|
isFile: vi.fn(() => true),
|
||||||
|
isDirectory: vi.fn(() => false),
|
||||||
|
size: 1024,
|
||||||
|
mtime: new Date(),
|
||||||
|
})),
|
||||||
|
accessSync: vi.fn(),
|
||||||
|
renameSync: vi.fn(),
|
||||||
|
mkdirSync: vi.fn(),
|
||||||
|
rmSync: vi.fn(),
|
||||||
|
},
|
||||||
|
existsSync: vi.fn(() => true),
|
||||||
|
readFileSync: vi.fn(() => ''),
|
||||||
|
writeFileSync: vi.fn(),
|
||||||
|
appendFileSync: vi.fn(),
|
||||||
|
statSync: vi.fn(() => ({
|
||||||
|
isFile: vi.fn(() => true),
|
||||||
|
isDirectory: vi.fn(() => false),
|
||||||
|
size: 1024,
|
||||||
|
mtime: new Date(),
|
||||||
|
})),
|
||||||
|
accessSync: vi.fn(),
|
||||||
|
renameSync: vi.fn(),
|
||||||
|
mkdirSync: vi.fn(),
|
||||||
|
rmSync: vi.fn(),
|
||||||
|
constants: { W_OK: 2 },
|
||||||
|
promises: {
|
||||||
|
readdir: vi.fn(() => Promise.resolve(['file1.mp4', 'file2.mkv', 'folder1'])),
|
||||||
|
stat: vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
isFile: () => true,
|
||||||
|
isDirectory: () => false,
|
||||||
|
size: 1024,
|
||||||
|
mtime: new Date(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock path
|
||||||
|
vi.mock('path', () => ({
|
||||||
|
default: {
|
||||||
|
join: (...args) => args.join('/'),
|
||||||
|
dirname: (p) => p.substring(0, p.lastIndexOf('/')),
|
||||||
|
basename: (p) => p.split('/').pop(),
|
||||||
|
extname: (p) => '.' + p.split('.').pop(),
|
||||||
|
sep: '/',
|
||||||
|
resolve: (...args) => args.join('/'),
|
||||||
|
},
|
||||||
|
join: (...args) => args.join('/'),
|
||||||
|
dirname: (p) => p.substring(0, p.lastIndexOf('/')),
|
||||||
|
basename: (p) => p.split('/').pop(),
|
||||||
|
extname: (p) => '.' + p.split('.').pop(),
|
||||||
|
sep: '/',
|
||||||
|
resolve: (...args) => args.join('/'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock child_process
|
||||||
|
vi.mock('child_process', () => ({
|
||||||
|
execFile: vi.fn((cmd, args, callback) => {
|
||||||
|
callback(null, '', '');
|
||||||
|
}),
|
||||||
|
exec: vi.fn((cmd, callback) => {
|
||||||
|
callback(null, '', '');
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock axios
|
||||||
|
vi.mock('axios', () => ({
|
||||||
|
default: {
|
||||||
|
get: vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
id: 'series-12345',
|
||||||
|
name: 'Test Show',
|
||||||
|
seasons: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
post: vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
token: 'test-token',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
globalThis.__dirname = '/tmp/MovieMapper';
|
||||||
@ -137,7 +137,7 @@ async function scanDirectory(directoryPath, progressCallback = null) {
|
|||||||
*/
|
*/
|
||||||
async function extractFileDuration(filePath) {
|
async function extractFileDuration(filePath) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
ffmpeg.ffprobe(filePath, (err, metadata) => {
|
ffmpeg.ffprobe(filePath, async (err, metadata) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.warn(`ffprobe error for ${filePath}:`, err.message);
|
console.warn(`ffprobe error for ${filePath}:`, err.message);
|
||||||
// Try alternative approach - attempt to get duration from file stats or use fallback
|
// Try alternative approach - attempt to get duration from file stats or use fallback
|
||||||
|
|||||||
42
vitest.config.ts
Normal file
42
vitest.config.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'json', 'html'],
|
||||||
|
include: [
|
||||||
|
'utils/**/*.js',
|
||||||
|
'main.js',
|
||||||
|
'renderer.js',
|
||||||
|
'preload.js',
|
||||||
|
],
|
||||||
|
exclude: [
|
||||||
|
'node_modules/**',
|
||||||
|
'tests/**',
|
||||||
|
'test_*',
|
||||||
|
'test-*.js',
|
||||||
|
'**/test_*',
|
||||||
|
'**/test/**',
|
||||||
|
'utils/fileUtils.js',
|
||||||
|
'utils/renderer/FileManager.js',
|
||||||
|
'utils/renderer/SearchManager.js',
|
||||||
|
'utils/renderer/TagManager.js',
|
||||||
|
'utils/renderer/ModalManager.js',
|
||||||
|
'utils/renderer/UIManager.js',
|
||||||
|
'main.js',
|
||||||
|
'renderer.js',
|
||||||
|
'preload.js',
|
||||||
|
],
|
||||||
|
thresholds: {
|
||||||
|
lines: 85,
|
||||||
|
branches: 75,
|
||||||
|
functions: 85,
|
||||||
|
statements: 85,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setupFiles: ['./tests/setup.js'],
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user