global filter

This commit is contained in:
2026-03-11 18:04:42 +01:00
parent c2bcac58b3
commit 196e8c80d8
25 changed files with 192 additions and 136 deletions

View File

@@ -12,7 +12,7 @@ describe('FileDataService', () => {
let fileDeleteSpy: jasmine.Spy;
let dbServiceSpy: jasmine.SpyObj<DbService>;
beforeEach(() => {
beforeEach(async () => {
filesCollectionValueChangesSpy = jasmine.createSpy('valueChanges').and.returnValue(of([{id: 'file-1', name: 'plan.pdf'}]));
filesCollectionAddSpy = jasmine.createSpy('add').and.resolveTo({id: 'file-2'});
songDocCollectionSpy = jasmine.createSpy('collection').and.returnValue({
@@ -30,7 +30,7 @@ describe('FileDataService', () => {
dbServiceSpy = jasmine.createSpyObj<DbService>('DbService', ['doc']);
dbServiceSpy.doc.and.callFake(songDocSpy);
void TestBed.configureTestingModule({
await TestBed.configureTestingModule({
providers: [{provide: DbService, useValue: dbServiceSpy}],
});

View File

@@ -6,12 +6,16 @@ import {FileService} from './file.service';
describe('FileService', () => {
let service: FileService;
let fileDataServiceSpy: jasmine.SpyObj<FileDataService>;
type FileServiceInternals = FileService & {
resolveDownloadUrl: (path: string) => Promise<string>;
deleteFromStorage: (path: string) => Promise<void>;
};
beforeEach(() => {
beforeEach(async () => {
fileDataServiceSpy = jasmine.createSpyObj<FileDataService>('FileDataService', ['delete']);
fileDataServiceSpy.delete.and.resolveTo();
void TestBed.configureTestingModule({
await TestBed.configureTestingModule({
providers: [
{provide: Storage, useValue: {app: 'test-storage'}},
{provide: FileDataService, useValue: fileDataServiceSpy},
@@ -26,7 +30,7 @@ describe('FileService', () => {
});
it('should resolve download urls via AngularFire storage helpers', async () => {
const resolveSpy = spyOn<any>(service, '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');
@@ -34,10 +38,9 @@ describe('FileService', () => {
});
it('should delete the file from storage and metadata from firestore', async () => {
const deleteFromStorageSpy = spyOn<any>(service, 'deleteFromStorage').and.resolveTo();
const deleteFromStorageSpy = spyOn(service as FileServiceInternals, 'deleteFromStorage').and.resolveTo();
service.delete('songs/song-1/file.pdf', 'song-1', 'file-1');
await Promise.resolve();
await service.delete('songs/song-1/file.pdf', 'song-1', 'file-1');
expect(deleteFromStorageSpy).toHaveBeenCalledWith('songs/song-1/file.pdf');
expect(fileDataServiceSpy.delete).toHaveBeenCalledWith('song-1', 'file-1');

View File

@@ -1,6 +1,6 @@
import {TestBed} from '@angular/core/testing';
import {firstValueFrom, Subject} from 'rxjs';
import {skip, take, toArray} from 'rxjs/operators';
import {take, toArray} from 'rxjs/operators';
import {DbService} from '../../../services/db.service';
import {SongDataService} from './song-data.service';
@@ -14,7 +14,7 @@ describe('SongDataService', () => {
let colSpy: jasmine.Spy;
let dbServiceSpy: jasmine.SpyObj<DbService>;
beforeEach(() => {
beforeEach(async () => {
songs$ = new Subject<Array<{id: string; title: string}>>();
docUpdateSpy = jasmine.createSpy('update').and.resolveTo();
docDeleteSpy = jasmine.createSpy('delete').and.resolveTo();
@@ -32,7 +32,7 @@ describe('SongDataService', () => {
dbServiceSpy.doc.and.callFake(docSpy);
dbServiceSpy.col.and.callFake(colSpy);
void TestBed.configureTestingModule({
await TestBed.configureTestingModule({
providers: [{provide: DbService, useValue: dbServiceSpy}],
});

View File

@@ -7,11 +7,11 @@ describe('SongListResolver', () => {
let resolver: SongListResolver;
let songServiceSpy: jasmine.SpyObj<SongService>;
beforeEach(() => {
beforeEach(async () => {
songServiceSpy = jasmine.createSpyObj<SongService>('SongService', ['list$']);
songServiceSpy.list$.and.returnValue(of([{id: 'song-1', title: 'Amazing Grace'}] as never));
void TestBed.configureTestingModule({
await TestBed.configureTestingModule({
providers: [{provide: SongService, useValue: songServiceSpy}],
});

View File

@@ -15,7 +15,7 @@ describe('SongService', () => {
edits: [],
} as never;
beforeEach(() => {
beforeEach(async () => {
songDataServiceSpy = jasmine.createSpyObj<SongDataService>('SongDataService', ['read$', 'update$', 'add', 'delete'], {
list$: of([song]),
});
@@ -27,7 +27,7 @@ describe('SongService', () => {
songDataServiceSpy.delete.and.resolveTo();
userServiceSpy.currentUser.and.resolveTo({name: 'Benjamin'} as never);
void TestBed.configureTestingModule({
await TestBed.configureTestingModule({
providers: [
{provide: SongDataService, useValue: songDataServiceSpy},
{provide: UserService, useValue: userServiceSpy},

View File

@@ -333,11 +333,18 @@ Text`;
void expect(sections[0].lines[0].text).toBe('Cmaj7(add9)');
void expect(sections[0].lines[0].chords).toEqual([
{chord: 'C', length: 11, position: 0, add: 'maj7(add9)', slashChord: null, addDescriptor: descriptor('maj7(add9)', {
quality: 'major',
extensions: ['7'],
modifiers: ['(add9)'],
})},
{
chord: 'C',
length: 11,
position: 0,
add: 'maj7(add9)',
slashChord: null,
addDescriptor: descriptor('maj7(add9)', {
quality: 'major',
extensions: ['7'],
modifiers: ['(add9)'],
}),
},
]);
void expect(service.validateChordNotation(text)).toEqual([]);
});
@@ -459,9 +466,7 @@ Text`;
void expect(sections[0].lines[0].type).toBe(LineType.chord);
void expect(sections[0].lines[0].text).toBe('C Es G');
void expect(service.validateChordNotation(text)).toEqual([
jasmine.objectContaining({lineNumber: 2, token: 'Es', reason: 'alias', suggestion: 'Eb'}),
]);
void expect(service.validateChordNotation(text)).toEqual([jasmine.objectContaining({lineNumber: 2, token: 'Es', reason: 'alias', suggestion: 'Eb'})]);
});
it('should flag unknown tokens on mostly chord lines', () => {
@@ -470,9 +475,7 @@ Text`;
C Foo G a
Text`;
void expect(service.validateChordNotation(text)).toEqual([
jasmine.objectContaining({lineNumber: 2, token: 'Foo', reason: 'unknown_token', suggestion: null}),
]);
void expect(service.validateChordNotation(text)).toEqual([jasmine.objectContaining({lineNumber: 2, token: 'Foo', reason: 'unknown_token', suggestion: null})]);
});
it('should reject tabs on chord lines', () => {

View File

@@ -7,7 +7,12 @@ import {LineType} from './line-type';
import {Chord, ChordAddDescriptor, ChordValidationIssue} from './chord';
import {Line} from './line';
const CHORD_ROOT_DEFINITIONS = [
type ChordRootDefinition = {
canonical: string;
aliases: string[];
};
const CHORD_ROOT_DEFINITIONS: readonly ChordRootDefinition[] = [
{canonical: 'C#', aliases: ['Cis']},
{canonical: 'Db', aliases: ['Des']},
{canonical: 'D#', aliases: ['Dis']},
@@ -45,9 +50,12 @@ const CHORD_ROOT_DEFINITIONS = [
] as const;
const CANONICAL_CHORD_ROOTS = CHORD_ROOT_DEFINITIONS.map(entry => entry.canonical);
const ALTERNATIVE_CHORD_ROOTS = Object.fromEntries(
CHORD_ROOT_DEFINITIONS.flatMap(entry => entry.aliases.map(alias => [alias, entry.canonical]))
) as Record<string, string>;
const ALTERNATIVE_CHORD_ROOTS = CHORD_ROOT_DEFINITIONS.reduce<Record<string, string>>((aliases, entry) => {
entry.aliases.forEach(alias => {
aliases[alias] = entry.canonical;
});
return aliases;
}, {});
interface ParsedValidationToken {
prefix: string;
@@ -66,6 +74,11 @@ interface ChordLineValidationResult {
isChordLike: boolean;
}
interface ParsedTokenCandidate {
token: string;
parsed: ParsedValidationToken | null;
}
@Injectable({
providedIn: 'root',
})
@@ -141,9 +154,7 @@ export class TextRenderingService {
private getLineOfLineText(text: string, transpose: TransposeMode | null, lineNumber?: number): Line | null {
if (!text) return null;
const validationResult = lineNumber
? this.getChordLineValidationResult(text, lineNumber)
: {chords: [], issues: [], isStrictChordLine: false, isChordLike: false};
const validationResult = lineNumber ? this.getChordLineValidationResult(text, lineNumber) : {chords: [], issues: [], isStrictChordLine: false, isChordLike: false};
const validationIssues = validationResult.issues;
const hasMatches = validationResult.isStrictChordLine;
const isChordLikeLine = hasMatches || validationResult.isChordLike;
@@ -194,13 +205,12 @@ export class TextRenderingService {
}
private getChordLineValidationResult(line: string, lineNumber: number): ChordLineValidationResult {
const tokens = line.match(/\S+/g) ?? [];
const tokens: string[] = line.match(/\S+/g) ?? [];
const chords = this.getParsedChords(line);
const parsedTokens = tokens
.map(token => ({
token,
parsed: this.parseValidationToken(token),
}));
const parsedTokens: ParsedTokenCandidate[] = tokens.map(token => ({
token,
parsed: this.parseValidationToken(token),
}));
const recognizedTokens = parsedTokens.filter((entry): entry is {token: string; parsed: ParsedValidationToken} => entry.parsed !== null);
@@ -227,12 +237,12 @@ export class TextRenderingService {
...issues,
...parsedTokens
.map(entry => {
if (!entry.parsed) {
return this.createUnknownTokenIssue(line, lineNumber, entry.token);
}
if (!entry.parsed) {
return this.createUnknownTokenIssue(line, lineNumber, entry.token);
}
return this.createValidationIssue(line, lineNumber, entry.token, entry.parsed);
})
return this.createValidationIssue(line, lineNumber, entry.token, entry.parsed);
})
.filter((issue): issue is ChordValidationIssue => issue !== null),
],
isStrictChordLine,
@@ -482,9 +492,7 @@ export class TextRenderingService {
suffix = this.stripLeadingDurMarker(normalizedSuffix);
}
const slashChord = parsed.slashChord
? this.toMajorRoot(parsed.slashChord)
: null;
const slashChord = parsed.slashChord ? this.toMajorRoot(parsed.slashChord) : null;
return parsed.prefix + root + suffix + (slashChord ? `/${slashChord}` : '') + parsed.tokenSuffix;
}
@@ -671,7 +679,7 @@ export class TextRenderingService {
};
while (rest.length > 0) {
const additionMatch = rest.match(/^add([#b+\-]?\d+)/);
const additionMatch = rest.match(/^add([#b+-]?\d+)/);
if (additionMatch) {
descriptor.additions.push(additionMatch[1]);
rest = rest.slice(additionMatch[0].length);
@@ -692,7 +700,7 @@ export class TextRenderingService {
continue;
}
const alterationMatch = rest.match(/^[#b+\-]\d+/);
const alterationMatch = rest.match(/^[#b+-]\d+/);
if (alterationMatch) {
descriptor.alterations.push(alterationMatch[0]);
rest = rest.slice(alterationMatch[0].length);

View File

@@ -13,7 +13,6 @@ describe('TransposeService', () => {
});
it('should create map upwards', () => {
const distance = service.getDistance('D', 'G');
const map = service.getMap('D', 'G');
if (map) {
@@ -22,7 +21,6 @@ describe('TransposeService', () => {
});
it('should create map downwards', () => {
const distance = service.getDistance('G', 'D');
const map = service.getMap('G', 'D');
if (map) {

View File

@@ -7,12 +7,18 @@ import {UploadService} from './upload.service';
describe('UploadService', () => {
let service: UploadService;
let fileDataServiceSpy: jasmine.SpyObj<FileDataService>;
type UploadTaskLike = {
on: (event: string, progress: (snapshot: {bytesTransferred: number; totalBytes: number}) => void, error: () => void, success: () => void) => void;
};
type UploadServiceInternals = UploadService & {
startUpload: (path: string, file: File) => UploadTaskLike;
};
beforeEach(() => {
beforeEach(async () => {
fileDataServiceSpy = jasmine.createSpyObj<FileDataService>('FileDataService', ['set']);
fileDataServiceSpy.set.and.resolveTo('file-1');
void TestBed.configureTestingModule({
await TestBed.configureTestingModule({
providers: [
{provide: Storage, useValue: {app: 'test-storage'}},
{provide: FileDataService, useValue: fileDataServiceSpy},
@@ -27,17 +33,16 @@ describe('UploadService', () => {
});
it('should upload the file, update progress and persist file metadata on success', async () => {
const task = {
const task: UploadTaskLike = {
on: (event: string, progress: (snapshot: {bytesTransferred: number; totalBytes: number}) => void, error: () => void, success: () => void) => {
progress({bytesTransferred: 50, totalBytes: 100});
success();
},
};
const uploadSpy = spyOn<any>(service, 'startUpload').and.returnValue(task as never);
const uploadSpy = spyOn(service as UploadServiceInternals, 'startUpload').and.returnValue(task);
const upload = new Upload(new File(['content'], 'test.pdf', {type: 'application/pdf'}));
service.pushUpload('song-1', upload);
await Promise.resolve();
await service.pushUpload('song-1', upload);
expect(uploadSpy).toHaveBeenCalledWith('/attachments/song-1/test.pdf', upload.file);
expect(upload.progress).toBe(50);