3 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
101 changed files with 3998 additions and 1208 deletions

View File

@@ -51,7 +51,7 @@
"docx",
"qrcode"
]
},
},
"configurations": {
"production": {
"fileReplacements": [
@@ -64,7 +64,7 @@
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
"maximumError": "10MB"
},
{
"type": "anyComponentStyle",
@@ -82,7 +82,7 @@
},
"defaultConfiguration": "production"
},
"serve": {
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
@@ -95,9 +95,20 @@
"defaultConfiguration": "development"
},
"test": {
"builder": "@angular/build:unit-test"
"builder": "@angular/build:unit-test",
"options": {
"runner": "vitest",
"tsConfig": "tsconfig.spec.json",
"setupFiles": [
"src/test-vitest.ts"
],
"runnerConfig": true
}
}
}
}
},
"cli": {
"analytics": false
}
}

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/compiler-cli": "^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/parser": "^8.57.0",
"eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"firebase-tools": "^15.9.1",
"jasmine-core": "~6.1.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",
"jsdom": "^29.0.0",
"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>
<div [@fader]="o.isActivated ? o.activatedRoute : ''" class="content">
<router-outlet #o="outlet"></router-outlet>
<div class="content">
<router-outlet></router-outlet>
</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 {AppComponent} from './app.component';
describe('AppComponent', () => {
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule, AppComponent],
}).compileComponents();
}));
});
it('should create the app', () => {
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';
@@ -6,11 +6,11 @@ describe('BrandComponent', () => {
let component: BrandComponent;
let fixture: ComponentFixture<BrandComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [BrandComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('NewUserComponent', () => {
let component: NewUserComponent;
let fixture: ComponentFixture<NewUserComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NewUserComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('GuestComponent', () => {
let component: GuestComponent;
let fixture: ComponentFixture<GuestComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [GuestComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('LegalComponent', () => {
let component: LegalComponent;
let fixture: ComponentFixture<LegalComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LegalComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('LogoComponent', () => {
let component: LogoComponent;
let fixture: ComponentFixture<LogoComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LogoComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('MonitorComponent', () => {
let component: MonitorComponent;
let fixture: ComponentFixture<MonitorComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MonitorComponent],
}).compileComponents();
}));
});
beforeEach(() => {
fixture = TestBed.createComponent(MonitorComponent);

View File

@@ -1,83 +1,81 @@
@if (show) {
<div @fade>
<app-card [closeIcon]="faIcon" [heading]="show.showType | showType" [subheading]="show.date.toDate() | date:'dd.MM.yyyy'" closeLink="/presentation/select">
@if (!progress) {
<div class="song">
@if (show) {
<div class="song-parts">
<div (click)="onSectionClick('title', -1, show.id)" [class.active]="show.presentationSongId === 'title'" class="song-part">
<div class="head">Veranstaltung</div>
</div>
<div (click)="onSectionClick('empty', -1, show.id)" [class.active]="show.presentationSongId === 'empty'" class="song-part">
<div class="head">Leer</div>
</div>
<app-card [closeIcon]="faIcon" [heading]="show.showType | showType" [subheading]="show.date.toDate() | date:'dd.MM.yyyy'" closeLink="/presentation/select">
@if (!progress) {
<div class="song">
@if (show) {
<div class="song-parts">
<div (click)="onSectionClick('title', -1, show.id)" [class.active]="show.presentationSongId === 'title'" class="song-part">
<div class="head">Veranstaltung</div>
</div>
<div (click)="onSectionClick('empty', -1, show.id)" [class.active]="show.presentationSongId === 'empty'" class="song-part">
<div class="head">Leer</div>
</div>
}
</div>
@for (song of presentationSongs; track trackBy($index, song)) {
<div class="song">
@if (show) {
<div [class.active]="show.presentationSongId === song.id" class="title song-part">
<div (click)="onSectionClick(song.id, -1, show.id)" class="head">{{ song.title }}</div>
</div>
} @if (show) {
<div class="song-parts">
@for (section of song.sections; track section.type + '-' + section.number + '-' + $index; let i = $index) {
<div
(click)="onSectionClick(song.id, i, show.id)"
[class.active]="
}
</div>
@for (song of presentationSongs; track trackBy($index, song)) {
<div class="song">
@if (show) {
<div [class.active]="show.presentationSongId === song.id" class="title song-part">
<div (click)="onSectionClick(song.id, -1, show.id)" class="head">{{ song.title }}</div>
</div>
} @if (show) {
<div class="song-parts">
@for (section of song.sections; track section.type + '-' + section.number + '-' + $index; let i = $index) {
<div
(click)="onSectionClick(song.id, i, show.id)"
[class.active]="
show.presentationSongId === song.id &&
show.presentationSection === i
"
class="song-part"
>
<div class="head">{{ section.type | sectionType }} {{ section.number + 1 }}</div>
<div class="fragment">{{ getFirstLine(section) }}</div>
</div>
}
class="song-part"
>
<div class="head">{{ section.type | sectionType }} {{ section.number + 1 }}</div>
<div class="fragment">{{ getFirstLine(section) }}</div>
</div>
}
</div>
}
<div class="song">
@if (show) {
<div [class.active]="show.presentationSongId === 'dynamicText'" class="title song-part">
<div (click)="onSectionClick('dynamicText', -1, show.id)" class="head">Freier Text</div>
</div>
}
<mat-form-field appearance="outline">
<mat-label>Überschrift</mat-label>
<input (ngModelChange)="onDynamicCaption($event, show.id)" [ngModel]="show.presentationDynamicCaption" autocomplete="off" id="dynamic-caption" matInput type="text" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Text</mat-label>
<textarea (ngModelChange)="onDynamicText($event, show.id)" [ngModel]="show.presentationDynamicText" autocomplete="off" id="dynamic-text" matInput></textarea>
</mat-form-field>
</div>
</div>
}
<div class="song">
@if (show) {
<div class="div-bottom">
<button class="btn-start-presentation" mat-button routerLink="/presentation/monitor">
<fa-icon [icon]="faDesktop"></fa-icon>
Präsentation starten
</button>
<mat-form-field appearance="outline">
<mat-label>Hintergrund</mat-label>
<mat-select (ngModelChange)="onBackground($event, show.id)" [ngModel]="show.presentationBackground">
<mat-option value="none">kein Hintergrund</mat-option>
<mat-option value="blue">Sternenhimmel</mat-option>
<mat-option value="green">Blätter</mat-option>
<mat-option value="leder">Leder</mat-option>
<mat-option value="praise">Lobpreis</mat-option>
<mat-option value="bible">Bibel</mat-option>
</mat-select>
</mat-form-field>
<mat-slider #slider [max]="100" [min]="10" [step]="2" class="zoom-slider" color="primary" ngDefaultControl
><input (ngModelChange)="onZoom($event, show.id)" [ngModel]="show.presentationZoom" matSliderThumb />
</mat-slider>
<div [class.active]="show.presentationSongId === 'dynamicText'" class="title song-part">
<div (click)="onSectionClick('dynamicText', -1, show.id)" class="head">Freier Text</div>
</div>
} @if (show) {
<app-add-song [addedLive]="true" [showSongs]="showSongs" [show]="show" [songs]="songs$|async"></app-add-song>
} }
</app-card>
</div>
}
<mat-form-field appearance="outline">
<mat-label>Überschrift</mat-label>
<input (ngModelChange)="onDynamicCaption($event, show.id)" [ngModel]="show.presentationDynamicCaption" autocomplete="off" id="dynamic-caption" matInput type="text" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Text</mat-label>
<textarea (ngModelChange)="onDynamicText($event, show.id)" [ngModel]="show.presentationDynamicText" autocomplete="off" id="dynamic-text" matInput></textarea>
</mat-form-field>
</div>
@if (show) {
<div class="div-bottom">
<button class="btn-start-presentation" mat-button routerLink="/presentation/monitor">
<fa-icon [icon]="faDesktop"></fa-icon>
Präsentation starten
</button>
<mat-form-field appearance="outline">
<mat-label>Hintergrund</mat-label>
<mat-select (ngModelChange)="onBackground($event, show.id)" [ngModel]="show.presentationBackground">
<mat-option value="none">kein Hintergrund</mat-option>
<mat-option value="blue">Sternenhimmel</mat-option>
<mat-option value="green">Blätter</mat-option>
<mat-option value="leder">Leder</mat-option>
<mat-option value="praise">Lobpreis</mat-option>
<mat-option value="bible">Bibel</mat-option>
</mat-select>
</mat-form-field>
<mat-slider #slider [max]="100" [min]="10" [step]="2" class="zoom-slider" color="primary" ngDefaultControl
><input (ngModelChange)="onZoom($event, show.id)" [ngModel]="show.presentationZoom" matSliderThumb />
</mat-slider>
</div>
} @if (show) {
<app-add-song [addedLive]="true" [showSongs]="showSongs" [show]="show" [songs]="songs$|async"></app-add-song>
} }
</app-card>
}

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';
@@ -6,11 +6,11 @@ describe('RemoteComponent', () => {
let component: RemoteComponent;
let fixture: ComponentFixture<RemoteComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RemoteComponent],
}).compileComponents();
}));
});
beforeEach(() => {
fixture = TestBed.createComponent(RemoteComponent);

View File

@@ -1,6 +1,6 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {Router} from '@angular/router';
import {of} from 'rxjs';
import {firstValueFrom, of} from 'rxjs';
import {GlobalSettingsService} from '../../../services/global-settings.service';
import {ShowService} from '../../shows/services/show.service';
import {SelectComponent} from './select.component';
@@ -52,12 +52,11 @@ describe('SelectComponent', () => {
expect(component.visible).toBeTrue();
});
it('should expose recent shows sorted descending by date', done => {
component.shows$.subscribe(shows => {
expect(showServiceSpy.list$).toHaveBeenCalledWith(true);
expect(shows.map(show => show.id)).toEqual(['recent-a', 'recent-b']);
done();
});
it('should expose recent shows sorted descending by date', async () => {
const shows = await firstValueFrom(component.shows$);
expect(showServiceSpy.list$).toHaveBeenCalledWith(true);
expect(shows.map(show => show.id)).toEqual(['recent-a', 'recent-b']);
});
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', () => {
let service: PresentationService;
beforeEach(() => {
void TestBed.configureTestingModule({});
beforeEach(async () => {
await TestBed.configureTestingModule({});
service = TestBed.inject(PresentationService);
});

View File

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

View File

@@ -4,7 +4,7 @@
<mat-label>Zeitraum</mat-label>
<mat-select formControlName="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-form-field>
@@ -14,7 +14,7 @@
<mat-select formControlName="owner">
<mat-option value="">Alle</mat-option>
@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-form-field>
@@ -25,17 +25,18 @@
<mat-option value="">Alle</mat-option>
<mat-optgroup label="öffentlich">
@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 label="privat">
@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-select>
</mat-form-field>
<mat-checkbox formControlName="archived">Archiviert</mat-checkbox>
</div>
<i>Anzahl der Suchergebnisse: {{ shows?.length ?? 0 }}</i>
<!-- <i>Anzahl der Suchergebnisse: {{ shows?.length ?? 0 }}</i>-->
</div>

View File

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

View File

@@ -1,31 +1,42 @@
<div>
<!-- <app-list-header *appRole="['leader']"></app-list-header>-->
<app-list-header *appRole="['leader']">
@if (shows$ | async; as shows) {
<app-filter [shows]="publicShows$ | async"></app-filter>
}
</app-list-header>
<ng-container *appRole="['leader']">
@if (privateShows$ | async; as shows) { @if (shows.length > 0) {
@if (showSidebar$ | async) {
<app-sidebar>
<div class="sidebar-content" sidebar>
<app-filter [shows]="(publicShows$ | async) ?? []"></app-filter>
</div>
<div content>
@if (privateShows$ | async; as privateShows) {
<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
[routerLink]="show.id"
[showStatusBadge]="show.published ? 'nicht gemeldet' : 'unveröffentlicht'"
[showStatusBadgeType]="show.published ? 'error' : 'none'"
[showStatusBadgeType]="show.archived ? 'warn' : show.published ? 'error' : 'none'"
[showStatusBadge]="show.archived ? 'archiviert' : show.published ? 'nicht gemeldet' : 'unveröffentlicht'"
[show]="show"
></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>
}
@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-card>
} }
</ng-container>
</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 | 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>
}

View File

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

View File

@@ -1,5 +1,6 @@
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 {ShowService} from '../services/show.service';
import {UserService} from '../../../services/user/user.service';
@@ -50,7 +51,7 @@ describe('ListComponent', () => {
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([
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')}}),
@@ -58,13 +59,11 @@ describe('ListComponent', () => {
createShow({id: 'draft-other', owner: 'user-2', published: false, reportedType: null, date: {toDate: () => new Date('2026-03-04')}}),
] as never);
component.privateShows$.subscribe(shows => {
expect(shows.map(show => show.id)).toEqual(['draft-own', 'pending-own']);
done();
});
const shows = await firstValueFrom(component.privateShows$);
expect(shows.map(show => show.id)).toEqual(['pending-own', 'draft-own']);
});
it('should ignore show filters for my shows', done => {
it('should ignore show filters for my shows', async () => {
const filterStore = TestBed.inject(FilterStoreService);
filterStore.updateShowFilter({time: 0, showType: 'service-worship'});
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')}}),
] as never);
component.privateShows$.subscribe(shows => {
expect(shows.map(show => show.id)).toEqual(['older-draft', 'pending-own']);
done();
});
const shows = await firstValueFrom(component.privateShows$);
expect(shows.map(show => show.id)).toEqual(['pending-own', 'older-draft']);
});
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 {map, switchMap} from 'rxjs/operators';
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 {FilterComponent} from './filter/filter.component';
import {CardComponent} from '../../../widget-modules/components/card/card.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 {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({
selector: 'app-list',
templateUrl: './list.component.html',
styleUrls: ['./list.component.less'],
animations: [fade],
imports: [RoleDirective, ListHeaderComponent, FilterComponent, CardComponent, ListItemComponent, RouterLink, AsyncPipe, SortByPipe],
imports: [FilterComponent, CardComponent, ListItemComponent, RouterLink, AsyncPipe, SidebarComponent, ButtonComponent, RoleDirective],
})
export class ListComponent {
public faNewShow = faPlus;
private showService = inject(ShowService);
private filterStore = inject(FilterStoreService);
private userService = inject(UserService);
@@ -32,9 +34,24 @@ export class ListComponent {
public lastMonths$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.time || 1));
public owner$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.owner));
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 privateShows$ = combineLatest([this.shows$, this.userService.user$]).pipe(
map(([shows, user]) => shows.filter(show => show.owner === user?.id).filter(show => !show.published || show.reportedType === 'pending'))
public ownShows$ = this.showService.list$(false, true);
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 fallbackPublicShows$ = combineLatest([this.shows$, this.lastMonths$]).pipe(
@@ -46,9 +63,10 @@ export class ListComponent {
map(([queriedShows, fallbackShows, owner, showType]) => {
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;
@@ -58,4 +76,17 @@ export class ListComponent {
startDate.setDate(startDate.getDate() - lastMonths * 30);
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';
@@ -6,11 +6,11 @@ describe('NewComponent', () => {
let component: NewComponent;
let fixture: ComponentFixture<NewComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NewComponent],
}).compileComponents();
}));
});
beforeEach(() => {
fixture = TestBed.createComponent(NewComponent);

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,15 @@
.third {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
column-gap: 20px;
.third,
:host ::ng-deep form,
div[formGroup] {
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';
@@ -6,11 +6,11 @@ describe('FilterComponent', () => {
let component: FilterComponent;
let fixture: ComponentFixture<FilterComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FilterComponent],
}).compileComponents();
}));
});
beforeEach(() => {
fixture = TestBed.createComponent(FilterComponent);

View File

@@ -1,43 +1,48 @@
@if (songs$ | async; as songs) {
<div>
<app-list-header [anyFilterActive]="anyFilterActive">
<app-sidebar>
<div sidebar class="sidebar-content">
<app-filter [songs]="songs"></app-filter>
</app-list-header>
<app-card [padding]="false">
@for (song of songs; track trackBy($index, song)) {
<div [routerLink]="song.id" class="list-item">
<div class="number">{{ song.number }}</div>
<div class="title">
<span>{{ song.title }}</span>
@if (song.hasChordValidationIssues) {
<span class="validation-star" title="Akkord-Validierungsfehler vorhanden">*</span>
}
</div>
<div>
<ng-container *appRole="['contributor']">
@if (song.status === 'draft') {
</div>
<div content>
<app-card [padding]="false">
@for (song of songs; track trackBy($index, song)) {
<div [routerLink]="song.id" class="list-item">
<div class="number">{{ song.number }}</div>
<div class="title">
<span>{{ song.title }}</span>
@if (song.hasChordValidationIssues) {
<span class="validation-star" title="Akkord-Validierungsfehler vorhanden">*</span>
}
</div>
<div>
<ng-container *appRole="['contributor']">
@if (song.status === 'draft') {
<div class="warning">
<fa-icon [icon]="faDraft"></fa-icon>
</div>
} @if (song.status === 'set') {
<div class="neutral">
<fa-icon [icon]="faDraft"></fa-icon>
</div>
} @if (song.status === 'final') {
<div class="success">
<fa-icon [icon]="faFinal"></fa-icon>
</div>
}
</ng-container>
@if (song.legalType === 'open') {
<div class="warning">
<fa-icon [icon]="faDraft"></fa-icon>
</div>
} @if (song.status === 'set') {
<div class="neutral">
<fa-icon [icon]="faDraft"></fa-icon>
</div>
} @if (song.status === 'final') {
<div class="success">
<fa-icon [icon]="faFinal"></fa-icon>
<fa-icon [icon]="faLegal"></fa-icon>
</div>
}
</ng-container>
@if (song.legalType === 'open') {
<div class="warning">
<fa-icon [icon]="faLegal"></fa-icon>
</div>
}
<div>{{ song.key }}</div>
</div>
<div>{{ song.key }}</div>
</div>
}
</app-card>
</div>
}
<div *appRole="['contributor']" class="list-action">
<app-button [fullWidth]="true" [icon]="faNewSong" routerLink="new">Neuen Song anlegen</app-button>
</div>
</app-card>
</div>
</app-sidebar>
}

View File

@@ -1,3 +1,7 @@
.sidebar-content {
padding: 20px;
}
.list-item {
padding: 5px 20px;
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 {of} from 'rxjs';
import {ActivatedRoute} from '@angular/router';
@@ -12,8 +12,8 @@ describe('SongListComponent', () => {
const songs = [{id: 'song-1', title: 'title1', number: 1, text: '', flags: ''}];
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SongListComponent],
providers: [
{provide: ActivatedRoute, useValue: {data: of({songs})}},
@@ -22,7 +22,7 @@ describe('SongListComponent', () => {
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
}));
});
beforeEach(() => {
fixture = TestBed.createComponent(SongListComponent);

View File

@@ -6,15 +6,16 @@ import {fade} from '../../../animations';
import {ActivatedRoute, RouterLink} from '@angular/router';
import {filterSong} from '../../../services/filter.helper';
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 {FilterStoreService} from '../../../services/filter-store.service';
import {AsyncPipe} from '@angular/common';
import {ListHeaderComponent} from '../../../widget-modules/components/list-header/list-header.component';
import {FilterComponent} from './filter/filter.component';
import {CardComponent} from '../../../widget-modules/components/card/card.component';
import {RoleDirective} from '../../../services/user/role.directive';
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 {
hasChordValidationIssues: boolean;
@@ -26,20 +27,21 @@ interface SongListItem extends Song {
styleUrls: ['./song-list.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [fade],
imports: [ListHeaderComponent, FilterComponent, CardComponent, RouterLink, RoleDirective, FaIconComponent, AsyncPipe],
imports: [FilterComponent, CardComponent, RouterLink, RoleDirective, FaIconComponent, AsyncPipe, SidebarComponent, ButtonComponent],
})
export class SongListComponent {
public faLegal = faBalanceScaleRight;
public faDraft = faPencilRuler;
public faFinal = faCheck;
public faNewSong = faPlus;
private route = inject(ActivatedRoute);
private textRenderingService = inject(TextRenderingService);
private filterStore = inject(FilterStoreService);
public anyFilterActive = false;
public songs$: Observable<SongListItem[]> = combineLatest([
this.filterStore.songFilter$,
this.route.data.pipe(map(data => (data['songs'] as Song[]).slice().sort((a, b) => a.number - b.number))),
]).pipe(
map(([filter, songs]) => {
this.anyFilterActive = this.checkIfFilterActive(filter);
return songs
.filter(song => this.filter(song, filter))
.map(song => ({
@@ -49,9 +51,6 @@ export class SongListComponent {
.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;
@@ -65,10 +64,6 @@ export class SongListComponent {
return baseFilter;
}
private checkIfFilterActive(filter: FilterValues): boolean {
return !!filter.q || !!filter.type || !!filter.key || !!filter.legalType || !!filter.flag;
}
private checkFlag(flag: string, flags: string) {
if (!flags) {
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';
@@ -6,11 +6,11 @@ describe('EditFileComponent', () => {
let component: EditFileComponent;
let fixture: ComponentFixture<EditFileComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [EditFileComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('FileComponent', () => {
let component: FileComponent;
let fixture: ComponentFixture<FileComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FileComponent],
}).compileComponents();
}));
});
beforeEach(() => {
fixture = TestBed.createComponent(FileComponent);

View File

@@ -4,8 +4,8 @@ import {EditSongGuard} from './edit-song.guard';
describe('EditSongGuard', () => {
let guard: EditSongGuard;
beforeEach(() => {
void TestBed.configureTestingModule({});
beforeEach(async () => {
await TestBed.configureTestingModule({});
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';
@@ -6,11 +6,11 @@ describe('EditSongComponent', () => {
let component: EditSongComponent;
let fixture: ComponentFixture<EditSongComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [EditSongComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('SaveDialogComponent', () => {
let component: SaveDialogComponent;
let fixture: ComponentFixture<SaveDialogComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SaveDialogComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('EditComponent', () => {
let component: EditComponent;
let fixture: ComponentFixture<EditComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [EditComponent],
}).compileComponents();
}));
});
beforeEach(() => {
fixture = TestBed.createComponent(EditComponent);

View File

@@ -4,8 +4,8 @@ import {EditService} from './edit.service';
describe('EditService', () => {
let service: EditService;
beforeEach(() => {
void TestBed.configureTestingModule({});
beforeEach(async () => {
await TestBed.configureTestingModule({});
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';
@@ -6,11 +6,11 @@ describe('HistoryComponent', () => {
let component: HistoryComponent;
let fixture: ComponentFixture<HistoryComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HistoryComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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 {FileComponent} from './file.component';
@@ -7,12 +7,12 @@ describe('FileComponent', () => {
let component: FileComponent;
let fixture: ComponentFixture<FileComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FileComponent],
providers: [{provide: Storage, useValue: {}}],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('NewComponent', () => {
let component: NewComponent;
let fixture: ComponentFixture<NewComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NewComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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 {of} from 'rxjs';
@@ -17,7 +17,7 @@ describe('SongComponent', () => {
params: of({songId: '4711'}),
};
beforeEach(waitForAsync(() => {
beforeEach(async () => {
const songServiceSpy = jasmine.createSpyObj<SongService>('SongService', ['read$']);
const fileDataServiceSpy = jasmine.createSpyObj<FileDataService>('FileDataService', ['read$']);
const userServiceSpy = jasmine.createSpyObj<UserService>('UserService', ['incSongCount', 'decSongCount'], {
@@ -33,7 +33,7 @@ describe('SongComponent', () => {
userServiceSpy.loggedIn$ = jasmine.createSpy('loggedIn$').and.returnValue(of(true));
userServiceSpy.getUserbyId$ = jasmine.createSpy('getUserbyId$').and.returnValue(of({name: 'Benjamin'}));
void TestBed.configureTestingModule({
await TestBed.configureTestingModule({
imports: [SongComponent],
providers: [
{provide: ActivatedRoute, useValue: mockActivatedRoute},
@@ -44,7 +44,7 @@ describe('SongComponent', () => {
{provide: ShowSongService, useValue: showSongServiceSpy},
],
}).compileComponents();
}));
});
beforeEach(() => {
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 {EditSongGuard} from './song/edit/edit-song.guard';
import {SongListResolver} from './services/song-list.resolver';
import {RoleGuard} from '../../widget-modules/guards/role.guard';
const routes: Routes = [
{
@@ -19,6 +20,10 @@ const routes: Routes = [
{
path: 'new',
component: NewComponent,
canActivate: [RoleGuard],
data: {
requiredRoles: ['contributor'],
},
},
{
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';
@@ -6,11 +6,11 @@ describe('InfoComponent', () => {
let component: InfoComponent;
let fixture: ComponentFixture<InfoComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [InfoComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('UserComponent', () => {
let component: UserComponent;
let fixture: ComponentFixture<UserComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('UsersComponent', () => {
let component: UsersComponent;
let fixture: ComponentFixture<UsersComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UsersComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LoginComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('LogoutComponent', () => {
let component: LogoutComponent;
let fixture: ComponentFixture<LogoutComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LogoutComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('NewComponent', () => {
let component: NewComponent;
let fixture: ComponentFixture<NewComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NewComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('PasswordSendComponent', () => {
let component: PasswordSendComponent;
let fixture: ComponentFixture<PasswordSendComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PasswordSendComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('PasswordComponent', () => {
let component: PasswordComponent;
let fixture: ComponentFixture<PasswordComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PasswordComponent],
}).compileComponents();
}));
});
beforeEach(() => {
fixture = TestBed.createComponent(PasswordComponent);

View File

@@ -1,5 +1,5 @@
import {TestBed} from '@angular/core/testing';
import {of} from 'rxjs';
import {firstValueFrom, of} from 'rxjs';
import {DbService} from './db.service';
import {ConfigService} from './config.service';
@@ -27,11 +27,8 @@ describe('ConfigService', () => {
expect(dbServiceSpy.doc$).toHaveBeenCalledWith('global/config');
});
it('should expose the shared config stream via get$', done => {
service.get$().subscribe(config => {
expect(config).toEqual({copyright: 'CCLI'} as never);
done();
});
it('should expose the shared config stream via get$', async () => {
await expectAsync(firstValueFrom(service.get$())).toBeResolvedTo({copyright: 'CCLI'} as never);
});
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';
describe('DbService', () => {
beforeEach(
() =>
void TestBed.configureTestingModule({
providers: [{provide: Firestore, useValue: {}}],
})
);
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [{provide: Firestore, useValue: {}}],
});
});
it('should be created', () => {
const service: DbService = TestBed.inject(DbService);

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import {TestBed} from '@angular/core/testing';
import {of} from 'rxjs';
import {firstValueFrom, of} from 'rxjs';
import {UserService} from './user.service';
import {UserSessionService} from './user-session.service';
import {UserSongUsageService} from './user-song-usage.service';
@@ -54,17 +54,10 @@ describe('UserService', () => {
expect(service).toBeTruthy();
});
it('should expose the session streams directly', done => {
service.userId$.subscribe(userId => {
expect(userId).toBe('user-1');
service.user$.subscribe(user => {
expect(user).toEqual({id: 'user-1'} as never);
service.users$.subscribe(users => {
expect(users).toEqual([{id: 'user-1'}] as never);
done();
});
});
});
it('should expose the session streams directly', async () => {
await expectAsync(firstValueFrom(service.userId$)).toBeResolvedTo('user-1');
await expectAsync(firstValueFrom(service.user$)).toBeResolvedTo({id: 'user-1'} as never);
await expectAsync(firstValueFrom(service.users$)).toBeResolvedTo([{id: 'user-1'}] as never);
});
it('should delegate session operations to UserSessionService', async () => {
@@ -85,20 +78,13 @@ describe('UserService', () => {
expect(sessionSpy.createNewUser).toHaveBeenCalledWith('mail', 'Benjamin', 'secret');
});
it('should delegate user lookup and loggedIn/list streams to UserSessionService', done => {
service.getUserbyId$('user-2').subscribe(user => {
expect(user).toEqual({id: 'user-2'} as never);
service.loggedIn$().subscribe(loggedIn => {
expect(loggedIn).toBeTrue();
service.list$().subscribe(users => {
expect(users).toEqual([{id: 'user-1'}] as never);
expect(sessionSpy.getUserbyId$).toHaveBeenCalledWith('user-2');
expect(sessionSpy.loggedIn$).toHaveBeenCalled();
expect(sessionSpy.list$).toHaveBeenCalled();
done();
});
});
});
it('should delegate user lookup and loggedIn/list streams to UserSessionService', async () => {
await expectAsync(firstValueFrom(service.getUserbyId$('user-2'))).toBeResolvedTo({id: 'user-2'} as never);
await expectAsync(firstValueFrom(service.loggedIn$())).toBeResolvedTo(true);
await expectAsync(firstValueFrom(service.list$())).toBeResolvedTo([{id: 'user-1'}] as never);
expect(sessionSpy.getUserbyId$).toHaveBeenCalledWith('user-2');
expect(sessionSpy.loggedIn$).toHaveBeenCalled();
expect(sessionSpy.list$).toHaveBeenCalled();
});
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';
@@ -6,11 +6,11 @@ describe('AddSongComponent', () => {
let component: AddSongComponent;
let fixture: ComponentFixture<AddSongComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AddSongComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('BrandComponent', () => {
let component: BrandComponent;
let fixture: ComponentFixture<BrandComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [BrandComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('FilterComponent', () => {
let component: FilterComponent;
let fixture: ComponentFixture<FilterComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FilterComponent],
}).compileComponents();
}));
});
beforeEach(() => {
fixture = TestBed.createComponent(FilterComponent);

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';
@@ -6,11 +6,11 @@ describe('LinkComponent', () => {
let component: LinkComponent;
let fixture: ComponentFixture<LinkComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LinkComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('NavigationComponent', () => {
let component: NavigationComponent;
let fixture: ComponentFixture<NavigationComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NavigationComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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';
@@ -6,11 +6,11 @@ describe('ButtonRowComponent', () => {
let component: ButtonRowComponent;
let fixture: ComponentFixture<ButtonRowComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ButtonRowComponent],
}).compileComponents();
}));
});
beforeEach(() => {
fixture = TestBed.createComponent(ButtonRowComponent);

View File

@@ -1,7 +1,21 @@
:host {
display: inline-flex;
}
:host(.full-width) {
display: flex;
width: 100%;
}
button {
color: var(--text);
transition: var(--transition);
:host(.full-width) & {
width: 100%;
justify-content: center;
}
&:hover {
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';
@@ -6,11 +6,11 @@ describe('ButtonComponent', () => {
let component: ButtonComponent;
let fixture: ComponentFixture<ButtonComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ButtonComponent],
}).compileComponents();
}));
});
beforeEach(() => {
fixture = TestBed.createComponent(ButtonComponent);

View File

@@ -9,8 +9,12 @@ import {FaIconComponent} from '@fortawesome/angular-fontawesome';
templateUrl: './button.component.html',
styleUrls: ['./button.component.less'],
imports: [MatButton, FaIconComponent],
host: {
'[class.full-width]': 'fullWidth',
},
})
export class ButtonComponent {
@Input() public disabled = false;
@Input() public fullWidth = false;
@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) {
<button [routerLink]="closeLink" class="btn-close" mat-icon-button>
<fa-icon [icon]="closeIcon"></fa-icon>

View File

@@ -70,9 +70,10 @@
}
.btn-close {
--icon-button-color: var(--text-soft);
--icon-button-hover-color: var(--text);
position: absolute;
right: 10px;
top: 15px;
opacity: 0.7;
color: var(--text-soft);
}

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';
@@ -6,11 +6,11 @@ describe('CardComponent', () => {
let component: CardComponent;
let fixture: ComponentFixture<CardComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CardComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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 {RouterLink} from '@angular/router';
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {fade} from '../../../animations';
@Component({
selector: 'app-card',
templateUrl: './card.component.html',
styleUrls: ['./card.component.less'],
imports: [MatIconButton, RouterLink, FaIconComponent],
animations: [fade],
})
export class CardComponent {
@Input() public padding = true;

View File

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

View File

@@ -10,11 +10,15 @@
display: flex;
align-items: center;
justify-content: flex-end;
color: var(--primary-hover);
}
.filter-active {
color: var(--danger);
.header .mat-mdc-icon-button {
--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;
}

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';
@@ -6,11 +6,11 @@ describe('ListHeaderComponent', () => {
let component: ListHeaderComponent;
let fixture: ComponentFixture<ListHeaderComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ListHeaderComponent],
}).compileComponents();
}));
});
beforeEach(() => {
fixture = TestBed.createComponent(ListHeaderComponent);

View File

@@ -20,6 +20,7 @@ export class ListHeaderComponent {
public filterVisible = false;
@Output() public filterVisibleChanged = new EventEmitter<boolean>();
@Input() public anyFilterActive = false;
@Input() public showFilterButton = true;
public onFilterClick(): void {
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';
@@ -6,11 +6,11 @@ describe('LogoComponent', () => {
let component: LogoComponent;
let fixture: ComponentFixture<LogoComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LogoComponent],
}).compileComponents();
}));
});
beforeEach(() => {
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;
padding: 0 var(--button-padding, 5px);
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';
@@ -6,11 +6,11 @@ describe('MenuButtonComponent', () => {
let component: MenuButtonComponent;
let fixture: ComponentFixture<MenuButtonComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MenuButtonComponent],
}).compileComponents();
}));
});
beforeEach(() => {
fixture = TestBed.createComponent(MenuButtonComponent);

View File

@@ -1 +1,12 @@
<aside></aside>
<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

@@ -1,5 +1,48 @@
: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: 200px;
width: var(--sidebar-width);
height: calc(100vh - 50px);
position: fixed;
@@ -8,4 +51,28 @@ aside {
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

@@ -1,9 +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: [],
imports: [MatIconButton, FaIconComponent],
templateUrl: './sidebar.component.html',
styleUrl: './sidebar.component.less',
host: {
'[class.collapsed]': 'collapsed',
},
})
export class SidebarComponent {}
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';
@@ -6,11 +6,11 @@ describe('SongTextComponent', () => {
let component: SongTextComponent;
let fixture: ComponentFixture<SongTextComponent>;
beforeEach(waitForAsync(() => {
void TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SongTextComponent],
}).compileComponents();
}));
});
beforeEach(() => {
fixture = TestBed.createComponent(SongTextComponent);

View File

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

View File

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

View File

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

View File

@@ -34,6 +34,10 @@
--focus-ring: 0 0 0 2px rgba(111, 143, 149, 0.28);
--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);
@@ -78,11 +82,11 @@ a {
}
.mat-mdc-icon-button {
color: var(--primary-color) !important;
color: var(--icon-button-color, var(--primary-color));
transition: var(--transition);
&:hover {
color: var(--primary-active) !important;
color: var(--icon-button-hover-color, var(--primary-active));
}
}
@@ -103,11 +107,11 @@ body .cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing {
}
.btn-icon {
opacity: 0.2;
opacity: var(--icon-button-opacity);
transition: var(--transition);
&:hover {
opacity: 1;
opacity: var(--icon-button-hover-opacity);
}
}
@@ -126,6 +130,5 @@ body {
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;

44
src/types/jasmine-compat.d.ts vendored Normal file
View File

@@ -0,0 +1,44 @@
type SpyAnd = {
returnValue(value?: unknown): jasmine.Spy;
resolveTo(value?: unknown): jasmine.Spy;
rejectWith(value?: unknown): jasmine.Spy;
callFake(fn: (...args: any[]) => unknown): jasmine.Spy;
callThrough(): jasmine.Spy;
};
type SpyCalls = {
argsFor(index: number): any[];
mostRecent(): {args: any[]};
};
declare global {
function spyOn<T = any>(object: T, methodName: any): jasmine.Spy;
function expectAsync<T>(value: Promise<T>): {
toBeResolvedTo(expected: T): Promise<void>;
toBeRejectedWithError(expected?: string | RegExp | Error): Promise<void>;
};
namespace jasmine {
type Spy<T extends (...args: any[]) => any = (...args: any[]) => any> = T & ReturnType<typeof import('vitest')['vi']['fn']> & {
and: SpyAnd;
calls: SpyCalls;
};
type SpyObj<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? Spy<T[K]> : T[K];
};
function createSpy(name?: string): Spy;
function createSpyObj<T>(
baseName: string,
methodNames: string[] | Record<string, unknown>,
propertyValues?: Record<string, unknown>
): SpyObj<T>;
function any(expectedClass: unknown): unknown;
function anything(): unknown;
function objectContaining<T>(value: Partial<T>): unknown;
function stringMatching(value: string | RegExp): unknown;
}
}
export {};

1
src/types/vitest-globals.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vitest/globals" />

13
src/types/vitest-matchers.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
import 'vitest';
declare module 'vitest' {
interface Assertion<T = any> {
toBeTrue(): T;
toBeFalse(): T;
}
interface AsymmetricMatchersContaining {
toBeTrue(): void;
toBeFalse(): void;
}
}

View File

@@ -3,15 +3,12 @@
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/test-vitest.ts",
"src/polyfills.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]

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