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,
"ignorePatterns": [
"projects/**/*"
],
"ignorePatterns": ["projects/**/*"],
"overrides": [
{
"files": [
"*.ts"
],
"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"
],
"files": ["*.ts"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@angular-eslint/recommended", "plugin:@angular-eslint/template/process-inline-templates"],
"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": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase",
"type": "attribute"
"style": "camelCase"
}
]
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {
"prettier/prettier": [
],
"@angular-eslint/component-selector": [
"error",
{
"endOfLine": "auto"
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
]
}
},
{
"files": [
"*.spec.ts"
],
"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"
}
"files": ["*.html"],
"extends": ["plugin:@angular-eslint/template/recommended", "plugin:@angular-eslint/template/accessibility"],
"rules": {}
}
]
}

View File

@@ -22,9 +22,7 @@
"base": "dist/wgenerator"
},
"index": "src/index.html",
"polyfills": [
"src/polyfills.ts"
],
"polyfills": ["src/polyfills.ts"],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "less",
"assets": [
@@ -40,17 +38,9 @@
"src/assets",
"src/manifest.webmanifest"
],
"styles": [
"src/custom-theme.scss",
"src/styles/styles.less",
"src/styles/shadow.less"
],
"styles": ["src/custom-theme.scss", "src/styles/styles.less", "src/styles/shadow.less"],
"scripts": [],
"allowedCommonJsDependencies": [
"lodash",
"docx",
"qrcode"
]
"allowedCommonJsDependencies": ["lodash", "docx", "qrcode"]
},
"configurations": {
"production": {
@@ -99,11 +89,15 @@
"options": {
"runner": "vitest",
"tsConfig": "tsconfig.spec.json",
"setupFiles": [
"src/test-vitest.ts"
],
"setupFiles": ["src/test-vitest.ts"],
"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",
"@typescript-eslint/eslint-plugin": "^8.57.0",
"@typescript-eslint/parser": "^8.57.0",
"angular-eslint": "^21.3.1",
"eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",

View File

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

View File

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

View File

@@ -4,10 +4,26 @@
<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
(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>
<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>
</div>
@@ -17,18 +33,31 @@
<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
(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>
} @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)"
(keydown.enter)="onSectionClick(song.id, i, show.id)"
(keydown.space)="onSectionClick(song.id, i, show.id)"
[class.active]="
show.presentationSongId === song.id &&
show.presentationSection === i
"
class="song-part"
role="button"
tabindex="0"
>
<div class="head">{{ section.type | sectionType }} {{ section.number + 1 }}</div>
<div class="fragment">{{ getFirstLine(section) }}</div>
@@ -41,7 +70,16 @@
<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
(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>
}
<mat-form-field appearance="outline">

View File

@@ -19,11 +19,7 @@ describe('SelectComponent', () => {
routerSpy = jasmine.createSpyObj<Router>('Router', ['navigateByUrl']);
showServiceSpy.list$.and.returnValue(
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
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
);
showServiceSpy.update$.and.resolveTo();
globalSettingsServiceSpy.set.and.resolveTo();

View File

@@ -5,6 +5,9 @@ import {MAT_DIALOG_DATA} from '@angular/material/dialog';
describe('ShareDialogComponent', () => {
let component: ShareDialogComponent;
let fixture: ComponentFixture<ShareDialogComponent>;
type ShareDialogComponentInternals = ShareDialogComponent & {
generateQrCode: () => Promise<string>;
};
beforeEach(async () => {
await TestBed.configureTestingModule({
@@ -25,7 +28,7 @@ describe('ShareDialogComponent', () => {
fixture = TestBed.createComponent(ShareDialogComponent);
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();
});

View File

@@ -18,9 +18,7 @@
<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) {
} @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>

View File

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

View File

@@ -27,9 +27,7 @@ export class ShowService {
(user: User | null, shows: Show[]) => ({user, shows})
),
map(s =>
s.shows
.filter(show => !show.archived || (includeOwnArchived && show.owner === s.user?.id))
.filter(show => show.published || (show.owner === s.user?.id && !publishedOnly))
s.shows.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>
@if (!edit) {
<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) {
<span>{{ iSong.keyOriginal }}&nbsp;&nbsp;</span>
}

View File

@@ -30,7 +30,7 @@ describe('FileService', () => {
});
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');
@@ -38,7 +38,7 @@ describe('FileService', () => {
});
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');

View File

@@ -478,13 +478,15 @@ Text`;
const service: TextRenderingService = TestBed.inject(TextRenderingService);
const text = 'Strophe\nC\tG\ta\nText';
void expect(service.validateChordNotation(text)).toEqual(expect.arrayContaining([
jasmine.objectContaining({
lineNumber: 2,
token: '\t',
reason: 'tab_character',
}),
]));
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

@@ -39,7 +39,7 @@ describe('UploadService', () => {
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'}));
service.pushUpload('song-1', upload);

View File

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

View File

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

View File

@@ -17,7 +17,7 @@
</button>
</div>
} @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
>@for (role of roles; track role) {

View File

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

View File

@@ -34,7 +34,9 @@ describe('UserSessionService', () => {
} as never);
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({
providers: [

View File

@@ -29,8 +29,8 @@ describe('UserSongUsageService', () => {
{id: 'show-2', owner: 'user-2', archived: true},
]) as never
);
showSongDataServiceSpy.list$.and.callFake((showId: string) =>
(of(showId === 'show-1' ? [{songId: 'song-1'}, {songId: 'song-1'}, {songId: 'song-2'}] : [{songId: 'song-3'}]) as never)
showSongDataServiceSpy.list$.and.callFake(
(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);

View File

@@ -56,7 +56,7 @@ export class AddSongComponent {
return 1;
}
return 0;
});
});
const filterValue = this.debouncedFilterValue;
return filterValue ? searchSongs(songs, filterValue) : songs;

View File

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

View File

@@ -1,5 +1,13 @@
@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) {
<button (click)="onChordClick()" class="menu" mat-icon-button>
<fa-icon [icon]="faLines"></fa-icon>
@@ -23,7 +31,16 @@
<ng-content></ng-content>
</div>
} @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) {
<button (click)="onChordClick()" class="menu" mat-icon-button>
<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 () => {
await expectAsync(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).toBeResolvedTo(
{commands: ['brand', 'new-user']} as never
);
await expectAsync(firstValueFrom(guard.canActivate({data: {requiredRoles: ['leader']}} as never))).toBeResolvedTo({commands: ['brand', 'new-user']} as never);
});
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,
get: () => ({
argsFor(index: number) {
return decorated.mock.calls[index] ?? [];
const calls = decorated.mock.calls as unknown[][];
return calls[index] ?? [];
},
mostRecent() {
const args = decorated.mock.lastCall ?? [];
@@ -146,11 +147,7 @@ function createSpy(name?: string): MockFunction {
return spy;
}
function createSpyObj<T>(
baseName: string,
methodNames: string[] | Record<string, unknown>,
propertyValues?: Record<string, unknown>
): T {
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);

View File

@@ -1,39 +1,38 @@
type UnknownFunction = (...args: unknown[]) => unknown;
type SpyAnd = {
returnValue(value?: unknown): jasmine.Spy;
resolveTo(value?: unknown): jasmine.Spy;
rejectWith(value?: unknown): jasmine.Spy;
callFake(fn: (...args: any[]) => unknown): jasmine.Spy;
callFake(fn: UnknownFunction): jasmine.Spy;
callThrough(): jasmine.Spy;
};
type SpyCalls = {
argsFor(index: number): any[];
mostRecent(): {args: any[]};
argsFor(index: number): unknown[];
mostRecent(): {args: unknown[]};
};
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>): {
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 Spy<T extends UnknownFunction = UnknownFunction> = 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];
[K in keyof T]: T[K] extends UnknownFunction ? 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 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;

View File

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