fix linting
Some checks failed
Angular Build / build (push) Has been cancelled

This commit is contained in:
2026-03-20 21:02:02 +01:00
parent d484239429
commit 893a13a8f2
30 changed files with 1936 additions and 3359 deletions

View File

@@ -1,86 +1,33 @@
{ {
"root": true, "root": true,
"ignorePatterns": [ "ignorePatterns": ["projects/**/*"],
"projects/**/*"
],
"overrides": [ "overrides": [
{ {
"files": [ "files": ["*.ts"],
"*.ts" "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@angular-eslint/recommended", "plugin:@angular-eslint/template/process-inline-templates"],
],
"parserOptions": {
"project": [
"tsconfig.json"
],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates",
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:prettier/recommended"
],
"rules": { "rules": {
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
],
"@typescript-eslint/explicit-member-accessibility": "error",
"@angular-eslint/component-selector": [
"error",
{
"prefix": "app",
"style": "kebab-case",
"type": "element"
}
],
"@typescript-eslint/unbound-method": [
"off"
],
"@angular-eslint/directive-selector": [ "@angular-eslint/directive-selector": [
"error", "error",
{ {
"type": "attribute",
"prefix": "app", "prefix": "app",
"style": "camelCase", "style": "camelCase"
"type": "attribute"
} }
]
}
},
{
"files": [
"*.html"
], ],
"extends": [ "@angular-eslint/component-selector": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {
"prettier/prettier": [
"error", "error",
{ {
"endOfLine": "auto" "type": "element",
"prefix": "app",
"style": "kebab-case"
} }
] ]
} }
}, },
{ {
"files": [ "files": ["*.html"],
"*.spec.ts" "extends": ["plugin:@angular-eslint/template/recommended", "plugin:@angular-eslint/template/accessibility"],
], "rules": {}
"rules": {
"@typescript-eslint/await-thenable": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-return": "off"
}
} }
] ]
} }

View File

@@ -22,9 +22,7 @@
"base": "dist/wgenerator" "base": "dist/wgenerator"
}, },
"index": "src/index.html", "index": "src/index.html",
"polyfills": [ "polyfills": ["src/polyfills.ts"],
"src/polyfills.ts"
],
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "less", "inlineStyleLanguage": "less",
"assets": [ "assets": [
@@ -40,17 +38,9 @@
"src/assets", "src/assets",
"src/manifest.webmanifest" "src/manifest.webmanifest"
], ],
"styles": [ "styles": ["src/custom-theme.scss", "src/styles/styles.less", "src/styles/shadow.less"],
"src/custom-theme.scss",
"src/styles/styles.less",
"src/styles/shadow.less"
],
"scripts": [], "scripts": [],
"allowedCommonJsDependencies": [ "allowedCommonJsDependencies": ["lodash", "docx", "qrcode"]
"lodash",
"docx",
"qrcode"
]
}, },
"configurations": { "configurations": {
"production": { "production": {
@@ -99,11 +89,15 @@
"options": { "options": {
"runner": "vitest", "runner": "vitest",
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"setupFiles": [ "setupFiles": ["src/test-vitest.ts"],
"src/test-vitest.ts"
],
"runnerConfig": true "runnerConfig": true
} }
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
}
} }
} }
} }

4932
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -50,6 +50,7 @@
"@angular/language-service": "^21.2.2", "@angular/language-service": "^21.2.2",
"@typescript-eslint/eslint-plugin": "^8.57.0", "@typescript-eslint/eslint-plugin": "^8.57.0",
"@typescript-eslint/parser": "^8.57.0", "@typescript-eslint/parser": "^8.57.0",
"angular-eslint": "^21.3.1",
"eslint": "^9.39.4", "eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5", "eslint-plugin-prettier": "^5.5.5",

View File

@@ -1,5 +1,4 @@
@if (showState$ | async; as state) { @if (showState$ | async; as state) { @if (state.status === 'loaded') {
@if (state.status === 'loaded') {
<div class="page"> <div class="page">
<div class="title"> <div class="title">
<div class="left">{{ state.show.showType | showType }}</div> <div class="left">{{ state.show.showType | showType }}</div>
@@ -22,16 +21,9 @@
</div> </div>
</div> </div>
} @else if (state.status === 'loading') { } @else if (state.status === 'loading') {
<div class="empty-state"> <div class="empty-state">Gastansicht wird geladen.</div>
Gastansicht wird geladen.
</div>
} @else if (state.status === 'not-found') { } @else if (state.status === 'not-found') {
<div class="empty-state"> <div class="empty-state">Für diesen Link wurde keine Gastansicht gefunden.</div>
Für diesen Link wurde keine Gastansicht gefunden.
</div>
} @else { } @else {
<div class="empty-state"> <div class="empty-state">{{ state.message }}</div>
{{ state.message }} } }
</div>
}
}

View File

@@ -74,8 +74,8 @@ export class GuestComponent {
} }
if (typeof value === 'object' && value !== null) { if (typeof value === 'object' && value !== null) {
if ('toDate' in value && typeof value.toDate === 'function') { if (this.hasToDate(value)) {
return value.toDate() as Date; return value.toDate();
} }
if ('seconds' in value && typeof value.seconds === 'number') { if ('seconds' in value && typeof value.seconds === 'number') {
@@ -90,6 +90,10 @@ export class GuestComponent {
return null; return null;
} }
private hasToDate(value: object): value is FirestoreDateLike {
return 'toDate' in value && typeof value.toDate === 'function';
}
} }
interface GuestShowView extends Omit<GuestShow, 'date' | 'updatedAt'> { interface GuestShowView extends Omit<GuestShow, 'date' | 'updatedAt'> {
@@ -97,8 +101,8 @@ interface GuestShowView extends Omit<GuestShow, 'date' | 'updatedAt'> {
updatedAt: Date | null; updatedAt: Date | null;
} }
type GuestShowState = type GuestShowState = {status: 'loading'} | {status: 'not-found'} | {status: 'error'; message: string} | {status: 'loaded'; show: GuestShowView};
| {status: 'loading'}
| {status: 'not-found'} type FirestoreDateLike = {
| {status: 'error'; message: string} toDate: () => Date;
| {status: 'loaded'; show: GuestShowView}; };

View File

@@ -4,10 +4,26 @@
<div class="song"> <div class="song">
@if (show) { @if (show) {
<div class="song-parts"> <div class="song-parts">
<div (click)="onSectionClick('title', -1, show.id)" [class.active]="show.presentationSongId === 'title'" class="song-part"> <div
(click)="onSectionClick('title', -1, show.id)"
(keydown.enter)="onSectionClick('title', -1, show.id)"
(keydown.space)="onSectionClick('title', -1, show.id)"
[class.active]="show.presentationSongId === 'title'"
class="song-part"
role="button"
tabindex="0"
>
<div class="head">Veranstaltung</div> <div class="head">Veranstaltung</div>
</div> </div>
<div (click)="onSectionClick('empty', -1, show.id)" [class.active]="show.presentationSongId === 'empty'" class="song-part"> <div
(click)="onSectionClick('empty', -1, show.id)"
(keydown.enter)="onSectionClick('empty', -1, show.id)"
(keydown.space)="onSectionClick('empty', -1, show.id)"
[class.active]="show.presentationSongId === 'empty'"
class="song-part"
role="button"
tabindex="0"
>
<div class="head">Leer</div> <div class="head">Leer</div>
</div> </div>
</div> </div>
@@ -17,18 +33,31 @@
<div class="song"> <div class="song">
@if (show) { @if (show) {
<div [class.active]="show.presentationSongId === song.id" class="title song-part"> <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
(click)="onSectionClick(song.id, -1, show.id)"
(keydown.enter)="onSectionClick(song.id, -1, show.id)"
(keydown.space)="onSectionClick(song.id, -1, show.id)"
class="head"
role="button"
tabindex="0"
>
{{ song.title }}
</div>
</div> </div>
} @if (show) { } @if (show) {
<div class="song-parts"> <div class="song-parts">
@for (section of song.sections; track section.type + '-' + section.number + '-' + $index; let i = $index) { @for (section of song.sections; track section.type + '-' + section.number + '-' + $index; let i = $index) {
<div <div
(click)="onSectionClick(song.id, i, show.id)" (click)="onSectionClick(song.id, i, show.id)"
(keydown.enter)="onSectionClick(song.id, i, show.id)"
(keydown.space)="onSectionClick(song.id, i, show.id)"
[class.active]=" [class.active]="
show.presentationSongId === song.id && show.presentationSongId === song.id &&
show.presentationSection === i show.presentationSection === i
" "
class="song-part" class="song-part"
role="button"
tabindex="0"
> >
<div class="head">{{ section.type | sectionType }} {{ section.number + 1 }}</div> <div class="head">{{ section.type | sectionType }} {{ section.number + 1 }}</div>
<div class="fragment">{{ getFirstLine(section) }}</div> <div class="fragment">{{ getFirstLine(section) }}</div>
@@ -41,7 +70,16 @@
<div class="song"> <div class="song">
@if (show) { @if (show) {
<div [class.active]="show.presentationSongId === 'dynamicText'" class="title song-part"> <div [class.active]="show.presentationSongId === 'dynamicText'" class="title song-part">
<div (click)="onSectionClick('dynamicText', -1, show.id)" class="head">Freier Text</div> <div
(click)="onSectionClick('dynamicText', -1, show.id)"
(keydown.enter)="onSectionClick('dynamicText', -1, show.id)"
(keydown.space)="onSectionClick('dynamicText', -1, show.id)"
class="head"
role="button"
tabindex="0"
>
Freier Text
</div>
</div> </div>
} }
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">

View File

@@ -19,11 +19,7 @@ describe('SelectComponent', () => {
routerSpy = jasmine.createSpyObj<Router>('Router', ['navigateByUrl']); routerSpy = jasmine.createSpyObj<Router>('Router', ['navigateByUrl']);
showServiceSpy.list$.and.returnValue( showServiceSpy.list$.and.returnValue(
of([ of([createShow('older', '2025-12-15T00:00:00Z'), createShow('recent-a', '2026-03-01T00:00:00Z'), createShow('recent-b', '2026-02-20T00:00:00Z')]) as never
createShow('older', '2025-12-15T00:00:00Z'),
createShow('recent-a', '2026-03-01T00:00:00Z'),
createShow('recent-b', '2026-02-20T00:00:00Z'),
]) as never
); );
showServiceSpy.update$.and.resolveTo(); showServiceSpy.update$.and.resolveTo();
globalSettingsServiceSpy.set.and.resolveTo(); globalSettingsServiceSpy.set.and.resolveTo();

View File

@@ -5,6 +5,9 @@ import {MAT_DIALOG_DATA} from '@angular/material/dialog';
describe('ShareDialogComponent', () => { describe('ShareDialogComponent', () => {
let component: ShareDialogComponent; let component: ShareDialogComponent;
let fixture: ComponentFixture<ShareDialogComponent>; let fixture: ComponentFixture<ShareDialogComponent>;
type ShareDialogComponentInternals = ShareDialogComponent & {
generateQrCode: () => Promise<string>;
};
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
@@ -25,7 +28,7 @@ describe('ShareDialogComponent', () => {
fixture = TestBed.createComponent(ShareDialogComponent); fixture = TestBed.createComponent(ShareDialogComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
spyOn<any>(component, 'generateQrCode').and.resolveTo('data:image/jpeg;base64,test'); spyOn(component as ShareDialogComponentInternals, 'generateQrCode').and.resolveTo('data:image/jpeg;base64,test');
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -18,9 +18,7 @@
<app-button [fullWidth]="true" [icon]="faNewShow" routerLink="new">Neue Veranstaltung anlegen </app-button> <app-button [fullWidth]="true" [icon]="faNewShow" routerLink="new">Neue Veranstaltung anlegen </app-button>
</div> </div>
</app-card> </app-card>
} } @if (publicShows$ | async; as shows) { @if (shows.length > 0) {
@if (publicShows$ | async; as shows) { @if (shows.length > 0) {
<app-card [padding]="false" heading="Veröffentlichte Veranstaltungen"> <app-card [padding]="false" heading="Veröffentlichte Veranstaltungen">
@for (show of shows; track trackBy($index, show)) { @for (show of shows; track trackBy($index, show)) {
<app-list-item [routerLink]="show.id" [show]="show"></app-list-item> <app-list-item [routerLink]="show.id" [show]="show"></app-list-item>

View File

@@ -3,13 +3,25 @@ import {DocxService} from './docx.service';
describe('DocxService', () => { describe('DocxService', () => {
let service: DocxService; let service: DocxService;
type PreparedData = {
show: {
showType: string;
date: {toDate: () => Date};
};
songs: unknown[];
user: {name: string};
config: {ccliLicenseId: string};
};
type DocxModuleLike = {
Packer: {toBlob: (document: unknown) => Promise<Blob>};
};
type DocxServiceInternals = DocxService & { type DocxServiceInternals = DocxService & {
prepareData: (showId: string) => Promise<unknown>; prepareData: (showId: string) => Promise<PreparedData | null>;
renderTitle: (docx: unknown, title: string) => unknown[]; renderTitle: (docx: unknown, title: string) => unknown[];
renderSongs: (docx: unknown, songs: unknown[], options: unknown, config: unknown) => unknown[]; renderSongs: (docx: unknown, songs: unknown[], options: unknown, config: unknown) => unknown[];
prepareNewDocument: (docx: unknown, data: unknown, options?: unknown, sections?: unknown) => unknown; prepareNewDocument: (docx: unknown, data: unknown, options?: unknown, sections?: unknown) => unknown;
saveAs: (blob: Blob, name: string) => void; saveAs: (blob: Blob, name: string) => void;
loadDocx: () => Promise<{Packer: {toBlob: (document: unknown) => Promise<Blob>}}>; loadDocx: () => Promise<DocxModuleLike>;
}; };
beforeEach(async () => { beforeEach(async () => {
@@ -23,8 +35,8 @@ describe('DocxService', () => {
it('should not try to save a document when the required data cannot be prepared', async () => { it('should not try to save a document when the required data cannot be prepared', async () => {
const serviceInternals = service as DocxServiceInternals; const serviceInternals = service as DocxServiceInternals;
const prepareDataSpy = spyOn<any>(serviceInternals, 'prepareData').and.resolveTo(null); const prepareDataSpy = spyOn(serviceInternals, 'prepareData').and.resolveTo(null);
const saveAsSpy = spyOn<any>(serviceInternals, 'saveAs'); const saveAsSpy = spyOn(serviceInternals, 'saveAs');
await service.create('show-1'); await service.create('show-1');
@@ -34,13 +46,13 @@ describe('DocxService', () => {
it('should build and save a docx file when all data is available', async () => { it('should build and save a docx file when all data is available', async () => {
const blob = new Blob(['docx']); const blob = new Blob(['docx']);
const docxModule = { const docxModule: DocxModuleLike = {
Packer: { Packer: {
toBlob: jasmine.createSpy('toBlob').and.resolveTo(blob), toBlob: jasmine.createSpy('toBlob').and.resolveTo(blob),
}, },
}; };
const serviceInternals = service as DocxServiceInternals; const serviceInternals = service as DocxServiceInternals;
const prepareDataSpy = spyOn<any>(serviceInternals, 'prepareData').and.resolveTo({ const prepareDataSpy = spyOn(serviceInternals, 'prepareData').and.resolveTo({
show: { show: {
showType: 'service-worship', showType: 'service-worship',
date: {toDate: () => new Date('2026-03-10T00:00:00Z')}, date: {toDate: () => new Date('2026-03-10T00:00:00Z')},
@@ -49,11 +61,11 @@ describe('DocxService', () => {
user: {name: 'Benjamin'}, user: {name: 'Benjamin'},
config: {ccliLicenseId: '12345'}, config: {ccliLicenseId: '12345'},
}); });
spyOn<any>(serviceInternals, 'loadDocx').and.resolveTo(docxModule); spyOn(serviceInternals, 'loadDocx').and.resolveTo(docxModule);
spyOn<any>(serviceInternals, 'renderTitle').and.returnValue([]); spyOn(serviceInternals, 'renderTitle').and.returnValue([]);
spyOn<any>(serviceInternals, 'renderSongs').and.returnValue([]); spyOn(serviceInternals, 'renderSongs').and.returnValue([]);
const prepareNewDocumentSpy = spyOn<any>(serviceInternals, 'prepareNewDocument').and.returnValue({doc: true}); const prepareNewDocumentSpy = spyOn(serviceInternals, 'prepareNewDocument').and.returnValue({doc: true});
const saveAsSpy = spyOn<any>(serviceInternals, 'saveAs'); const saveAsSpy = spyOn(serviceInternals, 'saveAs');
await service.create('show-1', {copyright: true}); await service.create('show-1', {copyright: true});

View File

@@ -56,10 +56,7 @@ describe('ShowService', () => {
}); });
it('should not include archived shows from other users when requested', async () => { it('should not include archived shows from other users when requested', async () => {
shows$.next([ shows$.next([...(shows as unknown as unknown[]), {id: 'show-4', owner: 'other-user', published: true, archived: true}]);
...(shows as unknown as unknown[]),
{id: 'show-4', owner: 'other-user', published: true, archived: true},
]);
const result = await firstValueFrom(service.list$(false, true)); const result = await firstValueFrom(service.list$(false, true));
expect(result.map(show => show.id)).toEqual(['show-1', 'show-2', 'show-3']); expect(result.map(show => show.id)).toEqual(['show-1', 'show-2', 'show-3']);

View File

@@ -27,9 +27,7 @@ export class ShowService {
(user: User | null, shows: Show[]) => ({user, shows}) (user: User | null, shows: Show[]) => ({user, shows})
), ),
map(s => map(s =>
s.shows s.shows.filter(show => !show.archived || (includeOwnArchived && show.owner === s.user?.id)).filter(show => show.published || (show.owner === s.user?.id && !publishedOnly))
.filter(show => !show.archived || (includeOwnArchived && show.owner === s.user?.id))
.filter(show => show.published || (show.owner === s.user?.id && !publishedOnly))
) )
); );
} }

View File

@@ -13,7 +13,7 @@
<span class="title">{{ iSong.title }}</span> <span class="title">{{ iSong.title }}</span>
@if (!edit) { @if (!edit) {
<div class="keys-container"> <div class="keys-container">
<div (click)="openKeySelect()" class="keys"> <div (click)="openKeySelect()" (keydown.enter)="openKeySelect()" (keydown.space)="openKeySelect()" class="keys" role="button" tabindex="0">
@if (iSong.keyOriginal !== iSong.key) { @if (iSong.keyOriginal !== iSong.key) {
<span>{{ iSong.keyOriginal }}&nbsp;&nbsp;</span> <span>{{ iSong.keyOriginal }}&nbsp;&nbsp;</span>
} }

View File

@@ -30,7 +30,7 @@ describe('FileService', () => {
}); });
it('should resolve download urls via AngularFire storage helpers', async () => { it('should resolve download urls via AngularFire storage helpers', async () => {
const resolveSpy = spyOn<any>(service as FileServiceInternals, 'resolveDownloadUrl').and.resolveTo('https://cdn.example/file.pdf'); const resolveSpy = spyOn(service as FileServiceInternals, 'resolveDownloadUrl').and.resolveTo('https://cdn.example/file.pdf');
await expectAsync(service.getDownloadUrl('songs/song-1/file.pdf').toPromise()).toBeResolvedTo('https://cdn.example/file.pdf'); await expectAsync(service.getDownloadUrl('songs/song-1/file.pdf').toPromise()).toBeResolvedTo('https://cdn.example/file.pdf');
@@ -38,7 +38,7 @@ describe('FileService', () => {
}); });
it('should delete the file from storage and metadata from firestore', async () => { it('should delete the file from storage and metadata from firestore', async () => {
const deleteFromStorageSpy = spyOn<any>(service as FileServiceInternals, 'deleteFromStorage').and.resolveTo(); const deleteFromStorageSpy = spyOn(service as FileServiceInternals, 'deleteFromStorage').and.resolveTo();
await service.delete('songs/song-1/file.pdf', 'song-1', 'file-1'); await service.delete('songs/song-1/file.pdf', 'song-1', 'file-1');

View File

@@ -478,13 +478,15 @@ Text`;
const service: TextRenderingService = TestBed.inject(TextRenderingService); const service: TextRenderingService = TestBed.inject(TextRenderingService);
const text = 'Strophe\nC\tG\ta\nText'; const text = 'Strophe\nC\tG\ta\nText';
void expect(service.validateChordNotation(text)).toEqual(expect.arrayContaining([ void expect(service.validateChordNotation(text)).toEqual(
expect.arrayContaining([
jasmine.objectContaining({ jasmine.objectContaining({
lineNumber: 2, lineNumber: 2,
token: '\t', token: '\t',
reason: 'tab_character', reason: 'tab_character',
}), }),
])); ])
);
}); });
it('should not flag tabs on non chord lines', () => { it('should not flag tabs on non chord lines', () => {

View File

@@ -39,7 +39,7 @@ describe('UploadService', () => {
success(); success();
}, },
}; };
const uploadSpy = spyOn<any>(service as UploadServiceInternals, 'startUpload').and.returnValue(task); const uploadSpy = spyOn(service as UploadServiceInternals, 'startUpload').and.returnValue(task);
const upload = new Upload(new File(['content'], 'test.pdf', {type: 'application/pdf'})); const upload = new Upload(new File(['content'], 'test.pdf', {type: 'application/pdf'}));
service.pushUpload('song-1', upload); service.pushUpload('song-1', upload);

View File

@@ -6,7 +6,7 @@
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Titel</mat-label> <mat-label>Titel</mat-label>
<input autofocus formControlName="title" matInput /> <input formControlName="title" matInput />
</mat-form-field> </mat-form-field>
</div> </div>

View File

@@ -3,7 +3,7 @@ import {roles} from '../../../services/user/roles';
@Pipe({name: 'role'}) @Pipe({name: 'role'})
export class RolePipe implements PipeTransform { export class RolePipe implements PipeTransform {
public transform(role: roles | string): string { public transform(role: roles): string {
switch (role) { switch (role) {
case 'contributor': case 'contributor':
return 'Mitarbeiter'; return 'Mitarbeiter';

View File

@@ -17,7 +17,7 @@
</button> </button>
</div> </div>
} @if (!edit) { } @if (!edit) {
<div (click)="edit = true" class="users list-item"> <div (click)="edit = true" (keydown.enter)="edit = true" (keydown.space)="edit = true" class="users list-item" role="button" tabindex="0">
<span>{{ name }}</span> <span>{{ name }}</span>
<span <span
>@for (role of roles; track role) { >@for (role of roles; track role) {

View File

@@ -48,14 +48,14 @@ export function createSongSearchComparator(filterValue: string): (a: Song, b: So
} }
type SearchAnalysis = { type SearchAnalysis = {
compact: string, compact: string;
tokens: string[], tokens: string[];
}; };
type SearchableSong = { type SearchableSong = {
text?: SearchAnalysis, text?: SearchAnalysis;
title?: SearchAnalysis, title?: SearchAnalysis;
artist?: SearchAnalysis, artist?: SearchAnalysis;
}; };
const searchableSongCache = new WeakMap<Song, SearchableSong>(); const searchableSongCache = new WeakMap<Song, SearchableSong>();

View File

@@ -34,7 +34,9 @@ describe('UserSessionService', () => {
} as never); } as never);
routerSpy.navigateByUrl.and.resolveTo(true); routerSpy.navigateByUrl.and.resolveTo(true);
createAuthStateSpy = spyOn<any>(UserSessionService.prototype, 'createAuthState$').and.returnValue(authStateSubject.asObservable() as never); createAuthStateSpy = spyOn(UserSessionService.prototype as UserSessionService & {createAuthState$: () => unknown}, 'createAuthState$').and.returnValue(
authStateSubject.asObservable() as never
);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
providers: [ providers: [

View File

@@ -29,8 +29,8 @@ describe('UserSongUsageService', () => {
{id: 'show-2', owner: 'user-2', archived: true}, {id: 'show-2', owner: 'user-2', archived: true},
]) as never ]) as never
); );
showSongDataServiceSpy.list$.and.callFake((showId: string) => showSongDataServiceSpy.list$.and.callFake(
(of(showId === 'show-1' ? [{songId: 'song-1'}, {songId: 'song-1'}, {songId: 'song-2'}] : [{songId: 'song-3'}]) as never) (showId: string) => of(showId === 'show-1' ? [{songId: 'song-1'}, {songId: 'song-1'}, {songId: 'song-2'}] : [{songId: 'song-3'}]) as never
); );
dbServiceSpy.doc.and.returnValue({update: jasmine.createSpy('update').and.resolveTo()} as never); dbServiceSpy.doc.and.returnValue({update: jasmine.createSpy('update').and.resolveTo()} as never);

View File

@@ -29,9 +29,7 @@ export class FilterComponent {
this.filterStore.updateSongFilter({q: this.value}); this.filterStore.updateSongFilter({q: this.value});
}); });
this.valueChanged$ this.valueChanged$.pipe(debounceTime(100), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe(text => void this.applyValueChange(text));
.pipe(debounceTime(100), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef))
.subscribe(text => void this.applyValueChange(text));
} }
public valueChange(text: string): void { public valueChange(text: string): void {

View File

@@ -1,5 +1,13 @@
@if (sections && !fullscreen) { @if (sections && !fullscreen) {
<div (click)="onClick()" [class.chords]="iChordMode !== 'hide'" class="song-text"> <div
(click)="onClick()"
(keydown.enter)="onClick()"
(keydown.space)="onClick()"
[class.chords]="iChordMode !== 'hide'"
class="song-text"
role="button"
tabindex="0"
>
@if (showSwitch) { @if (showSwitch) {
<button (click)="onChordClick()" class="menu" mat-icon-button> <button (click)="onChordClick()" class="menu" mat-icon-button>
<fa-icon [icon]="faLines"></fa-icon> <fa-icon [icon]="faLines"></fa-icon>
@@ -23,7 +31,16 @@
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>
} @if (sections && fullscreen) { } @if (sections && fullscreen) {
<div (click)="onClick()" [@songSwitch]="sections" [class.chords]="iChordMode !== 'hide'" class="song-text"> <div
(click)="onClick()"
(keydown.enter)="onClick()"
(keydown.space)="onClick()"
[@songSwitch]="sections"
[class.chords]="iChordMode !== 'hide'"
class="song-text"
role="button"
tabindex="0"
>
@if (showSwitch) { @if (showSwitch) {
<button (click)="onChordClick()" class="menu" mat-icon-button> <button (click)="onChordClick()" class="menu" mat-icon-button>
<fa-icon [icon]="faLines"></fa-icon> <fa-icon [icon]="faLines"></fa-icon>

View File

@@ -31,9 +31,7 @@ describe('RoleGuard', () => {
}); });
it('should deny access when there is no current user', async () => { it('should deny access when there is no current user', async () => {
await expectAsync(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).toBeResolvedTo( await expectAsync(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).toBeResolvedTo({commands: ['brand', 'new-user']} as never);
{commands: ['brand', 'new-user']} as never
);
}); });
it('should allow admins regardless of requiredRoles', async () => { it('should allow admins regardless of requiredRoles', async () => {

View File

@@ -125,7 +125,8 @@ function decorateMock<T extends ReturnType<typeof vi.fn>>(mock: T): T & MockFunc
configurable: true, configurable: true,
get: () => ({ get: () => ({
argsFor(index: number) { argsFor(index: number) {
return decorated.mock.calls[index] ?? []; const calls = decorated.mock.calls as unknown[][];
return calls[index] ?? [];
}, },
mostRecent() { mostRecent() {
const args = decorated.mock.lastCall ?? []; const args = decorated.mock.lastCall ?? [];
@@ -146,11 +147,7 @@ function createSpy(name?: string): MockFunction {
return spy; return spy;
} }
function createSpyObj<T>( function createSpyObj<T>(baseName: string, methodNames: string[] | Record<string, unknown>, propertyValues?: Record<string, unknown>): T {
baseName: string,
methodNames: string[] | Record<string, unknown>,
propertyValues?: Record<string, unknown>
): T {
const result: Record<string, unknown> = {}; const result: Record<string, unknown> = {};
const methods = Array.isArray(methodNames) ? methodNames : Object.keys(methodNames); const methods = Array.isArray(methodNames) ? methodNames : Object.keys(methodNames);

View File

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

View File

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