4 Commits

Author SHA1 Message Date
ecb25ee322 vitest implementation 2026-03-16 18:38:46 +01:00
2173ad6abf sidemenu shows 2026-03-16 18:16:19 +01:00
3bd359ee9e sidemenu song list 2026-03-16 17:24:10 +01:00
Benjamin Ifland
f9516bbc4d fix angular json 2026-03-16 16:02:57 +01:00
105 changed files with 4057 additions and 1306 deletions

View File

@@ -15,8 +15,9 @@
"prefix": "app", "prefix": "app",
"architect": { "architect": {
"build": { "build": {
"builder": "@angular-devkit/build-angular:application", "builder": "@angular/build:application",
"options": { "options": {
"browser": "src/main.ts",
"outputPath": { "outputPath": {
"base": "dist/wgenerator" "base": "dist/wgenerator"
}, },
@@ -25,7 +26,7 @@
"src/polyfills.ts" "src/polyfills.ts"
], ],
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"aot": false, "inlineStyleLanguage": "less",
"assets": [ "assets": [
"src/browserconfig.xml", "src/browserconfig.xml",
"src/android-chrome-192x192.png", "src/android-chrome-192x192.png",
@@ -49,17 +50,9 @@
"lodash", "lodash",
"docx", "docx",
"qrcode" "qrcode"
], ]
"browser": "src/main.ts"
}, },
"configurations": { "configurations": {
"development": {
"aot": false,
"extractLicenses": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
},
"production": { "production": {
"fileReplacements": [ "fileReplacements": [
{ {
@@ -67,114 +60,55 @@
"with": "src/environments/environment.prod.ts" "with": "src/environments/environment.prod.ts"
} }
], ],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
"maximumWarning": "2mb", "maximumWarning": "500kB",
"maximumError": "5mb" "maximumError": "10MB"
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",
"maximumWarning": "6kb", "maximumWarning": "4kB",
"maximumError": "10kb" "maximumError": "8kB"
} }
], ],
"serviceWorker": "ngsw-config.json" "outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
} }
}, },
"defaultConfiguration": "production" "defaultConfiguration": "production"
}, },
"serve": { "serve": {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular/build:dev-server",
"options": {
"buildTarget": "wgenerator:build:development"
},
"configurations": { "configurations": {
"development": {
"buildTarget": "wgenerator:build:development"
},
"production": { "production": {
"buildTarget": "wgenerator:build:production" "buildTarget": "wgenerator:build:production"
},
"development": {
"buildTarget": "wgenerator:build:development"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"
}, },
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "wgenerator:build"
}
},
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular/build:unit-test",
"options": { "options": {
"main": "src/test.ts", "runner": "vitest",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js", "setupFiles": [
"assets": [ "src/test-vitest.ts"
"src/favicon.ico",
"src/assets",
"src/manifest.webmanifest"
], ],
"styles": [], "runnerConfig": true
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
} }
} }
} }
} }
}, },
"cli": { "cli": {
"analytics": "4047dcd7-89f4-402f-958e-e365a5505c55", "analytics": false
"schematicCollections": [
"@angular-eslint/schematics"
]
},
"schematics": {
"@angular-eslint/schematics:application": {
"setParserOptionsProject": true
},
"@angular-eslint/schematics:library": {
"setParserOptionsProject": true
},
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
} }
} }

View File

@@ -1,32 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/wgenerator'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

3527
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -47,22 +47,15 @@
"@angular/cli": "^21.2.1", "@angular/cli": "^21.2.1",
"@angular/compiler-cli": "^21.2.2", "@angular/compiler-cli": "^21.2.2",
"@angular/language-service": "^21.2.2", "@angular/language-service": "^21.2.2",
"@types/jasmine": "~6.0.0",
"@types/jasminewd2": "~2.0.13",
"@typescript-eslint/eslint-plugin": "^8.57.0", "@typescript-eslint/eslint-plugin": "^8.57.0",
"@typescript-eslint/parser": "^8.57.0", "@typescript-eslint/parser": "^8.57.0",
"eslint": "^9.39.4", "eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5", "eslint-plugin-prettier": "^5.5.5",
"firebase-tools": "^15.9.1", "firebase-tools": "^15.9.1",
"jasmine-core": "~6.1.0", "jsdom": "^29.0.0",
"jasmine-spec-reporter": "~7.0.0",
"karma": "~6.4.4",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage-istanbul-reporter": "~3.0.3",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.2.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"typescript": "~5.9.3" "typescript": "~5.9.3",
"vitest": "^4.1.0"
} }
} }

View File

@@ -1,5 +1,5 @@
<app-navigation></app-navigation> <app-navigation></app-navigation>
<div [@fader]="o.isActivated ? o.activatedRoute : ''" class="content"> <div class="content">
<router-outlet #o="outlet"></router-outlet> <router-outlet></router-outlet>
</div> </div>

View File

@@ -1,13 +1,13 @@
import {TestBed, waitForAsync} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {RouterTestingModule} from '@angular/router/testing'; import {RouterTestingModule} from '@angular/router/testing';
import {AppComponent} from './app.component'; import {AppComponent} from './app.component';
describe('AppComponent', () => { describe('AppComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [RouterTestingModule, AppComponent], imports: [RouterTestingModule, AppComponent],
}).compileComponents(); }).compileComponents();
})); });
it('should create the app', () => { it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {BrandComponent} from './brand.component'; import {BrandComponent} from './brand.component';
@@ -6,11 +6,11 @@ describe('BrandComponent', () => {
let component: BrandComponent; let component: BrandComponent;
let fixture: ComponentFixture<BrandComponent>; let fixture: ComponentFixture<BrandComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [BrandComponent], imports: [BrandComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(BrandComponent); fixture = TestBed.createComponent(BrandComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {NewUserComponent} from './new-user.component'; import {NewUserComponent} from './new-user.component';
@@ -6,11 +6,11 @@ describe('NewUserComponent', () => {
let component: NewUserComponent; let component: NewUserComponent;
let fixture: ComponentFixture<NewUserComponent>; let fixture: ComponentFixture<NewUserComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [NewUserComponent], imports: [NewUserComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(NewUserComponent); fixture = TestBed.createComponent(NewUserComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {GuestComponent} from './guest.component'; import {GuestComponent} from './guest.component';
@@ -6,11 +6,11 @@ describe('GuestComponent', () => {
let component: GuestComponent; let component: GuestComponent;
let fixture: ComponentFixture<GuestComponent>; let fixture: ComponentFixture<GuestComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [GuestComponent], imports: [GuestComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(GuestComponent); fixture = TestBed.createComponent(GuestComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {LegalComponent} from './legal.component'; import {LegalComponent} from './legal.component';
@@ -6,11 +6,11 @@ describe('LegalComponent', () => {
let component: LegalComponent; let component: LegalComponent;
let fixture: ComponentFixture<LegalComponent>; let fixture: ComponentFixture<LegalComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [LegalComponent], imports: [LegalComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(LegalComponent); fixture = TestBed.createComponent(LegalComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {LogoComponent} from './logo.component'; import {LogoComponent} from './logo.component';
@@ -6,11 +6,11 @@ describe('LogoComponent', () => {
let component: LogoComponent; let component: LogoComponent;
let fixture: ComponentFixture<LogoComponent>; let fixture: ComponentFixture<LogoComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [LogoComponent], imports: [LogoComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(LogoComponent); fixture = TestBed.createComponent(LogoComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MonitorComponent} from './monitor.component'; import {MonitorComponent} from './monitor.component';
@@ -6,11 +6,11 @@ describe('MonitorComponent', () => {
let component: MonitorComponent; let component: MonitorComponent;
let fixture: ComponentFixture<MonitorComponent>; let fixture: ComponentFixture<MonitorComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [MonitorComponent], imports: [MonitorComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(MonitorComponent); fixture = TestBed.createComponent(MonitorComponent);

View File

@@ -1,6 +1,5 @@
@if (show) { @if (show) {
<div @fade> <app-card [closeIcon]="faIcon" [heading]="show.showType | showType" [subheading]="show.date.toDate() | date:'dd.MM.yyyy'" closeLink="/presentation/select">
<app-card [closeIcon]="faIcon" [heading]="show.showType | showType" [subheading]="show.date.toDate() | date:'dd.MM.yyyy'" closeLink="/presentation/select">
@if (!progress) { @if (!progress) {
<div class="song"> <div class="song">
@if (show) { @if (show) {
@@ -78,6 +77,5 @@
} @if (show) { } @if (show) {
<app-add-song [addedLive]="true" [showSongs]="showSongs" [show]="show" [songs]="songs$|async"></app-add-song> <app-add-song [addedLive]="true" [showSongs]="showSongs" [show]="show" [songs]="songs$|async"></app-add-song>
} } } }
</app-card> </app-card>
</div>
} }

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {RemoteComponent} from './remote.component'; import {RemoteComponent} from './remote.component';
@@ -6,11 +6,11 @@ describe('RemoteComponent', () => {
let component: RemoteComponent; let component: RemoteComponent;
let fixture: ComponentFixture<RemoteComponent>; let fixture: ComponentFixture<RemoteComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [RemoteComponent], imports: [RemoteComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(RemoteComponent); fixture = TestBed.createComponent(RemoteComponent);

View File

@@ -1,6 +1,6 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {of} from 'rxjs'; import {firstValueFrom, of} from 'rxjs';
import {GlobalSettingsService} from '../../../services/global-settings.service'; import {GlobalSettingsService} from '../../../services/global-settings.service';
import {ShowService} from '../../shows/services/show.service'; import {ShowService} from '../../shows/services/show.service';
import {SelectComponent} from './select.component'; import {SelectComponent} from './select.component';
@@ -52,12 +52,11 @@ describe('SelectComponent', () => {
expect(component.visible).toBeTrue(); expect(component.visible).toBeTrue();
}); });
it('should expose recent shows sorted descending by date', done => { it('should expose recent shows sorted descending by date', async () => {
component.shows$.subscribe(shows => { const shows = await firstValueFrom(component.shows$);
expect(showServiceSpy.list$).toHaveBeenCalledWith(true); expect(showServiceSpy.list$).toHaveBeenCalledWith(true);
expect(shows.map(show => show.id)).toEqual(['recent-a', 'recent-b']); expect(shows.map(show => show.id)).toEqual(['recent-a', 'recent-b']);
done();
});
}); });
it('should persist the selected show, trigger presentation reset and navigate', async () => { it('should persist the selected show, trigger presentation reset and navigate', async () => {

View File

@@ -5,8 +5,8 @@ import {PresentationService} from './presentation.service';
describe('PresentationService', () => { describe('PresentationService', () => {
let service: PresentationService; let service: PresentationService;
beforeEach(() => { beforeEach(async () => {
void TestBed.configureTestingModule({}); await TestBed.configureTestingModule({});
service = TestBed.inject(PresentationService); service = TestBed.inject(PresentationService);
}); });

View File

@@ -14,7 +14,7 @@
.list-head { .list-head {
padding: 3px 10px; padding: 3px 10px;
color: var(--text-muted); color: var(--text-soft);
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 600; font-weight: 600;
} }

View File

@@ -2,4 +2,5 @@ export interface FilterValues {
time: number; time: number;
owner: string; owner: string;
showType: string; showType: string;
archived: boolean;
} }

View File

@@ -4,7 +4,7 @@
<mat-label>Zeitraum</mat-label> <mat-label>Zeitraum</mat-label>
<mat-select formControlName="time"> <mat-select formControlName="time">
@for (time of times; track time) { @for (time of times; track time) {
<mat-option [value]="time.key">{{ time.value }} </mat-option> <mat-option [value]="time.key">{{ time.value }}</mat-option>
} }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
@@ -14,7 +14,7 @@
<mat-select formControlName="owner"> <mat-select formControlName="owner">
<mat-option value="">Alle</mat-option> <mat-option value="">Alle</mat-option>
@for (owner of owners; track owner) { @for (owner of owners; track owner) {
<mat-option [value]="owner.key">{{ owner.value }} </mat-option> <mat-option [value]="owner.key">{{ owner.value }}</mat-option>
} }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
@@ -25,17 +25,18 @@
<mat-option value="">Alle</mat-option> <mat-option value="">Alle</mat-option>
<mat-optgroup label="öffentlich"> <mat-optgroup label="öffentlich">
@for (key of showTypePublic; track key) { @for (key of showTypePublic; track key) {
<mat-option [value]="key">{{ key | showType }} </mat-option> <mat-option [value]="key">{{ key | showType }}</mat-option>
} }
</mat-optgroup> </mat-optgroup>
<mat-optgroup label="privat"> <mat-optgroup label="privat">
@for (key of showTypePrivate; track key) { @for (key of showTypePrivate; track key) {
<mat-option [value]="key">{{ key | showType }} </mat-option> <mat-option [value]="key">{{ key | showType }}</mat-option>
} }
</mat-optgroup> </mat-optgroup>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-checkbox formControlName="archived">Archiviert</mat-checkbox>
</div> </div>
<i>Anzahl der Suchergebnisse: {{ shows?.length ?? 0 }}</i> <!-- <i>Anzahl der Suchergebnisse: {{ shows?.length ?? 0 }}</i>-->
</div> </div>

View File

@@ -1,5 +1,14 @@
.third { .third,
display: grid; div[formGroup] {
grid-template-columns: 1fr 1fr 1fr; display: flex;
column-gap: 20px; flex-direction: column;
gap: 12px;
}
.third {
gap: 0;
}
:host ::ng-deep .mat-mdc-form-field {
width: 100%;
} }

View File

@@ -1,4 +1,4 @@
import {Component, DestroyRef, Input, inject} from '@angular/core'; import {Component, DestroyRef, inject, Input} from '@angular/core';
import {KeyValue} from '@angular/common'; import {KeyValue} from '@angular/common';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
@@ -14,28 +14,23 @@ import {MatFormField, MatLabel} from '@angular/material/form-field';
import {MatSelect} from '@angular/material/select'; import {MatSelect} from '@angular/material/select';
import {MatOptgroup, MatOption} from '@angular/material/core'; import {MatOptgroup, MatOption} from '@angular/material/core';
import {ShowTypePipe} from '../../../../widget-modules/pipes/show-type-translater/show-type.pipe'; import {ShowTypePipe} from '../../../../widget-modules/pipes/show-type-translater/show-type.pipe';
import {MatCheckbox} from '@angular/material/checkbox';
@Component({ @Component({
selector: 'app-filter', selector: 'app-filter',
templateUrl: './filter.component.html', templateUrl: './filter.component.html',
styleUrls: ['./filter.component.less'], styleUrls: ['./filter.component.less'],
imports: [ReactiveFormsModule, MatFormField, MatLabel, MatSelect, MatOption, MatOptgroup, ShowTypePipe], imports: [ReactiveFormsModule, MatFormField, MatLabel, MatSelect, MatOption, MatOptgroup, ShowTypePipe, MatCheckbox],
}) })
export class FilterComponent { export class FilterComponent {
private showService = inject(ShowService);
private userService = inject(UserService);
private filterStore = inject(FilterStoreService);
private destroyRef = inject(DestroyRef);
@Input() public shows: Show[] = []; @Input() public shows: Show[] = [];
public showTypePublic = ShowService.SHOW_TYPE_PUBLIC; public showTypePublic = ShowService.SHOW_TYPE_PUBLIC;
public showTypePrivate = ShowService.SHOW_TYPE_PRIVATE; public showTypePrivate = ShowService.SHOW_TYPE_PRIVATE;
public filterFormGroup: FormGroup<{ public filterFormGroup: FormGroup<{
time: FormControl<number>; time: FormControl<number>;
owner: FormControl<string | null>; owner: FormControl<string | null>;
showType: FormControl<string | null>; showType: FormControl<string | null>;
archived: FormControl<boolean>;
}>; }>;
public times: KeyValue<number, string>[] = [ public times: KeyValue<number, string>[] = [
{key: 1, value: 'letzter Monat'}, {key: 1, value: 'letzter Monat'},
@@ -43,8 +38,11 @@ export class FilterComponent {
{key: 12, value: 'letztes Jahr'}, {key: 12, value: 'letztes Jahr'},
{key: 99999, value: 'alle'}, {key: 99999, value: 'alle'},
]; ];
public owners: {key: string; value: string}[] = []; public owners: {key: string; value: string}[] = [];
private showService = inject(ShowService);
private userService = inject(UserService);
private filterStore = inject(FilterStoreService);
private destroyRef = inject(DestroyRef);
public constructor() { public constructor() {
const fb = inject(FormBuilder); const fb = inject(FormBuilder);
@@ -53,6 +51,7 @@ export class FilterComponent {
time: fb.nonNullable.control(1), time: fb.nonNullable.control(1),
owner: fb.control<string | null>(null), owner: fb.control<string | null>(null),
showType: fb.control<string | null>(null), showType: fb.control<string | null>(null),
archived: fb.nonNullable.control(false),
}); });
this.filterStore.showFilter$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(filterValues => { this.filterStore.showFilter$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(filterValues => {
@@ -61,6 +60,7 @@ export class FilterComponent {
time: filterValues.time, time: filterValues.time,
owner: filterValues.owner || null, owner: filterValues.owner || null,
showType: filterValues.showType || null, showType: filterValues.showType || null,
archived: !!filterValues.archived,
}, },
{emitEvent: false} {emitEvent: false}
); );
@@ -69,6 +69,7 @@ export class FilterComponent {
this.filterFormGroup.controls.time.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('time', value)); this.filterFormGroup.controls.time.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('time', value));
this.filterFormGroup.controls.owner.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('owner', value ?? '')); this.filterFormGroup.controls.owner.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('owner', value ?? ''));
this.filterFormGroup.controls.showType.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('showType', value ?? '')); this.filterFormGroup.controls.showType.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('showType', value ?? ''));
this.filterFormGroup.controls.archived.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('archived', value));
this.owners$() this.owners$()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))

View File

@@ -1,31 +1,42 @@
<div> @if (showSidebar$ | async) {
<!-- <app-list-header *appRole="['leader']"></app-list-header>--> <app-sidebar>
<app-list-header *appRole="['leader']"> <div class="sidebar-content" sidebar>
@if (shows$ | async; as shows) { <app-filter [shows]="(publicShows$ | async) ?? []"></app-filter>
<app-filter [shows]="publicShows$ | async"></app-filter> </div>
} <div content>
</app-list-header> @if (privateShows$ | async; as privateShows) {
<ng-container *appRole="['leader']">
@if (privateShows$ | async; as shows) { @if (shows.length > 0) {
<app-card [padding]="false" heading="Meine Veranstaltungen"> <app-card [padding]="false" heading="Meine Veranstaltungen">
@for (show of shows | sortBy: 'desc':'date'; track trackBy($index, show)) { @for (show of privateShows; track trackBy($index, show)) {
<app-list-item <app-list-item
[routerLink]="show.id" [routerLink]="show.id"
[showStatusBadge]="show.published ? 'nicht gemeldet' : 'unveröffentlicht'" [showStatusBadgeType]="show.archived ? 'warn' : show.published ? 'error' : 'none'"
[showStatusBadgeType]="show.published ? 'error' : 'none'" [showStatusBadge]="show.archived ? 'archiviert' : show.published ? 'nicht gemeldet' : 'unveröffentlicht'"
[show]="show" [show]="show"
></app-list-item> ></app-list-item>
} }
<div *appRole="['leader']" class="list-action">
<app-button [fullWidth]="true" [icon]="faNewShow" routerLink="new">Neue Veranstaltung anlegen </app-button>
</div>
</app-card> </app-card>
} } }
</ng-container>
@if (publicShows$ | async; as shows) { @if (shows.length > 0) { @if (publicShows$ | async; as shows) { @if (shows.length > 0) {
<app-card [padding]="false" heading="Veröffentlichte Veranstaltungen"> <app-card [padding]="false" heading="Veröffentlichte Veranstaltungen">
@for (show of shows | sortBy: 'desc':'date'; track trackBy($index, show)) { @for (show of shows; track trackBy($index, show)) {
<app-list-item [routerLink]="show.id" [show]="show"></app-list-item>
}
</app-card>
} }
</div>
</app-sidebar>
} @else {
<div>
@if (publicShows$ | async; as shows) { @if (shows.length > 0) {
<app-card [padding]="false" heading="Veröffentlichte Veranstaltungen">
@for (show of shows; track trackBy($index, show)) {
<app-list-item [routerLink]="show.id" [show]="show"></app-list-item> <app-list-item [routerLink]="show.id" [show]="show"></app-list-item>
} }
</app-card> </app-card>
} } } }
</div> </div>
}

View File

@@ -0,0 +1,3 @@
.sidebar-content {
padding: 20px;
}

View File

@@ -1,5 +1,6 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {BehaviorSubject, of} from 'rxjs'; import {BehaviorSubject, firstValueFrom, of} from 'rxjs';
import {skip, take} from 'rxjs/operators';
import {ListComponent} from './list.component'; import {ListComponent} from './list.component';
import {ShowService} from '../services/show.service'; import {ShowService} from '../services/show.service';
import {UserService} from '../../../services/user/user.service'; import {UserService} from '../../../services/user/user.service';
@@ -50,7 +51,7 @@ describe('ListComponent', () => {
void expect(component).toBeTruthy(); void expect(component).toBeTruthy();
}); });
it('should list own drafts and pending published shows in my shows', done => { it('should list own drafts and pending published shows in my shows', async () => {
shows$.next([ shows$.next([
createShow({id: 'draft-own', owner: 'user-1', published: false, reportedType: null}), createShow({id: 'draft-own', owner: 'user-1', published: false, reportedType: null}),
createShow({id: 'pending-own', owner: 'user-1', published: true, reportedType: 'pending', date: {toDate: () => new Date('2026-03-02')}}), createShow({id: 'pending-own', owner: 'user-1', published: true, reportedType: 'pending', date: {toDate: () => new Date('2026-03-02')}}),
@@ -58,13 +59,11 @@ describe('ListComponent', () => {
createShow({id: 'draft-other', owner: 'user-2', published: false, reportedType: null, date: {toDate: () => new Date('2026-03-04')}}), createShow({id: 'draft-other', owner: 'user-2', published: false, reportedType: null, date: {toDate: () => new Date('2026-03-04')}}),
] as never); ] as never);
component.privateShows$.subscribe(shows => { const shows = await firstValueFrom(component.privateShows$);
expect(shows.map(show => show.id)).toEqual(['draft-own', 'pending-own']); expect(shows.map(show => show.id)).toEqual(['pending-own', 'draft-own']);
done();
});
}); });
it('should ignore show filters for my shows', done => { it('should ignore show filters for my shows', async () => {
const filterStore = TestBed.inject(FilterStoreService); const filterStore = TestBed.inject(FilterStoreService);
filterStore.updateShowFilter({time: 0, showType: 'service-worship'}); filterStore.updateShowFilter({time: 0, showType: 'service-worship'});
shows$.next([ shows$.next([
@@ -72,9 +71,36 @@ describe('ListComponent', () => {
createShow({id: 'pending-own', owner: 'user-1', published: true, reportedType: 'pending', showType: 'home-group', date: {toDate: () => new Date('2026-03-05')}}), createShow({id: 'pending-own', owner: 'user-1', published: true, reportedType: 'pending', showType: 'home-group', date: {toDate: () => new Date('2026-03-05')}}),
] as never); ] as never);
component.privateShows$.subscribe(shows => { const shows = await firstValueFrom(component.privateShows$);
expect(shows.map(show => show.id)).toEqual(['older-draft', 'pending-own']); expect(shows.map(show => show.id)).toEqual(['pending-own', 'older-draft']);
done();
}); });
it('should hide archived own shows until archived filter is enabled', async () => {
const filterStore = TestBed.inject(FilterStoreService);
shows$.next([
createShow({id: 'draft-own', owner: 'user-1', published: false, reportedType: null, date: {toDate: () => new Date('2026-03-02')}}),
createShow({id: 'archived-own', owner: 'user-1', published: true, archived: true, reportedType: 'reported', date: {toDate: () => new Date('2026-03-03')}}),
] as never);
const initialShows = await firstValueFrom(component.privateShows$.pipe(take(1)));
expect(initialShows.map(show => show.id)).toEqual(['draft-own']);
const updatedShowsPromise = firstValueFrom(component.privateShows$.pipe(skip(1), take(1)));
filterStore.updateShowFilter({archived: true});
const updatedShows = await updatedShowsPromise;
expect(updatedShows.map(show => show.id)).toEqual(['archived-own', 'draft-own']);
});
it('should sort public shows by date descending', async () => {
const filterStore = TestBed.inject(FilterStoreService);
filterStore.updateShowFilter({time: 99999});
shows$.next([
createShow({id: 'old-public', owner: 'user-2', published: true, archived: false, date: {toDate: () => new Date('2026-01-01')}}),
createShow({id: 'new-public', owner: 'user-3', published: true, archived: false, date: {toDate: () => new Date('2026-03-10')}}),
createShow({id: 'mid-public', owner: 'user-4', published: true, archived: false, date: {toDate: () => new Date('2026-02-05')}}),
] as never);
const shows = await firstValueFrom(component.publicShows$.pipe(take(1)));
expect(shows.map(show => show.id)).toEqual(['new-public', 'mid-public', 'old-public']);
}); });
}); });

View File

@@ -7,23 +7,25 @@ import {FilterValues} from './filter/filter-values';
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
import {map, switchMap} from 'rxjs/operators'; import {map, switchMap} from 'rxjs/operators';
import {FilterStoreService} from '../../../services/filter-store.service'; import {FilterStoreService} from '../../../services/filter-store.service';
import {RoleDirective} from '../../../services/user/role.directive';
import {ListHeaderComponent} from '../../../widget-modules/components/list-header/list-header.component';
import {AsyncPipe} from '@angular/common'; import {AsyncPipe} from '@angular/common';
import {FilterComponent} from './filter/filter.component'; import {FilterComponent} from './filter/filter.component';
import {CardComponent} from '../../../widget-modules/components/card/card.component'; import {CardComponent} from '../../../widget-modules/components/card/card.component';
import {ListItemComponent} from './list-item/list-item.component'; import {ListItemComponent} from './list-item/list-item.component';
import {SortByPipe} from '../../../widget-modules/pipes/sort-by/sort-by.pipe';
import {UserService} from '../../../services/user/user.service'; import {UserService} from '../../../services/user/user.service';
import {SidebarComponent} from '../../../widget-modules/components/sidebar/sidebar.component';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
import {faPlus} from '@fortawesome/free-solid-svg-icons';
import {RoleDirective} from '../../../services/user/role.directive';
@Component({ @Component({
selector: 'app-list', selector: 'app-list',
templateUrl: './list.component.html', templateUrl: './list.component.html',
styleUrls: ['./list.component.less'], styleUrls: ['./list.component.less'],
animations: [fade], animations: [fade],
imports: [RoleDirective, ListHeaderComponent, FilterComponent, CardComponent, ListItemComponent, RouterLink, AsyncPipe, SortByPipe], imports: [FilterComponent, CardComponent, ListItemComponent, RouterLink, AsyncPipe, SidebarComponent, ButtonComponent, RoleDirective],
}) })
export class ListComponent { export class ListComponent {
public faNewShow = faPlus;
private showService = inject(ShowService); private showService = inject(ShowService);
private filterStore = inject(FilterStoreService); private filterStore = inject(FilterStoreService);
private userService = inject(UserService); private userService = inject(UserService);
@@ -32,9 +34,24 @@ export class ListComponent {
public lastMonths$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.time || 1)); public lastMonths$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.time || 1));
public owner$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.owner)); public owner$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.owner));
public showType$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.showType)); public showType$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.showType));
public archived$ = this.filter$.pipe(map((filterValues: FilterValues) => !!filterValues.archived));
public shows$ = this.showService.list$(); public shows$ = this.showService.list$();
public privateShows$ = combineLatest([this.shows$, this.userService.user$]).pipe( public ownShows$ = this.showService.list$(false, true);
map(([shows, user]) => shows.filter(show => show.owner === user?.id).filter(show => !show.published || show.reportedType === 'pending')) public privateShows$ = combineLatest([this.ownShows$, this.userService.user$, this.archived$]).pipe(
map(([shows, user, showArchived]) =>
shows.filter(show => {
if (show.owner !== user?.id) {
return false;
}
if (show.archived) {
return showArchived;
}
return !show.published || show.reportedType === 'pending';
})
),
map(shows => this.sortShowsByDateDesc(shows))
); );
public queriedPublicShows$ = this.lastMonths$.pipe(switchMap(lastMonths => this.showService.listPublicSince$(lastMonths))); public queriedPublicShows$ = this.lastMonths$.pipe(switchMap(lastMonths => this.showService.listPublicSince$(lastMonths)));
public fallbackPublicShows$ = combineLatest([this.shows$, this.lastMonths$]).pipe( public fallbackPublicShows$ = combineLatest([this.shows$, this.lastMonths$]).pipe(
@@ -46,9 +63,10 @@ export class ListComponent {
map(([queriedShows, fallbackShows, owner, showType]) => { map(([queriedShows, fallbackShows, owner, showType]) => {
const shows = queriedShows.length > 0 || fallbackShows.length === 0 ? queriedShows : fallbackShows; const shows = queriedShows.length > 0 || fallbackShows.length === 0 ? queriedShows : fallbackShows;
return shows.filter(show => !owner || show.owner === owner).filter(show => !showType || show.showType === showType); return this.sortShowsByDateDesc(shows.filter(show => !owner || show.owner === owner).filter(show => !showType || show.showType === showType));
}) })
); );
public showSidebar$ = this.userService.user$.pipe(map(user => this.hasSidebarAccess(user?.role)));
public trackBy = (index: number, show: unknown) => (show as Show).id; public trackBy = (index: number, show: unknown) => (show as Show).id;
@@ -58,4 +76,17 @@ export class ListComponent {
startDate.setDate(startDate.getDate() - lastMonths * 30); startDate.setDate(startDate.getDate() - lastMonths * 30);
return show.date.toDate() >= startDate; return show.date.toDate() >= startDate;
} }
private sortShowsByDateDesc(shows: Show[]): Show[] {
return [...shows].sort((left, right) => right.date.toDate().getTime() - left.date.toDate().getTime());
}
private hasSidebarAccess(role: string | null | undefined): boolean {
if (!role) {
return false;
}
const roles = role.split(';').map(item => item.trim());
return roles.includes('admin') || roles.includes('leader');
}
} }

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {NewComponent} from './new.component'; import {NewComponent} from './new.component';
@@ -6,11 +6,11 @@ describe('NewComponent', () => {
let component: NewComponent; let component: NewComponent;
let fixture: ComponentFixture<NewComponent>; let fixture: ComponentFixture<NewComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [NewComponent], imports: [NewComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(NewComponent); fixture = TestBed.createComponent(NewComponent);

View File

@@ -1,5 +1,5 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {BehaviorSubject, of} from 'rxjs'; import {BehaviorSubject, firstValueFrom, of} from 'rxjs';
import {ShowDataService} from './show-data.service'; import {ShowDataService} from './show-data.service';
import {ShowService} from './show.service'; import {ShowService} from './show.service';
import {UserService} from '../../../services/user/user.service'; import {UserService} from '../../../services/user/user.service';
@@ -8,6 +8,7 @@ describe('ShowService', () => {
let service: ShowService; let service: ShowService;
let showDataServiceSpy: jasmine.SpyObj<ShowDataService>; let showDataServiceSpy: jasmine.SpyObj<ShowDataService>;
let user$: BehaviorSubject<unknown>; let user$: BehaviorSubject<unknown>;
let shows$: BehaviorSubject<unknown[]>;
const shows = [ const shows = [
{id: 'show-1', owner: 'user-1', published: false, archived: false}, {id: 'show-1', owner: 'user-1', published: false, archived: false},
{id: 'show-2', owner: 'other-user', published: true, archived: false}, {id: 'show-2', owner: 'other-user', published: true, archived: false},
@@ -16,8 +17,9 @@ describe('ShowService', () => {
beforeEach(async () => { beforeEach(async () => {
user$ = new BehaviorSubject<unknown>({id: 'user-1'}); user$ = new BehaviorSubject<unknown>({id: 'user-1'});
shows$ = new BehaviorSubject<unknown[]>(shows as unknown[]);
showDataServiceSpy = jasmine.createSpyObj<ShowDataService>('ShowDataService', ['read$', 'listPublicSince$', 'update', 'add'], { showDataServiceSpy = jasmine.createSpyObj<ShowDataService>('ShowDataService', ['read$', 'listPublicSince$', 'update', 'add'], {
list$: of(shows) as unknown as ShowDataService['list$'], list$: shows$.asObservable() as unknown as ShowDataService['list$'],
}); });
showDataServiceSpy.read$.and.returnValue(of(shows[0])); showDataServiceSpy.read$.and.returnValue(of(shows[0]));
showDataServiceSpy.listPublicSince$.and.returnValue(of([shows[1]])); showDataServiceSpy.listPublicSince$.and.returnValue(of([shows[1]]));
@@ -38,34 +40,39 @@ describe('ShowService', () => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
it('should list published shows and own drafts, but exclude archived ones', done => { it('should list published shows and own drafts, but exclude archived ones', async () => {
service.list$().subscribe(result => { const result = await firstValueFrom(service.list$());
expect(result.map(show => show.id)).toEqual(['show-1', 'show-2']); expect(result.map(show => show.id)).toEqual(['show-1', 'show-2']);
done();
});
}); });
it('should filter out private drafts when publishedOnly is true', done => { it('should filter out private drafts when publishedOnly is true', async () => {
service.list$(true).subscribe(result => { const result = await firstValueFrom(service.list$(true));
expect(result.map(show => show.id)).toEqual(['show-2']); expect(result.map(show => show.id)).toEqual(['show-2']);
done();
});
}); });
it('should delegate public listing to the data service', done => { it('should include own archived shows when requested', async () => {
service.listPublicSince$(6).subscribe(result => { const result = await firstValueFrom(service.list$(false, true));
expect(result).toEqual([shows[1]]); expect(result.map(show => show.id)).toEqual(['show-1', 'show-2', 'show-3']);
});
it('should not include archived shows from other users when requested', async () => {
shows$.next([
...(shows as unknown as unknown[]),
{id: 'show-4', owner: 'other-user', published: true, archived: true},
]);
const result = await firstValueFrom(service.list$(false, true));
expect(result.map(show => show.id)).toEqual(['show-1', 'show-2', 'show-3']);
});
it('should delegate public listing to the data service', async () => {
await expectAsync(firstValueFrom(service.listPublicSince$(6))).toBeResolvedTo([shows[1]]);
expect(showDataServiceSpy.listPublicSince$).toHaveBeenCalledWith(6); expect(showDataServiceSpy.listPublicSince$).toHaveBeenCalledWith(6);
done();
});
}); });
it('should delegate reads to the data service', done => { it('should delegate reads to the data service', async () => {
service.read$('show-1').subscribe(result => { await expectAsync(firstValueFrom(service.read$('show-1'))).toBeResolvedTo(shows[0]);
expect(result).toEqual(shows[0]);
expect(showDataServiceSpy.read$).toHaveBeenCalledWith('show-1'); expect(showDataServiceSpy.read$).toHaveBeenCalledWith('show-1');
done();
});
}); });
it('should delegate updates to the data service', async () => { it('should delegate updates to the data service', async () => {

View File

@@ -20,13 +20,17 @@ export class ShowService {
public read$ = (showId: string): Observable<Show | null> => this.showDataService.read$(showId); public read$ = (showId: string): Observable<Show | null> => this.showDataService.read$(showId);
public listPublicSince$ = (lastMonths: number): Observable<Show[]> => this.showDataService.listPublicSince$(lastMonths); public listPublicSince$ = (lastMonths: number): Observable<Show[]> => this.showDataService.listPublicSince$(lastMonths);
public list$(publishedOnly = false): Observable<Show[]> { public list$(publishedOnly = false, includeOwnArchived = false): Observable<Show[]> {
return this.userService.user$.pipe( return this.userService.user$.pipe(
switchMap( switchMap(
() => this.showDataService.list$, () => this.showDataService.list$,
(user: User | null, shows: Show[]) => ({user, shows}) (user: User | null, shows: Show[]) => ({user, shows})
), ),
map(s => s.shows.filter(show => !show.archived).filter(show => show.published || (show.owner === s.user?.id && !publishedOnly))) map(s =>
s.shows
.filter(show => !show.archived || (includeOwnArchived && show.owner === s.user?.id))
.filter(show => show.published || (show.owner === s.user?.id && !publishedOnly))
)
); );
} }

View File

@@ -59,7 +59,7 @@
} }
.next-song { .next-song {
color: var(--text-muted); color: var(--text-soft);
position: fixed; position: fixed;
bottom: 0; bottom: 0;
right: 10px; right: 10px;
@@ -70,7 +70,7 @@
} }
.time { .time {
color: var(--text-muted); color: var(--text-soft);
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 10px; left: 10px;

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {SongComponent} from './song.component'; import {SongComponent} from './song.component';
@@ -6,11 +6,11 @@ describe('SongComponent', () => {
let component: SongComponent; let component: SongComponent;
let fixture: ComponentFixture<SongComponent>; let fixture: ComponentFixture<SongComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [SongComponent], imports: [SongComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(SongComponent); fixture = TestBed.createComponent(SongComponent);

View File

@@ -4,6 +4,7 @@ import {NewComponent} from './new/new.component';
import {ListComponent} from './list/list.component'; import {ListComponent} from './list/list.component';
import {ShowComponent} from './show/show.component'; import {ShowComponent} from './show/show.component';
import {EditComponent} from './edit/edit.component'; import {EditComponent} from './edit/edit.component';
import {RoleGuard} from '../../widget-modules/guards/role.guard';
const routes: Routes = [ const routes: Routes = [
{ {
@@ -14,6 +15,10 @@ const routes: Routes = [
{ {
path: 'new', path: 'new',
component: NewComponent, component: NewComponent,
canActivate: [RoleGuard],
data: {
requiredRoles: ['leader'],
},
}, },
{ {
path: ':showId/edit', path: ':showId/edit',

View File

@@ -1,5 +1,5 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {of} from 'rxjs'; import {firstValueFrom, of} from 'rxjs';
import {SongService} from './song.service'; import {SongService} from './song.service';
import {SongListResolver} from './song-list.resolver'; import {SongListResolver} from './song-list.resolver';
@@ -22,11 +22,8 @@ describe('SongListResolver', () => {
expect(resolver).toBeTruthy(); expect(resolver).toBeTruthy();
}); });
it('should resolve the first emitted song list from the service', done => { it('should resolve the first emitted song list from the service', async () => {
resolver.resolve().subscribe(songs => { await expectAsync(firstValueFrom(resolver.resolve())).toBeResolvedTo([{id: 'song-1', title: 'Amazing Grace'}] as never);
expect(songServiceSpy.listLoaded$).toHaveBeenCalled(); expect(songServiceSpy.listLoaded$).toHaveBeenCalled();
expect(songs).toEqual([{id: 'song-1', title: 'Amazing Grace'}] as never);
done();
});
}); });
}); });

View File

@@ -1,5 +1,5 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {of} from 'rxjs'; import {firstValueFrom, of} from 'rxjs';
import {SongDataService} from './song-data.service'; import {SongDataService} from './song-data.service';
import {SongService} from './song.service'; import {SongService} from './song.service';
import {UserService} from '../../../services/user/user.service'; import {UserService} from '../../../services/user/user.service';
@@ -41,11 +41,8 @@ describe('SongService', () => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
it('should list songs from the data service', done => { it('should list songs from the data service', async () => {
service.list$().subscribe(songs => { await expectAsync(firstValueFrom(service.list$())).toBeResolvedTo([song]);
expect(songs).toEqual([song]);
done();
});
}); });
it('should delegate reads to the data service', async () => { it('should delegate reads to the data service', async () => {

View File

@@ -38,7 +38,7 @@ Bridge
Cool bridge without any chords Cool bridge without any chords
`; `;
beforeEach(() => void TestBed.configureTestingModule({})); beforeEach(async () => await TestBed.configureTestingModule({}));
it('should be created', () => { it('should be created', () => {
const service: TextRenderingService = TestBed.inject(TextRenderingService); const service: TextRenderingService = TestBed.inject(TextRenderingService);
@@ -478,13 +478,13 @@ Text`;
const service: TextRenderingService = TestBed.inject(TextRenderingService); const service: TextRenderingService = TestBed.inject(TextRenderingService);
const text = 'Strophe\nC\tG\ta\nText'; const text = 'Strophe\nC\tG\ta\nText';
void expect(service.validateChordNotation(text)).toContain( void expect(service.validateChordNotation(text)).toEqual(expect.arrayContaining([
jasmine.objectContaining({ jasmine.objectContaining({
lineNumber: 2, lineNumber: 2,
token: '\t', token: '\t',
reason: 'tab_character', reason: 'tab_character',
}) }),
); ]));
}); });
it('should not flag tabs on non chord lines', () => { it('should not flag tabs on non chord lines', () => {

View File

@@ -7,8 +7,8 @@ import {Line} from './line';
describe('TransposeService', () => { describe('TransposeService', () => {
let service: TransposeService; let service: TransposeService;
beforeEach(() => { beforeEach(async () => {
void TestBed.configureTestingModule({}); await TestBed.configureTestingModule({});
service = TestBed.inject(TransposeService); service = TestBed.inject(TransposeService);
}); });

View File

@@ -1,5 +1,15 @@
.third { .third,
display: grid; :host ::ng-deep form,
grid-template-columns: 1fr 1fr 1fr 1fr; div[formGroup] {
column-gap: 20px; display: flex;
flex-direction: column;
gap: 12px;
}
.third {
gap: 0;
}
:host ::ng-deep .mat-mdc-form-field {
width: 100%;
} }

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {FilterComponent} from './filter.component'; import {FilterComponent} from './filter.component';
@@ -6,11 +6,11 @@ describe('FilterComponent', () => {
let component: FilterComponent; let component: FilterComponent;
let fixture: ComponentFixture<FilterComponent>; let fixture: ComponentFixture<FilterComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [FilterComponent], imports: [FilterComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(FilterComponent); fixture = TestBed.createComponent(FilterComponent);

View File

@@ -1,8 +1,9 @@
@if (songs$ | async; as songs) { @if (songs$ | async; as songs) {
<div> <app-sidebar>
<app-list-header [anyFilterActive]="anyFilterActive"> <div sidebar class="sidebar-content">
<app-filter [songs]="songs"></app-filter> <app-filter [songs]="songs"></app-filter>
</app-list-header> </div>
<div content>
<app-card [padding]="false"> <app-card [padding]="false">
@for (song of songs; track trackBy($index, song)) { @for (song of songs; track trackBy($index, song)) {
<div [routerLink]="song.id" class="list-item"> <div [routerLink]="song.id" class="list-item">
@@ -38,6 +39,10 @@
<div>{{ song.key }}</div> <div>{{ song.key }}</div>
</div> </div>
} }
<div *appRole="['contributor']" class="list-action">
<app-button [fullWidth]="true" [icon]="faNewSong" routerLink="new">Neuen Song anlegen</app-button>
</div>
</app-card> </app-card>
</div> </div>
</app-sidebar>
} }

View File

@@ -1,3 +1,7 @@
.sidebar-content {
padding: 20px;
}
.list-item { .list-item {
padding: 5px 20px; padding: 5px 20px;
display: grid; display: grid;

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {SongListComponent} from './song-list.component'; import {SongListComponent} from './song-list.component';
import {of} from 'rxjs'; import {of} from 'rxjs';
import {ActivatedRoute} from '@angular/router'; import {ActivatedRoute} from '@angular/router';
@@ -12,8 +12,8 @@ describe('SongListComponent', () => {
const songs = [{id: 'song-1', title: 'title1', number: 1, text: '', flags: ''}]; const songs = [{id: 'song-1', title: 'title1', number: 1, text: '', flags: ''}];
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [SongListComponent], imports: [SongListComponent],
providers: [ providers: [
{provide: ActivatedRoute, useValue: {data: of({songs})}}, {provide: ActivatedRoute, useValue: {data: of({songs})}},
@@ -22,7 +22,7 @@ describe('SongListComponent', () => {
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(SongListComponent); fixture = TestBed.createComponent(SongListComponent);

View File

@@ -6,15 +6,16 @@ import {fade} from '../../../animations';
import {ActivatedRoute, RouterLink} from '@angular/router'; import {ActivatedRoute, RouterLink} from '@angular/router';
import {filterSong} from '../../../services/filter.helper'; import {filterSong} from '../../../services/filter.helper';
import {FilterValues} from './filter/filter-values'; import {FilterValues} from './filter/filter-values';
import {faBalanceScaleRight, faCheck, faPencilRuler} from '@fortawesome/free-solid-svg-icons'; import {faBalanceScaleRight, faCheck, faPencilRuler, faPlus} from '@fortawesome/free-solid-svg-icons';
import {TextRenderingService} from '../services/text-rendering.service'; import {TextRenderingService} from '../services/text-rendering.service';
import {FilterStoreService} from '../../../services/filter-store.service'; import {FilterStoreService} from '../../../services/filter-store.service';
import {AsyncPipe} from '@angular/common'; import {AsyncPipe} from '@angular/common';
import {ListHeaderComponent} from '../../../widget-modules/components/list-header/list-header.component';
import {FilterComponent} from './filter/filter.component'; import {FilterComponent} from './filter/filter.component';
import {CardComponent} from '../../../widget-modules/components/card/card.component'; import {CardComponent} from '../../../widget-modules/components/card/card.component';
import {RoleDirective} from '../../../services/user/role.directive'; import {RoleDirective} from '../../../services/user/role.directive';
import {FaIconComponent} from '@fortawesome/angular-fontawesome'; import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {SidebarComponent} from '../../../widget-modules/components/sidebar/sidebar.component';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
interface SongListItem extends Song { interface SongListItem extends Song {
hasChordValidationIssues: boolean; hasChordValidationIssues: boolean;
@@ -26,20 +27,21 @@ interface SongListItem extends Song {
styleUrls: ['./song-list.component.less'], styleUrls: ['./song-list.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
animations: [fade], animations: [fade],
imports: [ListHeaderComponent, FilterComponent, CardComponent, RouterLink, RoleDirective, FaIconComponent, AsyncPipe], imports: [FilterComponent, CardComponent, RouterLink, RoleDirective, FaIconComponent, AsyncPipe, SidebarComponent, ButtonComponent],
}) })
export class SongListComponent { export class SongListComponent {
public faLegal = faBalanceScaleRight;
public faDraft = faPencilRuler;
public faFinal = faCheck;
public faNewSong = faPlus;
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private textRenderingService = inject(TextRenderingService); private textRenderingService = inject(TextRenderingService);
private filterStore = inject(FilterStoreService); private filterStore = inject(FilterStoreService);
public anyFilterActive = false;
public songs$: Observable<SongListItem[]> = combineLatest([ public songs$: Observable<SongListItem[]> = combineLatest([
this.filterStore.songFilter$, this.filterStore.songFilter$,
this.route.data.pipe(map(data => (data['songs'] as Song[]).slice().sort((a, b) => a.number - b.number))), this.route.data.pipe(map(data => (data['songs'] as Song[]).slice().sort((a, b) => a.number - b.number))),
]).pipe( ]).pipe(
map(([filter, songs]) => { map(([filter, songs]) => {
this.anyFilterActive = this.checkIfFilterActive(filter);
return songs return songs
.filter(song => this.filter(song, filter)) .filter(song => this.filter(song, filter))
.map(song => ({ .map(song => ({
@@ -49,9 +51,6 @@ export class SongListComponent {
.sort((a, b) => a.title?.localeCompare(b.title)); .sort((a, b) => a.title?.localeCompare(b.title));
}) })
); );
public faLegal = faBalanceScaleRight;
public faDraft = faPencilRuler;
public faFinal = faCheck;
public trackBy = (index: number, show: SongListItem) => show.id; public trackBy = (index: number, show: SongListItem) => show.id;
@@ -65,10 +64,6 @@ export class SongListComponent {
return baseFilter; return baseFilter;
} }
private checkIfFilterActive(filter: FilterValues): boolean {
return !!filter.q || !!filter.type || !!filter.key || !!filter.legalType || !!filter.flag;
}
private checkFlag(flag: string, flags: string) { private checkFlag(flag: string, flags: string) {
if (!flags) { if (!flags) {
return false; return false;

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {EditFileComponent} from './edit-file.component'; import {EditFileComponent} from './edit-file.component';
@@ -6,11 +6,11 @@ describe('EditFileComponent', () => {
let component: EditFileComponent; let component: EditFileComponent;
let fixture: ComponentFixture<EditFileComponent>; let fixture: ComponentFixture<EditFileComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [EditFileComponent], imports: [EditFileComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(EditFileComponent); fixture = TestBed.createComponent(EditFileComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {FileComponent} from './file.component'; import {FileComponent} from './file.component';
@@ -6,11 +6,11 @@ describe('FileComponent', () => {
let component: FileComponent; let component: FileComponent;
let fixture: ComponentFixture<FileComponent>; let fixture: ComponentFixture<FileComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [FileComponent], imports: [FileComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(FileComponent); fixture = TestBed.createComponent(FileComponent);

View File

@@ -4,8 +4,8 @@ import {EditSongGuard} from './edit-song.guard';
describe('EditSongGuard', () => { describe('EditSongGuard', () => {
let guard: EditSongGuard; let guard: EditSongGuard;
beforeEach(() => { beforeEach(async () => {
void TestBed.configureTestingModule({}); await TestBed.configureTestingModule({});
guard = TestBed.inject(EditSongGuard); guard = TestBed.inject(EditSongGuard);
}); });

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {EditSongComponent} from './edit-song.component'; import {EditSongComponent} from './edit-song.component';
@@ -6,11 +6,11 @@ describe('EditSongComponent', () => {
let component: EditSongComponent; let component: EditSongComponent;
let fixture: ComponentFixture<EditSongComponent>; let fixture: ComponentFixture<EditSongComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [EditSongComponent], imports: [EditSongComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(EditSongComponent); fixture = TestBed.createComponent(EditSongComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {SaveDialogComponent} from './save-dialog.component'; import {SaveDialogComponent} from './save-dialog.component';
@@ -6,11 +6,11 @@ describe('SaveDialogComponent', () => {
let component: SaveDialogComponent; let component: SaveDialogComponent;
let fixture: ComponentFixture<SaveDialogComponent>; let fixture: ComponentFixture<SaveDialogComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [SaveDialogComponent], imports: [SaveDialogComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(SaveDialogComponent); fixture = TestBed.createComponent(SaveDialogComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {EditComponent} from './edit.component'; import {EditComponent} from './edit.component';
@@ -6,11 +6,11 @@ describe('EditComponent', () => {
let component: EditComponent; let component: EditComponent;
let fixture: ComponentFixture<EditComponent>; let fixture: ComponentFixture<EditComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [EditComponent], imports: [EditComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(EditComponent); fixture = TestBed.createComponent(EditComponent);

View File

@@ -4,8 +4,8 @@ import {EditService} from './edit.service';
describe('EditService', () => { describe('EditService', () => {
let service: EditService; let service: EditService;
beforeEach(() => { beforeEach(async () => {
void TestBed.configureTestingModule({}); await TestBed.configureTestingModule({});
service = TestBed.inject(EditService); service = TestBed.inject(EditService);
}); });

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HistoryComponent} from './history.component'; import {HistoryComponent} from './history.component';
@@ -6,11 +6,11 @@ describe('HistoryComponent', () => {
let component: HistoryComponent; let component: HistoryComponent;
let fixture: ComponentFixture<HistoryComponent>; let fixture: ComponentFixture<HistoryComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [HistoryComponent], imports: [HistoryComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(HistoryComponent); fixture = TestBed.createComponent(HistoryComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {Storage} from '@angular/fire/storage'; import {Storage} from '@angular/fire/storage';
import {FileComponent} from './file.component'; import {FileComponent} from './file.component';
@@ -7,12 +7,12 @@ describe('FileComponent', () => {
let component: FileComponent; let component: FileComponent;
let fixture: ComponentFixture<FileComponent>; let fixture: ComponentFixture<FileComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [FileComponent], imports: [FileComponent],
providers: [{provide: Storage, useValue: {}}], providers: [{provide: Storage, useValue: {}}],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(FileComponent); fixture = TestBed.createComponent(FileComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {NewComponent} from './new.component'; import {NewComponent} from './new.component';
@@ -6,11 +6,11 @@ describe('NewComponent', () => {
let component: NewComponent; let component: NewComponent;
let fixture: ComponentFixture<NewComponent>; let fixture: ComponentFixture<NewComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [NewComponent], imports: [NewComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(NewComponent); fixture = TestBed.createComponent(NewComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {SongComponent} from './song.component'; import {SongComponent} from './song.component';
import {of} from 'rxjs'; import {of} from 'rxjs';
@@ -17,7 +17,7 @@ describe('SongComponent', () => {
params: of({songId: '4711'}), params: of({songId: '4711'}),
}; };
beforeEach(waitForAsync(() => { beforeEach(async () => {
const songServiceSpy = jasmine.createSpyObj<SongService>('SongService', ['read$']); const songServiceSpy = jasmine.createSpyObj<SongService>('SongService', ['read$']);
const fileDataServiceSpy = jasmine.createSpyObj<FileDataService>('FileDataService', ['read$']); const fileDataServiceSpy = jasmine.createSpyObj<FileDataService>('FileDataService', ['read$']);
const userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['incSongCount', 'decSongCount'], { const userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['incSongCount', 'decSongCount'], {
@@ -33,7 +33,7 @@ describe('SongComponent', () => {
userServiceSpy.loggedIn$ = jasmine.createSpy('loggedIn$').and.returnValue(of(true)); userServiceSpy.loggedIn$ = jasmine.createSpy('loggedIn$').and.returnValue(of(true));
userServiceSpy.getUserbyId$ = jasmine.createSpy('getUserbyId$').and.returnValue(of({name: 'Benjamin'})); userServiceSpy.getUserbyId$ = jasmine.createSpy('getUserbyId$').and.returnValue(of({name: 'Benjamin'}));
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [SongComponent], imports: [SongComponent],
providers: [ providers: [
{provide: ActivatedRoute, useValue: mockActivatedRoute}, {provide: ActivatedRoute, useValue: mockActivatedRoute},
@@ -44,7 +44,7 @@ describe('SongComponent', () => {
{provide: ShowSongService, useValue: showSongServiceSpy}, {provide: ShowSongService, useValue: showSongServiceSpy},
], ],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(SongComponent); fixture = TestBed.createComponent(SongComponent);

View File

@@ -6,6 +6,7 @@ import {EditComponent} from './song/edit/edit.component';
import {NewComponent} from './song/new/new.component'; import {NewComponent} from './song/new/new.component';
import {EditSongGuard} from './song/edit/edit-song.guard'; import {EditSongGuard} from './song/edit/edit-song.guard';
import {SongListResolver} from './services/song-list.resolver'; import {SongListResolver} from './services/song-list.resolver';
import {RoleGuard} from '../../widget-modules/guards/role.guard';
const routes: Routes = [ const routes: Routes = [
{ {
@@ -19,6 +20,10 @@ const routes: Routes = [
{ {
path: 'new', path: 'new',
component: NewComponent, component: NewComponent,
canActivate: [RoleGuard],
data: {
requiredRoles: ['contributor'],
},
}, },
{ {
path: ':songId/edit', path: ':songId/edit',

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {InfoComponent} from './info.component'; import {InfoComponent} from './info.component';
@@ -6,11 +6,11 @@ describe('InfoComponent', () => {
let component: InfoComponent; let component: InfoComponent;
let fixture: ComponentFixture<InfoComponent>; let fixture: ComponentFixture<InfoComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [InfoComponent], imports: [InfoComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(InfoComponent); fixture = TestBed.createComponent(InfoComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {UserComponent} from './user.component'; import {UserComponent} from './user.component';
@@ -6,11 +6,11 @@ describe('UserComponent', () => {
let component: UserComponent; let component: UserComponent;
let fixture: ComponentFixture<UserComponent>; let fixture: ComponentFixture<UserComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [UserComponent], imports: [UserComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(UserComponent); fixture = TestBed.createComponent(UserComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {UsersComponent} from './users.component'; import {UsersComponent} from './users.component';
@@ -6,11 +6,11 @@ describe('UsersComponent', () => {
let component: UsersComponent; let component: UsersComponent;
let fixture: ComponentFixture<UsersComponent>; let fixture: ComponentFixture<UsersComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [UsersComponent], imports: [UsersComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(UsersComponent); fixture = TestBed.createComponent(UsersComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {LoginComponent} from './login.component'; import {LoginComponent} from './login.component';
@@ -6,11 +6,11 @@ describe('LoginComponent', () => {
let component: LoginComponent; let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>; let fixture: ComponentFixture<LoginComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [LoginComponent], imports: [LoginComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent); fixture = TestBed.createComponent(LoginComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {LogoutComponent} from './logout.component'; import {LogoutComponent} from './logout.component';
@@ -6,11 +6,11 @@ describe('LogoutComponent', () => {
let component: LogoutComponent; let component: LogoutComponent;
let fixture: ComponentFixture<LogoutComponent>; let fixture: ComponentFixture<LogoutComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [LogoutComponent], imports: [LogoutComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(LogoutComponent); fixture = TestBed.createComponent(LogoutComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {NewComponent} from './new.component'; import {NewComponent} from './new.component';
@@ -6,11 +6,11 @@ describe('NewComponent', () => {
let component: NewComponent; let component: NewComponent;
let fixture: ComponentFixture<NewComponent>; let fixture: ComponentFixture<NewComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [NewComponent], imports: [NewComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(NewComponent); fixture = TestBed.createComponent(NewComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {PasswordSendComponent} from './password-send.component'; import {PasswordSendComponent} from './password-send.component';
@@ -6,11 +6,11 @@ describe('PasswordSendComponent', () => {
let component: PasswordSendComponent; let component: PasswordSendComponent;
let fixture: ComponentFixture<PasswordSendComponent>; let fixture: ComponentFixture<PasswordSendComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [PasswordSendComponent], imports: [PasswordSendComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(PasswordSendComponent); fixture = TestBed.createComponent(PasswordSendComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {PasswordComponent} from './password.component'; import {PasswordComponent} from './password.component';
@@ -6,11 +6,11 @@ describe('PasswordComponent', () => {
let component: PasswordComponent; let component: PasswordComponent;
let fixture: ComponentFixture<PasswordComponent>; let fixture: ComponentFixture<PasswordComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [PasswordComponent], imports: [PasswordComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(PasswordComponent); fixture = TestBed.createComponent(PasswordComponent);

View File

@@ -1,5 +1,5 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {of} from 'rxjs'; import {firstValueFrom, of} from 'rxjs';
import {DbService} from './db.service'; import {DbService} from './db.service';
import {ConfigService} from './config.service'; import {ConfigService} from './config.service';
@@ -27,11 +27,8 @@ describe('ConfigService', () => {
expect(dbServiceSpy.doc$).toHaveBeenCalledWith('global/config'); expect(dbServiceSpy.doc$).toHaveBeenCalledWith('global/config');
}); });
it('should expose the shared config stream via get$', done => { it('should expose the shared config stream via get$', async () => {
service.get$().subscribe(config => { await expectAsync(firstValueFrom(service.get$())).toBeResolvedTo({copyright: 'CCLI'} as never);
expect(config).toEqual({copyright: 'CCLI'} as never);
done();
});
}); });
it('should resolve the current config via get()', async () => { it('should resolve the current config via get()', async () => {

View File

@@ -4,12 +4,11 @@ import {Firestore} from '@angular/fire/firestore';
import {DbService} from './db.service'; import {DbService} from './db.service';
describe('DbService', () => { describe('DbService', () => {
beforeEach( beforeEach(async () => {
() => await TestBed.configureTestingModule({
void TestBed.configureTestingModule({
providers: [{provide: Firestore, useValue: {}}], providers: [{provide: Firestore, useValue: {}}],
}) });
); });
it('should be created', () => { it('should be created', () => {
const service: DbService = TestBed.inject(DbService); const service: DbService = TestBed.inject(DbService);

View File

@@ -15,6 +15,7 @@ const DEFAULT_SHOW_FILTER: ShowFilterValues = {
time: 1, time: 1,
owner: '', owner: '',
showType: '', showType: '',
archived: false,
}; };
@Injectable({ @Injectable({

View File

@@ -1,5 +1,5 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {of} from 'rxjs'; import {firstValueFrom, of} from 'rxjs';
import {DbService} from './db.service'; import {DbService} from './db.service';
import {GlobalSettingsService} from './global-settings.service'; import {GlobalSettingsService} from './global-settings.service';
@@ -30,11 +30,8 @@ describe('GlobalSettingsService', () => {
expect(dbServiceSpy.doc$).toHaveBeenCalledWith('global/static'); expect(dbServiceSpy.doc$).toHaveBeenCalledWith('global/static');
}); });
it('should expose the shared settings stream via the getter', done => { it('should expose the shared settings stream via the getter', async () => {
service.get$.subscribe(settings => { await expectAsync(firstValueFrom(service.get$)).toBeResolvedTo({churchName: 'ICF'} as never);
expect(settings).toEqual({churchName: 'ICF'} as never);
done();
});
}); });
it('should update the static global settings document', async () => { it('should update the static global settings document', async () => {

View File

@@ -5,8 +5,8 @@ import {ScrollService} from './scroll.service';
describe('ScrollService', () => { describe('ScrollService', () => {
let service: ScrollService; let service: ScrollService;
beforeEach(() => { beforeEach(async () => {
void TestBed.configureTestingModule({}); await TestBed.configureTestingModule({});
service = TestBed.inject(ScrollService); service = TestBed.inject(ScrollService);
}); });

View File

@@ -1,5 +1,5 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {of} from 'rxjs'; import {firstValueFrom, of} from 'rxjs';
import {UserService} from '../user.service'; import {UserService} from '../user.service';
import {UserNameComponent} from './user-name.component'; import {UserNameComponent} from './user-name.component';
@@ -8,15 +8,15 @@ describe('UserNameComponent', () => {
let fixture: ComponentFixture<UserNameComponent>; let fixture: ComponentFixture<UserNameComponent>;
let userServiceSpy: jasmine.SpyObj<UserService>; let userServiceSpy: jasmine.SpyObj<UserService>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['getUserbyId$']); userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['getUserbyId$']);
userServiceSpy.getUserbyId$.and.returnValue(of({name: 'Benjamin'} as never)); userServiceSpy.getUserbyId$.and.returnValue(of({name: 'Benjamin'} as never));
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [UserNameComponent], imports: [UserNameComponent],
providers: [{provide: UserService, useValue: userServiceSpy}], providers: [{provide: UserService, useValue: userServiceSpy}],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(UserNameComponent); fixture = TestBed.createComponent(UserNameComponent);
@@ -28,24 +28,18 @@ describe('UserNameComponent', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should resolve the user name when the userId input changes', done => { it('should resolve the user name when the userId input changes', async () => {
component.userId = 'user-1'; component.userId = 'user-1';
component.name$?.subscribe(name => {
expect(userServiceSpy.getUserbyId$).toHaveBeenCalledWith('user-1'); expect(userServiceSpy.getUserbyId$).toHaveBeenCalledWith('user-1');
expect(name).toBe('Benjamin'); await expectAsync(firstValueFrom(component.name$!)).toBeResolvedTo('Benjamin');
done();
});
}); });
it('should map missing users to null names', done => { it('should map missing users to null names', async () => {
userServiceSpy.getUserbyId$.and.returnValue(of(null)); userServiceSpy.getUserbyId$.and.returnValue(of(null));
component.userId = 'missing-user'; component.userId = 'missing-user';
component.name$?.subscribe(name => { await expectAsync(firstValueFrom(component.name$!)).toBeResolvedTo(null);
expect(name).toBeNull();
done();
});
}); });
}); });

View File

@@ -1,5 +1,5 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {of} from 'rxjs'; import {firstValueFrom, of} from 'rxjs';
import {UserService} from './user.service'; import {UserService} from './user.service';
import {UserSessionService} from './user-session.service'; import {UserSessionService} from './user-session.service';
import {UserSongUsageService} from './user-song-usage.service'; import {UserSongUsageService} from './user-song-usage.service';
@@ -54,17 +54,10 @@ describe('UserService', () => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
it('should expose the session streams directly', done => { it('should expose the session streams directly', async () => {
service.userId$.subscribe(userId => { await expectAsync(firstValueFrom(service.userId$)).toBeResolvedTo('user-1');
expect(userId).toBe('user-1'); await expectAsync(firstValueFrom(service.user$)).toBeResolvedTo({id: 'user-1'} as never);
service.user$.subscribe(user => { await expectAsync(firstValueFrom(service.users$)).toBeResolvedTo([{id: 'user-1'}] as never);
expect(user).toEqual({id: 'user-1'} as never);
service.users$.subscribe(users => {
expect(users).toEqual([{id: 'user-1'}] as never);
done();
});
});
});
}); });
it('should delegate session operations to UserSessionService', async () => { it('should delegate session operations to UserSessionService', async () => {
@@ -85,20 +78,13 @@ describe('UserService', () => {
expect(sessionSpy.createNewUser).toHaveBeenCalledWith('mail', 'Benjamin', 'secret'); expect(sessionSpy.createNewUser).toHaveBeenCalledWith('mail', 'Benjamin', 'secret');
}); });
it('should delegate user lookup and loggedIn/list streams to UserSessionService', done => { it('should delegate user lookup and loggedIn/list streams to UserSessionService', async () => {
service.getUserbyId$('user-2').subscribe(user => { await expectAsync(firstValueFrom(service.getUserbyId$('user-2'))).toBeResolvedTo({id: 'user-2'} as never);
expect(user).toEqual({id: 'user-2'} as never); await expectAsync(firstValueFrom(service.loggedIn$())).toBeResolvedTo(true);
service.loggedIn$().subscribe(loggedIn => { await expectAsync(firstValueFrom(service.list$())).toBeResolvedTo([{id: 'user-1'}] as never);
expect(loggedIn).toBeTrue();
service.list$().subscribe(users => {
expect(users).toEqual([{id: 'user-1'}] as never);
expect(sessionSpy.getUserbyId$).toHaveBeenCalledWith('user-2'); expect(sessionSpy.getUserbyId$).toHaveBeenCalledWith('user-2');
expect(sessionSpy.loggedIn$).toHaveBeenCalled(); expect(sessionSpy.loggedIn$).toHaveBeenCalled();
expect(sessionSpy.list$).toHaveBeenCalled(); expect(sessionSpy.list$).toHaveBeenCalled();
done();
});
});
});
}); });
it('should delegate song usage operations to UserSongUsageService', async () => { it('should delegate song usage operations to UserSongUsageService', async () => {

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {AddSongComponent} from './add-song.component'; import {AddSongComponent} from './add-song.component';
@@ -6,11 +6,11 @@ describe('AddSongComponent', () => {
let component: AddSongComponent; let component: AddSongComponent;
let fixture: ComponentFixture<AddSongComponent>; let fixture: ComponentFixture<AddSongComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [AddSongComponent], imports: [AddSongComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(AddSongComponent); fixture = TestBed.createComponent(AddSongComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {BrandComponent} from './brand.component'; import {BrandComponent} from './brand.component';
@@ -6,11 +6,11 @@ describe('BrandComponent', () => {
let component: BrandComponent; let component: BrandComponent;
let fixture: ComponentFixture<BrandComponent>; let fixture: ComponentFixture<BrandComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [BrandComponent], imports: [BrandComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(BrandComponent); fixture = TestBed.createComponent(BrandComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {FilterComponent} from './filter.component'; import {FilterComponent} from './filter.component';
@@ -6,11 +6,11 @@ describe('FilterComponent', () => {
let component: FilterComponent; let component: FilterComponent;
let fixture: ComponentFixture<FilterComponent>; let fixture: ComponentFixture<FilterComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [FilterComponent], imports: [FilterComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(FilterComponent); fixture = TestBed.createComponent(FilterComponent);

View File

@@ -3,18 +3,17 @@ a {
display: block; display: block;
height: 50px; height: 50px;
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: 500;
text-decoration: none; text-decoration: none;
letter-spacing: 1.1px;
padding: 15px; padding: 15px;
box-sizing: border-box; box-sizing: border-box;
background: transparent; background: transparent;
transition: var(--transition); transition: var(--transition-fast);
border-color: transparent; border-color: transparent;
fa-icon { fa-icon {
display: inline-block; display: inline-block;
transform: scale(1);
transition: var(--transition);
} }
@media screen and (max-width: 860px) { @media screen and (max-width: 860px) {
@@ -28,18 +27,11 @@ a {
color: var(--primary-hover); color: var(--primary-hover);
border-bottom: 5px solid var(--hover-background); border-bottom: 5px solid var(--hover-background);
fa-icon {
transform: scale(1.2);
}
} }
&.active { &.active {
border-bottom: 5px solid var(--primary-color); border-bottom: 5px solid var(--primary-color);
opacity: 1; opacity: 1;
color: var(--text-inverse); color: var(--text-inverse);
fa-icon {
transform: scale(1.3);
}
} }
} }

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {LinkComponent} from './link.component'; import {LinkComponent} from './link.component';
@@ -6,11 +6,11 @@ describe('LinkComponent', () => {
let component: LinkComponent; let component: LinkComponent;
let fixture: ComponentFixture<LinkComponent>; let fixture: ComponentFixture<LinkComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [LinkComponent], imports: [LinkComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(LinkComponent); fixture = TestBed.createComponent(LinkComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {NavigationComponent} from './navigation.component'; import {NavigationComponent} from './navigation.component';
@@ -6,11 +6,11 @@ describe('NavigationComponent', () => {
let component: NavigationComponent; let component: NavigationComponent;
let fixture: ComponentFixture<NavigationComponent>; let fixture: ComponentFixture<NavigationComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [NavigationComponent], imports: [NavigationComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(NavigationComponent); fixture = TestBed.createComponent(NavigationComponent);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ButtonRowComponent} from './button-row.component'; import {ButtonRowComponent} from './button-row.component';
@@ -6,11 +6,11 @@ describe('ButtonRowComponent', () => {
let component: ButtonRowComponent; let component: ButtonRowComponent;
let fixture: ComponentFixture<ButtonRowComponent>; let fixture: ComponentFixture<ButtonRowComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ButtonRowComponent], imports: [ButtonRowComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ButtonRowComponent); fixture = TestBed.createComponent(ButtonRowComponent);

View File

@@ -1,7 +1,21 @@
:host {
display: inline-flex;
}
:host(.full-width) {
display: flex;
width: 100%;
}
button { button {
color: var(--text); color: var(--text);
transition: var(--transition); transition: var(--transition);
:host(.full-width) & {
width: 100%;
justify-content: center;
}
&:hover { &:hover {
color: var(--primary-active); color: var(--primary-active);
} }

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ButtonComponent} from './button.component'; import {ButtonComponent} from './button.component';
@@ -6,11 +6,11 @@ describe('ButtonComponent', () => {
let component: ButtonComponent; let component: ButtonComponent;
let fixture: ComponentFixture<ButtonComponent>; let fixture: ComponentFixture<ButtonComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ButtonComponent], imports: [ButtonComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ButtonComponent); fixture = TestBed.createComponent(ButtonComponent);

View File

@@ -9,8 +9,12 @@ import {FaIconComponent} from '@fortawesome/angular-fontawesome';
templateUrl: './button.component.html', templateUrl: './button.component.html',
styleUrls: ['./button.component.less'], styleUrls: ['./button.component.less'],
imports: [MatButton, FaIconComponent], imports: [MatButton, FaIconComponent],
host: {
'[class.full-width]': 'fullWidth',
},
}) })
export class ButtonComponent { export class ButtonComponent {
@Input() public disabled = false; @Input() public disabled = false;
@Input() public fullWidth = false;
@Input() public icon: IconProp | null = null; @Input() public icon: IconProp | null = null;
} }

View File

@@ -1,4 +1,4 @@
<div [class.fullscreen]="fullscreen" [class.padding]="padding" class="card"> <div @fade [class.fullscreen]="fullscreen" [class.padding]="padding" class="card">
@if (closeLink && !fullscreen) { @if (closeLink && !fullscreen) {
<button [routerLink]="closeLink" class="btn-close" mat-icon-button> <button [routerLink]="closeLink" class="btn-close" mat-icon-button>
<fa-icon [icon]="closeIcon"></fa-icon> <fa-icon [icon]="closeIcon"></fa-icon>

View File

@@ -59,7 +59,7 @@
margin-bottom: 20px; margin-bottom: 20px;
margin-right: 20px; margin-right: 20px;
opacity: 0.7; opacity: 0.7;
color: var(--text-muted); color: var(--text-soft);
padding-left: 20px; padding-left: 20px;
padding-top: 20px; padding-top: 20px;
} }
@@ -70,9 +70,10 @@
} }
.btn-close { .btn-close {
--icon-button-color: var(--text-soft);
--icon-button-hover-color: var(--text);
position: absolute; position: absolute;
right: 10px; right: 10px;
top: 15px; top: 15px;
opacity: 0.7; opacity: 0.7;
color: var(--text-muted);
} }

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {CardComponent} from './card.component'; import {CardComponent} from './card.component';
@@ -6,11 +6,11 @@ describe('CardComponent', () => {
let component: CardComponent; let component: CardComponent;
let fixture: ComponentFixture<CardComponent>; let fixture: ComponentFixture<CardComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [CardComponent], imports: [CardComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(CardComponent); fixture = TestBed.createComponent(CardComponent);

View File

@@ -4,12 +4,14 @@ import {faTimes} from '@fortawesome/free-solid-svg-icons';
import {MatIconButton} from '@angular/material/button'; import {MatIconButton} from '@angular/material/button';
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
import {FaIconComponent} from '@fortawesome/angular-fontawesome'; import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {fade} from '../../../animations';
@Component({ @Component({
selector: 'app-card', selector: 'app-card',
templateUrl: './card.component.html', templateUrl: './card.component.html',
styleUrls: ['./card.component.less'], styleUrls: ['./card.component.less'],
imports: [MatIconButton, RouterLink, FaIconComponent], imports: [MatIconButton, RouterLink, FaIconComponent],
animations: [fade],
}) })
export class CardComponent { export class CardComponent {
@Input() public padding = true; @Input() public padding = true;

View File

@@ -1,13 +1,15 @@
<div class="header"> <div class="header">
@if (showFilterButton) {
<button (click)="onFilterClick()" [class.filter-active]="anyFilterActive" mat-icon-button> <button (click)="onFilterClick()" [class.filter-active]="anyFilterActive" mat-icon-button>
<fa-icon [icon]="faFilter"></fa-icon> <fa-icon [icon]="faFilter"></fa-icon>
</button> </button>
}
<button mat-icon-button routerLink="new"> <button mat-icon-button routerLink="new">
<fa-icon [icon]="faNew"></fa-icon> <fa-icon [icon]="faNew"></fa-icon>
</button> </button>
</div> </div>
@if (filterVisible || anyFilterActive) { @if (showFilterButton && (filterVisible || anyFilterActive)) {
<div @fade> <div @fade>
<app-card> <app-card>
<ng-content></ng-content> <ng-content></ng-content>

View File

@@ -10,11 +10,15 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
color: var(--primary-hover);
} }
.filter-active { .header .mat-mdc-icon-button {
color: var(--danger); --icon-button-color: var(--primary-hover);
--icon-button-hover-color: var(--primary-active);
}
.header .mat-mdc-icon-button.filter-active {
--icon-button-color: var(--danger);
--icon-button-hover-color: var(--danger);
cursor: not-allowed; cursor: not-allowed;
} }

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ListHeaderComponent} from './list-header.component'; import {ListHeaderComponent} from './list-header.component';
@@ -6,11 +6,11 @@ describe('ListHeaderComponent', () => {
let component: ListHeaderComponent; let component: ListHeaderComponent;
let fixture: ComponentFixture<ListHeaderComponent>; let fixture: ComponentFixture<ListHeaderComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ListHeaderComponent], imports: [ListHeaderComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ListHeaderComponent); fixture = TestBed.createComponent(ListHeaderComponent);

View File

@@ -20,6 +20,7 @@ export class ListHeaderComponent {
public filterVisible = false; public filterVisible = false;
@Output() public filterVisibleChanged = new EventEmitter<boolean>(); @Output() public filterVisibleChanged = new EventEmitter<boolean>();
@Input() public anyFilterActive = false; @Input() public anyFilterActive = false;
@Input() public showFilterButton = true;
public onFilterClick(): void { public onFilterClick(): void {
this.filterVisible = !this.filterVisible || this.anyFilterActive; this.filterVisible = !this.filterVisible || this.anyFilterActive;

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {LogoComponent} from './logo.component'; import {LogoComponent} from './logo.component';
@@ -6,11 +6,11 @@ describe('LogoComponent', () => {
let component: LogoComponent; let component: LogoComponent;
let fixture: ComponentFixture<LogoComponent>; let fixture: ComponentFixture<LogoComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [LogoComponent], imports: [LogoComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(LogoComponent); fixture = TestBed.createComponent(LogoComponent);

View File

@@ -1,4 +1,6 @@
button { .mat-mdc-button {
--icon-button-color: var(--primary-color);
--icon-button-hover-color: var(--primary-active);
min-width: 0; min-width: 0;
padding: 0 var(--button-padding, 5px); padding: 0 var(--button-padding, 5px);
font-size: var(--button-font-size, 1em); font-size: var(--button-font-size, 1em);

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MenuButtonComponent} from './menu-button.component'; import {MenuButtonComponent} from './menu-button.component';
@@ -6,11 +6,11 @@ describe('MenuButtonComponent', () => {
let component: MenuButtonComponent; let component: MenuButtonComponent;
let fixture: ComponentFixture<MenuButtonComponent>; let fixture: ComponentFixture<MenuButtonComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [MenuButtonComponent], imports: [MenuButtonComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(MenuButtonComponent); fixture = TestBed.createComponent(MenuButtonComponent);

View File

@@ -0,0 +1,12 @@
<button class="sidebar-toggle" (click)="toggle()" [attr.aria-expanded]="!collapsed" aria-label="Sidebar umschalten" mat-icon-button type="button">
<fa-icon [icon]="collapsed ? closedIcon : openIcon"></fa-icon>
</button>
<aside [class.collapsed]="collapsed">
<div class="sidebar-toggle-placeholder" aria-hidden="true"></div>
<div class="sidebar-body">
<ng-content select="[sidebar]"></ng-content>
</div>
</aside>
<div class="content">
<ng-content select="[content]"></ng-content>
</div>

View File

@@ -0,0 +1,78 @@
:host {
--sidebar-width: 300px;
--sidebar-toggle-size: 48px;
--sidebar-toggle-offset: 12px;
display: grid;
width: 100%;
max-width: 100%;
min-width: 0;
grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
align-items: start;
box-sizing: border-box;
transition: grid-template-columns 200ms ease;
}
:host.collapsed {
grid-template-columns: 0 minmax(0, 1fr);
}
.sidebar-toggle {
--icon-button-color: var(--primary-hover);
--icon-button-hover-color: var(--primary-active);
position: fixed;
top: calc(50px + var(--sidebar-toggle-offset));
left: var(--sidebar-toggle-offset);
z-index: 11;
color: var(--icon-button-color);
background: transparent;
box-shadow: none;
}
:host.collapsed .sidebar-toggle {
--icon-button-color: var(--text-inverse);
--icon-button-hover-color: var(--text-inverse);
}
.sidebar-toggle fa-icon {
color: inherit;
}
.sidebar-toggle:hover {
color: var(--icon-button-hover-color);
}
aside {
width: var(--sidebar-width);
height: calc(100vh - 50px);
position: fixed;
top: 50px;
left: 0;
bottom: 0;
background: var(--surface);
box-shadow: var(--shadow-card-2);
overflow: hidden;
transform: translateX(0);
transition: transform 200ms ease;
}
aside.collapsed {
transform: translateX(calc(-1 * var(--sidebar-width)));
}
.sidebar-toggle-placeholder {
height: calc(var(--sidebar-toggle-size) + var(--sidebar-toggle-offset));
flex: 0 0 auto;
}
.sidebar-body {
height: calc(100% - var(--sidebar-toggle-size) - var(--sidebar-toggle-offset));
}
.content {
grid-column: 2;
min-width: 0;
width: 100%;
padding: 0;
}

View File

@@ -0,0 +1,22 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {SidebarComponent} from './sidebar.component';
describe('SidebarComponent', () => {
let component: SidebarComponent;
let fixture: ComponentFixture<SidebarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SidebarComponent],
}).compileComponents();
fixture = TestBed.createComponent(SidebarComponent);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,23 @@
import {Component} from '@angular/core';
import {MatIconButton} from '@angular/material/button';
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {faBars, faChevronLeft} from '@fortawesome/free-solid-svg-icons';
@Component({
selector: 'app-sidebar',
imports: [MatIconButton, FaIconComponent],
templateUrl: './sidebar.component.html',
styleUrl: './sidebar.component.less',
host: {
'[class.collapsed]': 'collapsed',
},
})
export class SidebarComponent {
public collapsed = true;
public openIcon = faChevronLeft;
public closedIcon = faBars;
public toggle(): void {
this.collapsed = !this.collapsed;
}
}

View File

@@ -1,4 +1,4 @@
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {SongTextComponent} from './song-text.component'; import {SongTextComponent} from './song-text.component';
@@ -6,11 +6,11 @@ describe('SongTextComponent', () => {
let component: SongTextComponent; let component: SongTextComponent;
let fixture: ComponentFixture<SongTextComponent>; let fixture: ComponentFixture<SongTextComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
void TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [SongTextComponent], imports: [SongTextComponent],
}).compileComponents(); }).compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(SongTextComponent); fixture = TestBed.createComponent(SongTextComponent);

View File

@@ -1,6 +1,6 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {of} from 'rxjs'; import {firstValueFrom, of} from 'rxjs';
import {UserService} from '../../services/user/user.service'; import {UserService} from '../../services/user/user.service';
import {RoleGuard} from './role.guard'; import {RoleGuard} from './role.guard';
@@ -30,14 +30,13 @@ describe('RoleGuard', () => {
expect(() => guard.canActivate({data: {}} as never)).toThrowError('requiredRoles is not defined!'); expect(() => guard.canActivate({data: {}} as never)).toThrowError('requiredRoles is not defined!');
}); });
it('should deny access when there is no current user', done => { it('should deny access when there is no current user', async () => {
guard.canActivate({data: {requiredRoles: ['leader']}} as never).subscribe(result => { await expectAsync(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).toBeResolvedTo(
expect(result).toEqual({commands: ['brand', 'new-user']} as never); {commands: ['brand', 'new-user']} as never
done(); );
});
}); });
it('should allow admins regardless of requiredRoles', done => { it('should allow admins regardless of requiredRoles', async () => {
TestBed.resetTestingModule(); TestBed.resetTestingModule();
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']); routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -48,13 +47,10 @@ describe('RoleGuard', () => {
}); });
guard = TestBed.inject(RoleGuard); guard = TestBed.inject(RoleGuard);
guard.canActivate({data: {requiredRoles: ['leader']}} as never).subscribe(result => { await expectAsync(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).toBeResolvedTo(true);
expect(result).toBeTrue();
done();
});
}); });
it('should allow users with a matching required role', done => { it('should allow users with a matching required role', async () => {
TestBed.resetTestingModule(); TestBed.resetTestingModule();
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']); routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -65,13 +61,10 @@ describe('RoleGuard', () => {
}); });
guard = TestBed.inject(RoleGuard); guard = TestBed.inject(RoleGuard);
guard.canActivate({data: {requiredRoles: ['leader']}} as never).subscribe(result => { await expectAsync(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).toBeResolvedTo(true);
expect(result).toBeTrue();
done();
});
}); });
it('should redirect users without the required role to their role default route', done => { it('should redirect users without the required role to their role default route', async () => {
TestBed.resetTestingModule(); TestBed.resetTestingModule();
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']); routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
routerSpy.createUrlTree.and.returnValue({redirect: ['presentation']} as never); routerSpy.createUrlTree.and.returnValue({redirect: ['presentation']} as never);
@@ -83,14 +76,12 @@ describe('RoleGuard', () => {
}); });
guard = TestBed.inject(RoleGuard); guard = TestBed.inject(RoleGuard);
guard.canActivate({data: {requiredRoles: ['leader']}} as never).subscribe(result => { const result = await firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never));
expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['presentation']); expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['presentation']);
expect(result).toEqual({redirect: ['presentation']} as never); expect(result).toEqual({redirect: ['presentation']} as never);
done();
});
}); });
it('should redirect members to shows instead of new-user', done => { it('should redirect members to shows instead of new-user', async () => {
TestBed.resetTestingModule(); TestBed.resetTestingModule();
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']); routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
routerSpy.createUrlTree.and.returnValue({redirect: ['shows']} as never); routerSpy.createUrlTree.and.returnValue({redirect: ['shows']} as never);
@@ -102,14 +93,12 @@ describe('RoleGuard', () => {
}); });
guard = TestBed.inject(RoleGuard); guard = TestBed.inject(RoleGuard);
guard.canActivate({data: {requiredRoles: ['user']}} as never).subscribe(result => { const result = await firstValueFrom(guard.canActivate({data: {requiredRoles: ['user']}} as never));
expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['shows']); expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['shows']);
expect(result).toEqual({redirect: ['shows']} as never); expect(result).toEqual({redirect: ['shows']} as never);
done();
});
}); });
it('should choose a matching default route from all assigned roles', done => { it('should choose a matching default route from all assigned roles', async () => {
TestBed.resetTestingModule(); TestBed.resetTestingModule();
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']); routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
routerSpy.createUrlTree.and.callFake(commands => ({redirect: commands}) as never); routerSpy.createUrlTree.and.callFake(commands => ({redirect: commands}) as never);
@@ -121,10 +110,8 @@ describe('RoleGuard', () => {
}); });
guard = TestBed.inject(RoleGuard); guard = TestBed.inject(RoleGuard);
guard.canActivate({data: {requiredRoles: ['presenter']}} as never).subscribe(result => { const result = await firstValueFrom(guard.canActivate({data: {requiredRoles: ['presenter']}} as never));
expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['shows']); expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['shows']);
expect(result).toEqual({redirect: ['shows']} as never); expect(result).toEqual({redirect: ['shows']} as never);
done();
});
}); });
}); });

View File

@@ -16,7 +16,7 @@ $wgenerator-theme: mat.m2-define-light-theme((
warn: $wgenerator-warn, warn: $wgenerator-warn,
), ),
typography: mat.m2-define-typography-config(), typography: mat.m2-define-typography-config(),
density: 0, density: -2,
)); ));
@include mat.all-component-themes($wgenerator-theme); @include mat.all-component-themes($wgenerator-theme);

View File

@@ -28,7 +28,7 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
display: flex; display: flex;
z-index: 1; z-index: 100;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }

View File

@@ -11,7 +11,6 @@
--surface-muted: rgba(41, 46, 73, 0.06); --surface-muted: rgba(41, 46, 73, 0.06);
--text: #1f2433; --text: #1f2433;
--text-muted: #5f6b73;
--text-soft: #7a858c; --text-soft: #7a858c;
--text-inverse: #f7fbff; --text-inverse: #f7fbff;
@@ -34,6 +33,14 @@
--link-color: var(--primary-active); --link-color: var(--primary-active);
--focus-ring: 0 0 0 2px rgba(111, 143, 149, 0.28); --focus-ring: 0 0 0 2px rgba(111, 143, 149, 0.28);
--transition: all 300ms ease-in-out; --transition: all 300ms ease-in-out;
--transition-fast: all 150ms ease-in-out;
--icon-button-color: var(--primary-color);
--icon-button-hover-color: var(--primary-active);
--icon-button-opacity: 1;
--icon-button-hover-opacity: 1;
--mat-dialog-supporting-text-color: var(--text);
} }
html { html {
@@ -75,11 +82,11 @@ a {
} }
.mat-mdc-icon-button { .mat-mdc-icon-button {
color: var(--primary-color) !important; color: var(--icon-button-color, var(--primary-color));
transition: var(--transition); transition: var(--transition);
&:hover { &:hover {
color: var(--primary-active) !important; color: var(--icon-button-hover-color, var(--primary-active));
} }
} }
@@ -100,11 +107,11 @@ body .cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing {
} }
.btn-icon { .btn-icon {
opacity: 0.2; opacity: var(--icon-button-opacity);
transition: var(--transition); transition: var(--transition);
&:hover { &:hover {
opacity: 1; opacity: var(--icon-button-hover-opacity);
} }
} }
@@ -123,6 +130,5 @@ body {
opacity: 0; opacity: 0;
} }
--mat-form-field-container-text-line-height: 16px;
--mat-form-field-container-text-size: 16px;
} }

228
src/test-vitest.ts Normal file
View File

@@ -0,0 +1,228 @@
import {expect, vi} from 'vitest';
import 'zone.js/testing';
import {TestBed} from '@angular/core/testing';
import {provideNoopAnimations} from '@angular/platform-browser/animations';
import {ActivatedRoute, provideRouter} from '@angular/router';
import {BehaviorSubject, Observable, of} from 'rxjs';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {provideNativeDateAdapter} from '@angular/material/core';
import {getApp, getApps, initializeApp, provideFirebaseApp} from '@angular/fire/app';
import {getAuth, provideAuth} from '@angular/fire/auth';
import {initializeFirestore, provideFirestore} from '@angular/fire/firestore';
import {getStorage, provideStorage} from '@angular/fire/storage';
import {environment} from './environments/environment';
import {DbService} from './app/services/db.service';
type TestingModuleDefinition = Parameters<typeof TestBed.configureTestingModule>[0];
type TestingProviderList = NonNullable<NonNullable<TestingModuleDefinition>['providers']>;
type CollectionStub = {
valueChanges: () => Observable<unknown[]>;
add: () => Promise<{id: string}>;
};
type DocumentStub = {
set: () => Promise<void>;
update: () => Promise<void>;
delete: () => Promise<void>;
collection: () => CollectionStub;
};
type MockFunction = ReturnType<typeof vi.fn> & {
and: {
returnValue: (value: unknown) => MockFunction;
resolveTo: (value: unknown) => MockFunction;
rejectWith: (value: unknown) => MockFunction;
callFake: (fn: (...args: unknown[]) => unknown) => MockFunction;
callThrough: () => MockFunction;
};
calls: {
argsFor: (index: number) => unknown[];
mostRecent: () => {args: unknown[]};
};
};
const routeParams$ = new BehaviorSubject<Record<string, unknown>>({});
const queryParams$ = new BehaviorSubject<Record<string, unknown>>({});
const defaultFirebaseApp = getApps().length > 0 ? getApp() : initializeApp(environment.firebase);
const defaultTestingProviders: TestingProviderList = [
provideNoopAnimations(),
provideNativeDateAdapter(),
provideRouter([]),
provideFirebaseApp(() => defaultFirebaseApp),
provideAuth(() => getAuth(defaultFirebaseApp)),
provideFirestore(() => initializeFirestore(defaultFirebaseApp, {})),
provideStorage(() => getStorage(defaultFirebaseApp)),
{
provide: ActivatedRoute,
useValue: {
snapshot: {params: {}, queryParams: {}, data: {}},
params: routeParams$.asObservable(),
queryParams: queryParams$.asObservable(),
data: of({}),
fragment: of(null),
},
},
{provide: MAT_DIALOG_DATA, useValue: {}},
{provide: MatDialogRef, useValue: {close: () => void 0}},
{
provide: DbService,
useValue: {
col$: () => of([]),
doc$: () => of(null),
col: (): CollectionStub => ({
valueChanges: () => of([]),
add: () => Promise.resolve({id: 'test-id'}),
}),
doc: (): DocumentStub => ({
set: () => Promise.resolve(),
update: () => Promise.resolve(),
delete: () => Promise.resolve(),
collection: (): CollectionStub => ({
valueChanges: () => of([]),
add: () => Promise.resolve({id: 'test-id'}),
}),
}),
},
},
];
const originalConfigureTestingModule = TestBed.configureTestingModule.bind(TestBed);
const configureTestingModule: typeof TestBed.configureTestingModule = moduleDef => {
const extraProviders: TestingProviderList = moduleDef?.providers ?? [];
const mergedModuleDef: TestingModuleDefinition = moduleDef ? {...moduleDef} : {};
mergedModuleDef.providers = defaultTestingProviders.concat(extraProviders);
return originalConfigureTestingModule(mergedModuleDef);
};
TestBed.configureTestingModule = configureTestingModule;
function decorateMock<T extends ReturnType<typeof vi.fn>>(mock: T): T & MockFunction {
const decorated = mock as T & MockFunction;
Object.defineProperty(decorated, 'and', {
configurable: true,
get: () => ({
returnValue(value: unknown) {
decorated.mockReturnValue(value);
return decorated;
},
resolveTo(value: unknown) {
decorated.mockResolvedValue(value);
return decorated;
},
rejectWith(value: unknown) {
decorated.mockRejectedValue(value);
return decorated;
},
callFake(fn: (...args: unknown[]) => unknown) {
decorated.mockImplementation(fn);
return decorated;
},
callThrough() {
return decorated;
},
}),
});
Object.defineProperty(decorated, 'calls', {
configurable: true,
get: () => ({
argsFor(index: number) {
return decorated.mock.calls[index] ?? [];
},
mostRecent() {
const args = decorated.mock.lastCall ?? [];
return {args};
},
}),
});
return decorated;
}
function createSpy(name?: string): MockFunction {
const spy = decorateMock(vi.fn());
if (name) {
spy.mockName(name);
}
return spy;
}
function createSpyObj<T>(
baseName: string,
methodNames: string[] | Record<string, unknown>,
propertyValues?: Record<string, unknown>
): T {
const result: Record<string, unknown> = {};
const methods = Array.isArray(methodNames) ? methodNames : Object.keys(methodNames);
for (const methodName of methods) {
result[methodName] = createSpy(`${baseName}.${methodName}`);
}
if (!Array.isArray(methodNames)) {
for (const [key, value] of Object.entries(methodNames)) {
(result[key] as MockFunction).and.returnValue(value);
}
}
if (propertyValues) {
for (const [key, value] of Object.entries(propertyValues)) {
result[key] = value;
}
}
return result as T;
}
function spyOnCompat<T extends object, K extends keyof T>(object: T, methodName: K): MockFunction {
const spy = vi.spyOn(object as Record<PropertyKey, (...args: unknown[]) => unknown>, methodName as PropertyKey);
return decorateMock(spy as unknown as ReturnType<typeof vi.fn>);
}
function expectAsyncCompat<T>(value: Promise<T>) {
return {
async toBeResolvedTo(expected: T) {
await expect(value).resolves.toEqual(expected);
},
async toBeRejectedWithError(expected?: string | RegExp | Error) {
if (expected instanceof Error) {
await expect(value).rejects.toThrowError(expected.message);
} else if (expected !== undefined) {
await expect(value).rejects.toThrowError(expected);
} else {
await expect(value).rejects.toThrowError();
}
},
};
}
expect.extend({
toBeTrue(received: unknown) {
return {
pass: received === true,
message: () => `expected ${String(received)} to be true`,
};
},
toBeFalse(received: unknown) {
return {
pass: received === false,
message: () => `expected ${String(received)} to be false`,
};
},
});
Object.assign(globalThis, {
spyOn: spyOnCompat,
expectAsync: expectAsyncCompat,
jasmine: {
createSpy,
createSpyObj,
any: expect.any,
anything: expect.anything,
objectContaining: expect.objectContaining,
stringMatching: expect.stringMatching,
},
});

View File

@@ -1,90 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/testing';
import {getTestBed, TestBed} from '@angular/core/testing';
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing';
import {provideNoopAnimations} from '@angular/platform-browser/animations';
import {ActivatedRoute, provideRouter} from '@angular/router';
import {BehaviorSubject, of} from 'rxjs';
import {Observable} from 'rxjs';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {provideNativeDateAdapter} from '@angular/material/core';
import {provideFirebaseApp, initializeApp} from '@angular/fire/app';
import {getApp, getApps} from '@angular/fire/app';
import {getAuth, provideAuth} from '@angular/fire/auth';
import {initializeFirestore, provideFirestore} from '@angular/fire/firestore';
import {getStorage, provideStorage} from '@angular/fire/storage';
import {environment} from './environments/environment';
import {DbService} from './app/services/db.service';
type req = {keys: () => {map: (context: req) => void}};
type TestingModuleDefinition = Parameters<typeof TestBed.configureTestingModule>[0];
type TestingProviderList = NonNullable<NonNullable<TestingModuleDefinition>['providers']>;
type CollectionStub = {
valueChanges: () => Observable<unknown[]>;
add: () => Promise<{id: string}>;
};
type DocumentStub = {
set: () => Promise<void>;
update: () => Promise<void>;
delete: () => Promise<void>;
collection: () => CollectionStub;
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
const routeParams$ = new BehaviorSubject<Record<string, unknown>>({});
const queryParams$ = new BehaviorSubject<Record<string, unknown>>({});
const defaultTestingProviders: TestingProviderList = [
provideNoopAnimations(),
provideNativeDateAdapter(),
provideRouter([]),
provideFirebaseApp(() => (getApps().some(app => app.name === 'wgenerator-tests') ? getApp('wgenerator-tests') : initializeApp(environment.firebase, 'wgenerator-tests'))),
provideAuth(() => getAuth(getApp('wgenerator-tests'))),
provideFirestore(() => initializeFirestore(getApp('wgenerator-tests'), {})),
provideStorage(() => getStorage(getApp('wgenerator-tests'))),
{
provide: ActivatedRoute,
useValue: {
snapshot: {params: {}, queryParams: {}, data: {}},
params: routeParams$.asObservable(),
queryParams: queryParams$.asObservable(),
data: of({}),
fragment: of(null),
},
},
{provide: MAT_DIALOG_DATA, useValue: {}},
{provide: MatDialogRef, useValue: {close: () => void 0}},
{
provide: DbService,
useValue: {
col$: () => of([]),
doc$: () => of(null),
col: (): CollectionStub => ({
valueChanges: () => of([]),
add: () => Promise.resolve({id: 'test-id'}),
}),
doc: (): DocumentStub => ({
set: () => Promise.resolve(),
update: () => Promise.resolve(),
delete: () => Promise.resolve(),
collection: (): CollectionStub => ({
valueChanges: () => of([]),
add: () => Promise.resolve({id: 'test-id'}),
}),
}),
},
},
];
const originalConfigureTestingModule = TestBed.configureTestingModule.bind(TestBed);
const configureTestingModule: typeof TestBed.configureTestingModule = moduleDef => {
const extraProviders: TestingProviderList = moduleDef?.providers ?? [];
const mergedModuleDef: TestingModuleDefinition = moduleDef ? {...moduleDef} : {};
mergedModuleDef.providers = defaultTestingProviders.concat(extraProviders);
return originalConfigureTestingModule(mergedModuleDef);
};
TestBed.configureTestingModule = configureTestingModule;

Some files were not shown because too many files have changed in this diff Show More