Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b55479b
refactor: update to angular 21
CyAScott Mar 10, 2026
36cbf97
fix: tests
CyAScott Mar 14, 2026
abf66c1
refactor: test
CyAScott Mar 14, 2026
2ee8b67
feat: adding test coverage script
CyAScott Mar 14, 2026
1973050
feat: added navigation tab tests
CyAScott Mar 14, 2026
922a104
feat: added rating section tests
CyAScott Mar 14, 2026
0048e1f
feat: add unit tests for PeopleSectionComponent and TypeaheadInputCom…
CyAScott Mar 14, 2026
4ed6dbd
feat: add unit tests for ReadonlyInfoSectionComponent, ThumbnailSecti…
CyAScott Mar 14, 2026
7e8a47a
feat: add unit tests for MediaEditorComponent
CyAScott Mar 14, 2026
f10bdf5
feat: add unit tests for SearchComponent and related functionality
CyAScott Mar 14, 2026
d266e44
fix: ensure hideTimeout is properly cleared in PlayerComponent
CyAScott Mar 14, 2026
56c46b4
refactor: update template syntax to use @for directive for better per…
CyAScott Mar 14, 2026
358cf0d
test: add unit tests for MetaComponent to ensure proper functionality…
CyAScott Mar 14, 2026
be97985
test: add unit tests for ImportComponent to verify functionality and …
CyAScott Mar 14, 2026
dfe6bba
test: add unit tests for LoginComponent to verify functionality and e…
CyAScott Mar 14, 2026
46df413
test: add unit tests for AddUserModalComponent, ChangePasswordModalCo…
CyAScott Mar 14, 2026
a98a283
refactor: update coverage exclusion patterns in vitest configuration
CyAScott Mar 14, 2026
0f8d7f4
feat: adding tests to GH actions
CyAScott Mar 14, 2026
ff0fcec
fix: downgrade @angular/platform-browser-dynamic to match version con…
CyAScott Mar 14, 2026
c589999
refactor: removing ngif
CyAScott Mar 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/pull-requests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ jobs:
with:
dotnet-version: 10.x

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Install frontend dependencies
working-directory: src/MediaBrowser.Frontend
run: npm ci

- name: Test Angular SPA
working-directory: src/MediaBrowser.Frontend
run: npm run coverage

- name: Restore dependencies
run: dotnet restore src/MediaBrowser.Tests/MediaBrowser.Tests.csproj

Expand Down
6,869 changes: 3,235 additions & 3,634 deletions src/MediaBrowser.Frontend/package-lock.json

Large diffs are not rendered by default.

41 changes: 21 additions & 20 deletions src/MediaBrowser.Frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "mediabrowser.frontend",
"version": "1.0.0",
"description": "This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.2.",
"description": "This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.0.0.",
"main": "index.js",
"scripts": {
"clean": "rm -f -r ../MediaBrowser/wwwroot",
Expand All @@ -10,7 +10,9 @@
"serve": "ng serve",
"build-angular": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
"test": "vitest",
"coverage": "vitest run --coverage",
"e2e": "playwright test"
},
"keywords": [],
"author": "",
Expand All @@ -29,30 +31,29 @@
]
},
"devDependencies": {
"@angular/build": "^20.3.5",
"@angular/cli": "^20.3.5",
"@angular/compiler-cli": "^20.3.4",
"@types/jasmine": "^5.1.9",
"jasmine-core": "^5.12.0",
"karma": "^6.4.4",
"karma-chrome-launcher": "^3.2.0",
"karma-coverage": "^2.2.1",
"karma-jasmine": "^5.1.0",
"karma-jasmine-html-reporter": "^2.1.0",
"ts-loader": "^9.5.4",
"@analogjs/vite-plugin-angular": "^2.3.1",
"@angular/build": "^21.0.0",
"@angular/cli": "^21.0.0",
"@angular/compiler-cli": "^21.0.0",
"@playwright/test": "^1.42.0",
"@vitest/coverage-v8": "^4.0.0",
"jsdom": "^24.0.0",
"typescript": "^5.9.3",
"vitest": "^4.0.0",
"webpack": "^5.102.1",
"webpack-cli": "^6.0.1"
},
"dependencies": {
"@angular/common": "^20.3.4",
"@angular/compiler": "^20.3.4",
"@angular/core": "^20.3.4",
"@angular/forms": "^20.3.4",
"@angular/platform-browser": "^20.3.4",
"@angular/router": "^20.3.4",
"@angular/common": "^21.0.0",
"@angular/compiler": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/forms": "^21.0.0",
"@angular/platform-browser": "^21.0.0",
"@angular/platform-browser-dynamic": "^21.0.0",
"@angular/router": "^21.0.0",
Comment thread
CyAScott marked this conversation as resolved.
"@fortawesome/fontawesome-free": "^7.1.0",
"rxjs": "^7.8.2",
"tslib": "^2.8.1"
"tslib": "^2.8.1",
"zone.js": "^0.16.1"
}
}
27 changes: 27 additions & 0 deletions src/MediaBrowser.Frontend/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
6 changes: 4 additions & 2 deletions src/MediaBrowser.Frontend/src/app/app.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { beforeEach, describe, expect, it } from 'vitest';
import { App } from './app';

describe('App', () => {
Expand All @@ -16,10 +17,11 @@ describe('App', () => {
expect(app).toBeTruthy();
});

it('should render title', () => {
it('should render the application layout', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, render');
expect(compiled.querySelector('app-navigation-tabs')).toBeTruthy();
expect(compiled.querySelector('main.main-content')).toBeTruthy();
});
});
2 changes: 1 addition & 1 deletion src/MediaBrowser.Frontend/src/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { UsersService } from './services/users.service';
selector: 'app-root',
imports: [RouterOutlet, NavigationTabsComponent],
templateUrl: './app.html',
styleUrl: './app.css'
styleUrls: ['./app.css']
})
export class App {
protected readonly title = signal('render');
Expand Down
60 changes: 33 additions & 27 deletions src/MediaBrowser.Frontend/src/app/import/import.html
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
<div class="import-container">
<div class="files-section" *ngIf="files.length > 0">
<table class="files-table">
<thead>
<tr>
<th>File Name</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let file of files">
<td class="filename">{{file.title}}</td>
<td class="action">
<a
class="import-btn"
title="Import this file"
[routerLink]="['/import', file.title]"
[state]="{ mediaData: file }">
<i class="fa-solid fa-upload"></i>
</a>
</td>
</tr>
</tbody>
</table>
</div>
@if (files.length > 0) {
<div class="files-section">
<table class="files-table">
<thead>
<tr>
<th>File Name</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@for (file of files; track $index) {
<tr>
<td class="filename">{{file.title}}</td>
<td class="action">
<a
class="import-btn"
title="Import this file"
[routerLink]="['/import', file.title]"
[state]="{ mediaData: file }">
<i class="fa-solid fa-upload"></i>
</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}

<!-- Empty state -->
<div class="empty-state" *ngIf="files.length === 0">
<p>No media files found in the selected directory.</p>
</div>
@if (files.length === 0) {
<div class="empty-state">
<p>No media files found in the selected directory.</p>
</div>
}
</div>
119 changes: 119 additions & 0 deletions src/MediaBrowser.Frontend/src/app/import/import.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { of, throwError } from 'rxjs';
import { ImportComponent } from './import';
import { ImportFileInfo, ImportService } from '../services/import.service';

function createImportFileInfo(overrides?: Partial<ImportFileInfo>): ImportFileInfo {
return {
createdOn: new Date('2025-01-01T00:00:00.000Z'),
ctimeMs: 1735689600000,
mime: 'video/mp4',
mtimeMs: 1735776000000,
name: 'movie.mp4',
size: 1234,
updatedOn: new Date('2025-01-02T00:00:00.000Z'),
url: 'https://example.com/movie.mp4',
...overrides
};
}

describe('ImportComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ImportComponent],
providers: [
provideZonelessChangeDetection(),
provideRouter([]),
{
provide: ImportService,
useValue: {
files: vi.fn()
}
}
]
}).compileComponents();
});

it('creates the component', () => {
const fixture = TestBed.createComponent(ImportComponent);
expect(fixture.componentInstance).toBeTruthy();
});

it('converts ImportFileInfo to MediaReadModel', () => {
const file = createImportFileInfo({
name: 'sample.mkv',
ctimeMs: 100,
mtimeMs: 200,
mime: 'video/x-matroska'
});

const model = ImportComponent.convertToMediaReadModel(file);

expect(model.title).toBe('sample.mkv');
expect(model.originalTitle).toBe('sample.mkv');
expect(model.mime).toBe('video/x-matroska');
expect(model.ctimeMs).toBe('100');
expect(model.mtimeMs).toBe('200');
expect(model.url).toBe(file.url);
expect(model.cast).toEqual([]);
expect(model.directors).toEqual([]);
expect(model.genres).toEqual([]);
expect(model.producers).toEqual([]);
expect(model.writers).toEqual([]);
expect(model.ffprobe).toEqual({});
});

it('loads files on init and maps them to MediaReadModel', async () => {
const importService = TestBed.inject(ImportService) as { files: ReturnType<typeof vi.fn> };
const file = createImportFileInfo({ name: 'loaded.mp4' });
importService.files.mockReturnValue(of([file]));

const fixture = TestBed.createComponent(ImportComponent);
const component = fixture.componentInstance;
const detectChangesSpy = vi.spyOn((component as unknown as { cdr: { detectChanges: () => void } }).cdr, 'detectChanges');

fixture.detectChanges();
await fixture.whenStable();

expect(importService.files).toHaveBeenCalledTimes(1);
expect(component.files).toHaveLength(1);
expect(component.files[0].title).toBe('loaded.mp4');
expect(component.files[0].ctimeMs).toBe(file.ctimeMs.toString());
expect(detectChangesSpy).toHaveBeenCalled();

const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelectorAll('tbody tr')).toHaveLength(1);
expect(compiled.querySelector('.filename')?.textContent).toContain('loaded.mp4');
});

it('handles scan errors by clearing files and logging the error', async () => {
const importService = TestBed.inject(ImportService) as { files: ReturnType<typeof vi.fn> };
const error = new Error('scan failed');
importService.files.mockReturnValue(throwError(() => error));

const fixture = TestBed.createComponent(ImportComponent);
const component = fixture.componentInstance;
component.files = [ImportComponent.convertToMediaReadModel(createImportFileInfo())];

const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const detectChangesSpy = vi.spyOn((component as unknown as { cdr: { detectChanges: () => void } }).cdr, 'detectChanges');

fixture.detectChanges();
await fixture.whenStable();

expect(importService.files).toHaveBeenCalledTimes(1);
expect(component.files).toEqual([]);
expect(consoleErrorSpy).toHaveBeenCalledWith('Error scanning directory:', error);
expect(detectChangesSpy).toHaveBeenCalled();

const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.empty-state p')?.textContent).toContain(
'No media files found in the selected directory.'
);

consoleErrorSpy.mockRestore();
});
});
12 changes: 8 additions & 4 deletions src/MediaBrowser.Frontend/src/app/login/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,20 @@ <h1>Media Browser</h1>
/>
</div>

<div class="error-message" *ngIf="errorMessage">
{{ errorMessage }}
</div>
@if (errorMessage) {
<div class="error-message">
{{ errorMessage }}
</div>
}

<button
type="submit"
class="login-button"
[disabled]="isLoading || !username.trim() || !password.trim()"
>
<span *ngIf="isLoading" class="loading-spinner"></span>
@if (isLoading) {
<span class="loading-spinner"></span>
}
{{ isLoading ? 'Signing in...' : 'Sign In' }}
</button>
</form>
Expand Down
Loading
Loading