build(lint): enforce Log helpers over console.* in src/app (#8239)

* build(lint): enforce Log helpers over console.* in src/app

Adds an ESLint no-console rule scoped to src/app (exempting log.ts,
*.spec.ts, and *.benchmark.ts) and migrates the remaining console.*
call sites to use Log / TaskLog / SyncLog / IssueLog / PluginLog so
their output is captured in the exportable log history users attach to
bug reports.

* test: spy on Log helpers instead of console after no-console migration

* fix(trello): keep username out of exportable log history

* test: derive today expectation from mocked DateService date
This commit is contained in:
Johannes Millan 2026-06-10 13:08:17 +02:00 committed by GitHub
parent 1999f2b413
commit cbb4e28ec4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 172 additions and 119 deletions

View file

@ -224,6 +224,21 @@ module.exports = tseslint.config(
'local-rules/no-multi-entity-effect': 'warn',
},
},
// App code must route logging through Log/SyncLog/OpLog/... helpers.
// Direct console.* calls bypass the exportable log history users attach
// to bug reports. The Log implementation itself, tests, and benchmarks
// (which intentionally dump timing numbers to stdout) are exempt.
{
files: ['src/app/**/*.ts'],
ignores: [
'src/app/**/*.spec.ts',
'src/app/**/*.benchmark.ts',
'src/app/core/log.ts',
],
rules: {
'no-console': 'error',
},
},
// HTML files
{
files: ['**/*.html'],

View file

@ -37,6 +37,7 @@ import { Project } from '../../../features/project/project.model';
import { isSingleEmoji } from '../../../util/extract-first-emoji';
import { expandCollapseAni } from '../../../ui/tree-dnd/tree.animations';
import { Router } from '@angular/router';
import { Log } from '../../../core/log';
const EXPAND_ANIMATION_RESET_DELAY_MS = 250;
@ -179,10 +180,7 @@ export class NavListTreeComponent implements OnDestroy {
node: TreeNode<MenuTreeViewNode>,
): void {
// TODO: Implement folder context menu
console.log(
'Folder context menu for:',
node.data?.k === MenuTreeKind.FOLDER ? node.data.name : 'unknown',
);
Log.log('Folder context menu for:', { id: node.id, kind: node.data?.k });
}
private _toggleFolder(treeNodeId: string): void {

View file

@ -46,6 +46,7 @@ import { firstValueFrom, Observable, of } from 'rxjs';
import { AsyncPipe } from '@angular/common';
import { DateService } from '../../core/date/date.service';
import { openWorkContextSettingsDialog } from '../../features/work-context/dialog-work-context-settings/open-work-context-settings-dialog';
import { Log } from '../../core/log';
@Component({
selector: 'work-context-menu',
@ -241,7 +242,7 @@ export class WorkContextMenuComponent implements OnInit {
try {
return await this._projectService.getCompletionInfo(this.contextId);
} catch (err) {
console.error(err);
Log.err(err);
this._snackService.open({ type: 'ERROR', msg: T.F.PROJECT.COMPLETE.ERROR });
return null;
}
@ -301,7 +302,7 @@ export class WorkContextMenuComponent implements OnInit {
msg: T.GLOBAL_SNACK.DUPLICATE_PROJECT_ERROR,
type: 'ERROR',
});
console.error(err);
Log.err(err);
}
}
@ -447,7 +448,7 @@ export class WorkContextMenuComponent implements OnInit {
msg: T.GLOBAL_SNACK.OPEN_SETTINGS_ERROR,
type: 'ERROR',
});
console.error(err);
Log.err(err);
}
}

View file

@ -5,6 +5,7 @@ import { T } from '../../t.const';
import { GlobalConfigService } from '../../features/config/global-config.service';
import { getDefaultClipboardImagesPath } from '../../util/get-default-clipboard-images-path';
import { MIME_TYPE_EXTENSIONS } from '../../../../electron/shared-with-frontend/mime-type-mapping.const';
import { Log } from '../log';
const DB_NAME = 'sp-clipboard-images';
const DB_VERSION = 1;
@ -117,7 +118,7 @@ export class ClipboardImageService {
return { success: true, imageUrl, markdownText };
}
} catch (error) {
console.error('[CLIPBOARD] Error getting file path:', error);
Log.err('[CLIPBOARD] Error getting file path:', error);
}
}
}
@ -164,8 +165,8 @@ export class ClipboardImageService {
return { success: true, imageUrl, markdownText };
} catch (error) {
console.error('[CLIPBOARD] Error saving clipboard image:', error);
console.error(
Log.err('[CLIPBOARD] Error saving clipboard image:', error);
Log.err(
'[CLIPBOARD] Error stack:',
error instanceof Error ? error.stack : 'no stack',
);
@ -234,7 +235,7 @@ export class ClipboardImageService {
return { success: true, imageUrl, markdownText };
} catch (error) {
console.error('Error getting image from file paths:', error);
Log.err('Error getting image from file paths:', error);
return null; // Fall back to regular clipboard handling
}
}
@ -316,7 +317,7 @@ export class ClipboardImageService {
this._blobUrlCache.set(imageId, blobUrl);
return blobUrl;
} catch (error) {
console.error('Error resolving indexeddb URL for clipboard image:', error);
Log.err('Error resolving indexeddb URL for clipboard image:', error);
return null;
}
}

View file

@ -9,6 +9,7 @@ import {
SimpleChanges,
} from '@angular/core';
import { ClipboardImageService } from './clipboard-image.service';
import { Log } from '../log';
/**
* Directive that resolves indexeddb:// URLs in img elements within the host element.
@ -98,7 +99,7 @@ export class ResolveClipboardImagesDirective implements OnInit, OnDestroy, OnCha
img.classList.add('indexeddb-image-error');
}
} catch (error) {
console.error('Error resolving clipboard image:', error);
Log.err('Error resolving clipboard image:', error);
img.alt = 'Error loading image';
img.classList.add('indexeddb-image-error');
}

View file

@ -2,6 +2,7 @@ import { Pipe, PipeTransform, inject } from '@angular/core';
import { ClipboardImageService } from './clipboard-image.service';
import { Observable, from, of, defer } from 'rxjs';
import { map, shareReplay, catchError } from 'rxjs/operators';
import { Log } from '../log';
@Pipe({
name: 'resolveClipboardUrl',
@ -38,7 +39,7 @@ export class ResolveClipboardUrlPipe implements PipeTransform {
return url;
}),
catchError((error) => {
console.error('Error resolving clipboard URL:', error);
Log.err('Error resolving clipboard URL:', error);
return of(url);
}),
shareReplay({ bufferSize: 1, refCount: false }),

View file

@ -88,7 +88,7 @@ export const logAdvancedStacktrace = (
// NOTE: there is an issue with this sometimes -> https://github.com/stacktracejs/stacktrace.js/issues/202
})
.catch(console.error);
.catch((err) => Log.err(err));
const _cleanHtml = (str: string): string => {
const div = document.createElement('div');

View file

@ -1,6 +1,7 @@
import { Directory, Filesystem } from '@capacitor/filesystem';
import { IS_NATIVE_PLATFORM } from '../../util/is-native-platform';
import { ShareResult } from './share.model';
import { Log } from '../log';
/**
* Sanitize filename for safe file system usage.
@ -43,7 +44,7 @@ export const downloadBlob = (blob: Blob, filename: string): boolean => {
anchor.click();
return true;
} catch (error) {
console.warn('Browser download failed:', error);
Log.warn('Browser download failed:', error);
return false;
} finally {
document.body.removeChild(anchor);
@ -65,7 +66,7 @@ export const cleanupCacheFile = async (relativePath: string): Promise<void> => {
directory: Directory.Cache,
});
} catch (cleanupError) {
console.warn('Failed to cleanup shared file:', cleanupError);
Log.warn('Failed to cleanup shared file:', cleanupError);
}
};
@ -120,7 +121,7 @@ export const openDownloadResult = async (result: ShareResult): Promise<void> =>
(window as any).ea.openPath(path);
return;
} catch (error) {
console.warn('Failed to open path via Electron bridge:', error);
Log.warn('Failed to open path via Electron bridge:', error);
}
}
@ -131,7 +132,7 @@ export const openDownloadResult = async (result: ShareResult): Promise<void> =>
window.open(candidate, '_blank', 'noopener');
return;
} catch (error) {
console.warn('Failed to open download in new window:', error);
Log.warn('Failed to open download in new window:', error);
}
}
};

View file

@ -17,6 +17,7 @@ import * as ShareTextUtil from './share-text.util';
import * as ShareUrlBuilder from './share-url-builder.util';
import * as ShareFileUtil from './share-file.util';
import * as SharePlatformUtil from './share-platform.util';
import { Log } from '../log';
export type ShareOutcome = 'shared' | 'cancelled' | 'unavailable' | 'failed';
export type { ShareSupport } from './share-platform.util';
@ -252,7 +253,7 @@ export class ShareService {
error: 'Share cancelled',
};
}
console.warn('Capacitor share failed:', error);
Log.warn('Capacitor share failed:', error);
}
}
@ -273,7 +274,7 @@ export class ShareService {
if (error instanceof Error && error.name === 'AbortError') {
return { success: false, error: 'Share cancelled' };
}
console.warn('Web Share API failed:', error);
Log.warn('Web Share API failed:', error);
}
}
@ -524,7 +525,7 @@ export class ShareService {
? `file://${resolvedUri}`
: `file:///${resolvedUri}`;
console.debug('[ShareService] shareCanvasViaNative', {
Log.debug('[ShareService] shareCanvasViaNative', {
resolvedUri,
fileUrl,
relativePath,
@ -535,9 +536,9 @@ export class ShareService {
path: relativePath,
directory: Directory.Cache,
});
console.debug('[ShareService] shareCanvasViaNative stat', stat);
Log.debug('[ShareService] shareCanvasViaNative stat', stat);
} catch (statError) {
console.warn('[ShareService] stat failed for shared image', statError);
Log.warn('[ShareService] stat failed for shared image', statError);
}
const canShare = (await sharePlugin.canShare?.())?.value ?? true;
@ -570,7 +571,7 @@ export class ShareService {
error: 'Share cancelled',
};
}
console.warn('Native image share failed:', error, {
Log.warn('Native image share failed:', error, {
fileUrl: fileUrl ?? 'n/a',
resolvedUri: resolvedUri ?? 'n/a',
relativePath,
@ -633,7 +634,7 @@ export class ShareService {
error: 'Share cancelled',
};
}
console.warn('Web Share API failed:', error);
Log.warn('Web Share API failed:', error);
}
return {
@ -683,7 +684,7 @@ export class ShareService {
uri,
};
} catch (error) {
console.warn('Native download failed, falling back to browser download:', error);
Log.warn('Native download failed, falling back to browser download:', error);
}
}
@ -703,7 +704,7 @@ export class ShareService {
target: 'download',
};
} catch (error) {
console.warn('Opening image in new tab failed:', error);
Log.warn('Opening image in new tab failed:', error);
}
}

View file

@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { getEnvOptional } from '../../util/env';
import { Log } from '../log';
export interface UnsplashPhoto {
id: string;
@ -56,7 +57,7 @@ export class UnsplashService {
}
if (!this.ACCESS_KEY) {
console.warn(
Log.warn(
'No Unsplash Access Key configured. Register at https://unsplash.com/developers?utm_source=super-productivity&utm_medium=referral&utm_campaign=api-credit',
);
return of({ results: [], total: 0, total_pages: 0 });
@ -81,7 +82,7 @@ export class UnsplashService {
})
.pipe(
catchError((error) => {
console.error('Unsplash API error:', error);
Log.err('Unsplash API error:', error);
return of({ results: [], total: 0, total_pages: 0 });
}),
);
@ -122,12 +123,12 @@ export class UnsplashService {
*/
trackPhotoDownload(photo: UnsplashPhoto): Observable<any> {
if (!photo.links?.download_location) {
console.warn('No download_location available for photo', photo.id);
Log.warn('No download_location available for photo', photo.id);
return of(null);
}
if (!this.ACCESS_KEY) {
console.warn('No Unsplash Access Key configured');
Log.warn('No Unsplash Access Key configured');
return of(null);
}
@ -138,7 +139,7 @@ export class UnsplashService {
// Call the download endpoint to track usage
return this._http.get(photo.links.download_location, { headers }).pipe(
catchError((error) => {
console.error('Failed to track photo download:', error);
Log.err('Failed to track photo download:', error);
// Don't fail the selection if tracking fails
return of(null);
}),

View file

@ -24,6 +24,7 @@ import { MatInput } from '@angular/material/input';
import { FormsModule } from '@angular/forms';
import { DialogClipboardImagesManagerComponent } from './dialog-clipboard-images-manager/dialog-clipboard-images-manager.component';
import { getDefaultClipboardImagesPath } from '../../../util/get-default-clipboard-images-path';
import { Log } from '../../../core/log';
@Component({
selector: 'clipboard-images-cfg',
@ -80,7 +81,7 @@ export class ClipboardImagesCfgComponent implements OnInit {
this.defaultImagePath = await getDefaultClipboardImagesPath();
this._cd.markForCheck();
} catch (error) {
console.error('Error loading default clipboard image path:', error);
Log.err('Error loading default clipboard image path:', error);
}
}

View file

@ -22,6 +22,7 @@ import { MatTooltip } from '@angular/material/tooltip';
import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatSelect } from '@angular/material/select';
import { MatOption } from '@angular/material/core';
import { Log } from '../../../../core/log';
type SortOrder = 'newest' | 'oldest' | 'largest' | 'smallest';
@ -100,7 +101,7 @@ export class DialogClipboardImagesManagerComponent implements OnInit {
}
this.imageUrls.set(urlMap);
} catch (error) {
console.error('Error loading clipboard images:', error);
Log.err('Error loading clipboard images:', error);
this._snackService.open({
type: 'ERROR',
msg: this._translateService.instant(T.GCF.CLIPBOARD_IMAGES.ERROR_LOADING),
@ -124,7 +125,7 @@ export class DialogClipboardImagesManagerComponent implements OnInit {
throw new Error('Delete operation returned false');
}
} catch (error) {
console.error('Error deleting clipboard image:', error);
Log.err('Error deleting clipboard image:', error);
this._snackService.open({
type: 'ERROR',
msg: this._translateService.instant(T.GCF.CLIPBOARD_IMAGES.ERROR_DELETING),
@ -159,7 +160,7 @@ export class DialogClipboardImagesManagerComponent implements OnInit {
});
}
} catch (error) {
console.error('Error deleting all clipboard images:', error);
Log.err('Error deleting all clipboard images:', error);
await this.loadImages();
this._snackService.open({
type: 'ERROR',

View file

@ -20,6 +20,7 @@ import { containsEmoji, extractFirstEmoji } from '../../../util/extract-first-em
import { isSingleEmoji } from '../../../util/extract-first-emoji';
import { startWith } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Log } from '../../../core/log';
@Component({
selector: 'icon-input',
@ -80,7 +81,7 @@ export class IconInputComponent extends FieldType<FormlyFieldConfig> implements
this.filteredIcons.set(icons.slice(0, 50));
}
} catch (error) {
console.error('Failed to load material icons:', error);
Log.err('Failed to load material icons:', error);
this.filteredIcons.set([]);
}
}
@ -123,7 +124,7 @@ export class IconInputComponent extends FieldType<FormlyFieldConfig> implements
this.isEmoji.set(false);
}
} catch (error) {
console.error('Failed to filter icons:', error);
Log.err('Failed to filter icons:', error);
this.filteredIcons.set([]);
}
}

View file

@ -29,6 +29,7 @@ import { AppStateActions } from '../../../root-store/app-state/app-state.actions
import { TaskSharedActions } from '../../../root-store/meta/task-shared.actions';
import { selectAllTasks } from '../../tasks/store/task.selectors';
import { normalizeStartOfNextDayConfig } from '../normalize-start-of-next-day-config';
import { Log } from '../../../core/log';
@Injectable()
export class GlobalConfigEffects {
@ -243,7 +244,7 @@ export class GlobalConfigEffects {
setTimeout(() => window.location.reload(), 1000);
})
.catch((err) => {
console.error('Failed to migrate user profiles:', err);
Log.err('Failed to migrate user profiles:', err);
this._snackService.open({
type: 'ERROR',
msg: 'Failed to enable user profiles. Please try again.',

View file

@ -10,6 +10,7 @@ import { CalendarContextInfoTarget } from '../../issue/providers/calendar/calend
import { selectEnabledIssueProviders } from '../../issue/store/issue-provider.selectors';
import { PluginIssueProviderRegistryService } from '../../../plugins/issue-provider/plugin-issue-provider-registry.service';
import { PluginService } from '../../../plugins/plugin.service';
import { IssueLog } from '../../../core/log';
@Component({
selector: 'issue-provider-setup-overview',
@ -69,7 +70,7 @@ export class IssueProviderSetupOverviewComponent {
this.pluginProviders = allProviders.filter((p) => !p.useAgendaView);
this.pluginCalendarProviders = allProviders.filter((p) => p.useAgendaView);
if (!isValidIssueProviderKey(issueProviderKey)) {
console.error(`Invalid issue provider key from plugin: "${issueProviderKey}"`);
IssueLog.err(`Invalid issue provider key from plugin: "${issueProviderKey}"`);
return;
}
this.openSetupDialog(issueProviderKey);

View file

@ -206,7 +206,7 @@ export class DialogEditIssueProviderComponent {
constructor() {
this._initOAuthAndOptions().catch((err) => {
console.error(
IssueLog.err(
'[DialogEditIssueProvider] OAuth init failed',
getSafeErrorLogMeta(err),
);
@ -453,7 +453,7 @@ export class DialogEditIssueProviderComponent {
}
} catch (e) {
anyFailed = true;
console.error('[DialogEditIssueProvider] loadOptions failed', {
IssueLog.err('[DialogEditIssueProvider] loadOptions failed', {
issueProviderKey: this.issueProviderKey,
hasFieldKey: !!field.key,
...getSafeErrorLogMeta(e),

View file

@ -18,6 +18,7 @@ import { ISSUE_PROVIDER_HUMANIZED, TRELLO_TYPE } from '../../issue.const';
import { T } from '../../../../t.const';
import { HANDLED_ERROR_PROP_STR } from '../../../../app.constants';
import { getErrorTxt } from '../../../../util/get-error-text';
import { IssueLog } from '../../../../core/log';
const BASE_URL = 'https://api.trello.com/1';
const DEFAULT_CARD_FIELDS = [
@ -195,7 +196,8 @@ export class TrelloApiService {
}),
catchError((error) => {
const errorMsg = `Trello failed to resolve username "${username}": ${getErrorTxt(error)}`;
console.error(errorMsg);
// errorMsg embeds the username (user content) — keep it out of the exportable log
IssueLog.err('Trello failed to resolve username:', getErrorTxt(error));
this._snackService.open({
type: 'ERROR',
msg: errorMsg,

View file

@ -27,6 +27,7 @@ import {
} from '../../../ui/heatmap/heatmap.component';
import { DateAdapter } from '@angular/material/core';
import { Worklog } from '../../worklog/worklog.model';
import { Log } from '../../../core/log';
interface YearlyActivityData {
dayMap: Map<string, DayData>;
@ -368,7 +369,7 @@ export class ActivityHeatmapComponent {
});
}
} else if (result.error && result.error !== 'Share cancelled') {
console.error('Share failed:', result.error);
Log.err('Share failed:', result.error);
this._snackService.open({
type: 'ERROR',
msg: 'Failed to share heatmap',
@ -377,7 +378,7 @@ export class ActivityHeatmapComponent {
} catch (error: any) {
const isAbort = error?.name === 'AbortError' || error?.error === 'Share cancelled';
if (!isAbort) {
console.error('Share failed:', error);
Log.err('Share failed:', error);
this._snackService.open({
type: 'ERROR',
msg: 'Failed to share heatmap',

View file

@ -31,6 +31,7 @@ import { ChartLazyLoaderService } from '../chart-lazy-loader.service';
import { ShareService } from '../../../core/share/share.service';
import { SnackService } from '../../../core/snack/snack.service';
import { T } from '../../../t.const';
import { Log } from '../../../core/log';
interface ChartClickEvent {
active: ActiveElement[];
@ -226,10 +227,10 @@ export class LazyChartComponent implements OnDestroy {
}
if (!result.success && result.error && result.error !== 'Share cancelled') {
console.error('Share failed:', result.error);
Log.err('Share failed:', result.error);
}
} catch (error) {
console.error('Share failed:', error);
Log.err('Share failed:', error);
} finally {
this.isSharing.set(false);
}
@ -247,14 +248,14 @@ export class LazyChartComponent implements OnDestroy {
await this.chartLoaderService.loadChartModule();
this.isModuleLoaded.set(true);
} catch (error) {
console.error('Failed to load chart module:', error);
Log.err('Failed to load chart module:', error);
}
}
private initChart(canvas: ElementRef<HTMLCanvasElement>): void {
const chartModule = this.chartLoaderService.getChartModule();
if (!chartModule) {
console.error('Chart module not loaded');
Log.err('Chart module not loaded');
return;
}
@ -269,7 +270,7 @@ export class LazyChartComponent implements OnDestroy {
this.chartInstance?.destroy();
this.chartInstance = new Chart(canvas.nativeElement, chartConfig);
} catch (error) {
console.error('Failed to initialize chart:', error);
Log.err('Failed to initialize chart:', error);
}
}

View file

@ -23,6 +23,7 @@ import { of, Subscription, timer } from 'rxjs';
import { SwipeDirective } from '../../ui/swipe-gesture/swipe.directive';
import { CssString, StyleObject, StyleObjectToString } from '../../util/styles';
import { LS } from '../../core/persistence/storage-keys.const';
import { Log } from '../../core/log';
import { readNumberLSBounded } from '../../util/ls-util';
import { MatIconModule } from '@angular/material/icon';
import { MatRippleModule } from '@angular/material/core';
@ -558,7 +559,7 @@ export class RightPanelComponent implements AfterViewInit, OnDestroy {
try {
localStorage.setItem(LS.RIGHT_PANEL_WIDTH, this.currentWidth().toString());
} catch (error) {
console.warn('Failed to save right panel width to localStorage:', error);
Log.warn('Failed to save right panel width to localStorage:', error);
}
}
@ -577,7 +578,7 @@ export class RightPanelComponent implements AfterViewInit, OnDestroy {
// Additional validation
if (!Number.isFinite(width) || width < RIGHT_PANEL_CONFIG.MIN_WIDTH) {
console.warn('Invalid right panel width detected, using default');
Log.warn('Invalid right panel width detected, using default');
this.currentWidth.set(RIGHT_PANEL_CONFIG.DEFAULT_WIDTH);
return;
}
@ -586,7 +587,7 @@ export class RightPanelComponent implements AfterViewInit, OnDestroy {
const validatedWidth = clampWidth(width, RIGHT_PANEL_CONFIG.MAX_WIDTH);
this.currentWidth.set(validatedWidth);
} catch (error) {
console.warn('Failed to initialize right panel width:', error);
Log.warn('Failed to initialize right panel width:', error);
this.currentWidth.set(RIGHT_PANEL_CONFIG.DEFAULT_WIDTH);
}
}

View file

@ -36,6 +36,7 @@ import { DateService } from 'src/app/core/date/date.service';
import { GlobalTrackingIntervalService } from '../../core/global-tracking-interval/global-tracking-interval.service';
import { ImexViewService } from '../../imex/imex-meta/imex-view.service';
import { BatchedTimeSyncAccumulator } from '../../core/util/batched-time-sync-accumulator';
import { Log } from '../../core/log';
@Injectable({
providedIn: 'root',
@ -223,7 +224,7 @@ export class SimpleCounterService implements OnDestroy {
}
})
.catch((error) => {
console.error('[SimpleCounterService] Error syncing stopwatch value:', error);
Log.err('[SimpleCounterService] Error syncing stopwatch value:', error);
});
}

View file

@ -17,6 +17,7 @@ import { MatTooltip } from '@angular/material/tooltip';
import { DateTimeFormatService } from '../../../core/date-time-format/date-time-format.service';
import { getNextRepeatOccurrence } from '../store/get-next-repeat-occurrence.util';
import { formatMonthDay } from '../../../util/format-month-day.util';
import { Log } from '../../../core/log';
@Component({
selector: 'repeat-cfg-preview',
@ -50,7 +51,7 @@ export class RepeatCfgPreviewComponent {
const nextLabel = this._translateService.instant(T.SCHEDULE.NEXT);
return `${nextLabel} ${formatted}`;
} catch (e) {
console.warn('Failed to compute next repeat occurrence', e);
Log.warn('Failed to compute next repeat occurrence', e);
return '';
}
});

View file

@ -31,6 +31,7 @@ import { ipcAddTaskFromAppUri$ } from '../../../core/ipc-events';
import { TaskService } from '../task.service';
import { TaskSharedActions } from '../../../root-store/meta/task-shared.actions';
import { LOCAL_ACTIONS } from '../../../util/local-actions.token';
import { TaskLog } from '../../../core/log';
// TODO send message to electron when current task changes here
@ -205,7 +206,10 @@ export class TaskElectronEffects {
tap((data) => {
// Double-check data validity as defensive programming
if (!data || !data.title || typeof data.title !== 'string') {
console.error('handleAddTaskFromProtocol$ received invalid data:', data);
TaskLog.err('handleAddTaskFromProtocol$ received invalid data', {
hasData: !!data,
titleType: typeof data?.title,
});
return;
}
this._taskService.add(data.title);

View file

@ -17,6 +17,7 @@ import {
import { androidInterface } from '../../android/android-interface';
import { generateNotificationId } from '../../android/android-notification-id.util';
import { PlannerActions } from '../../planner/store/planner.actions';
import { TaskLog } from '../../../core/log';
@Injectable()
export class TaskReminderEffects {
@ -97,7 +98,7 @@ export class TaskReminderEffects {
const notificationId = generateNotificationId(task.id);
androidInterface.cancelNativeReminder?.(notificationId);
} catch (e) {
console.error('Failed to cancel native reminder:', e);
TaskLog.err('Failed to cancel native reminder:', e);
}
}
@ -115,7 +116,7 @@ export class TaskReminderEffects {
const notificationId = generateNotificationId(task.id + '_deadline');
androidInterface.cancelNativeReminder?.(notificationId);
} catch (e) {
console.error('Failed to cancel native deadline reminder:', e);
TaskLog.err('Failed to cancel native deadline reminder:', e);
}
}
@ -218,7 +219,7 @@ export class TaskReminderEffects {
androidInterface.cancelNativeReminder?.(deadlineNotificationId);
}
} catch (e) {
console.error('Failed to cancel native reminder:', e);
TaskLog.err('Failed to cancel native reminder:', e);
}
}),
),
@ -243,7 +244,7 @@ export class TaskReminderEffects {
const notificationId = generateNotificationId(id);
androidInterface.cancelNativeReminder?.(notificationId);
} catch (e) {
console.error('Failed to cancel native reminder:', e);
TaskLog.err('Failed to cancel native reminder:', e);
}
});
}),
@ -267,7 +268,7 @@ export class TaskReminderEffects {
generateNotificationId(id + '_deadline'),
);
} catch (e) {
console.error('Failed to cancel native reminder:', e);
TaskLog.err('Failed to cancel native reminder:', e);
}
});
}),
@ -290,7 +291,7 @@ export class TaskReminderEffects {
generateNotificationId(id + '_deadline'),
);
} catch (e) {
console.error('Failed to cancel native reminder:', e);
TaskLog.err('Failed to cancel native reminder:', e);
}
});
}),
@ -313,7 +314,7 @@ export class TaskReminderEffects {
generateNotificationId(task.id + '_deadline'),
);
} catch (e) {
console.error('Failed to cancel native reminder:', e);
TaskLog.err('Failed to cancel native reminder:', e);
}
// Also cancel for subtasks
task.subTaskIds?.forEach((subId) => {
@ -323,7 +324,7 @@ export class TaskReminderEffects {
generateNotificationId(subId + '_deadline'),
);
} catch (e) {
console.error('Failed to cancel native reminder:', e);
TaskLog.err('Failed to cancel native reminder:', e);
}
});
});

View file

@ -7,6 +7,7 @@ import { TaskAttachment } from '../task-attachment.model';
import { T } from '../../../../t.const';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ClipboardImageService } from '../../../../core/clipboard-image/clipboard-image.service';
import { Log } from '../../../../core/log';
describe('TaskAttachmentListComponent', () => {
let component: TaskAttachmentListComponent;
@ -152,12 +153,12 @@ describe('TaskAttachmentListComponent', () => {
.and.returnValue(Promise.reject(new Error('Permission denied')));
setNavigatorClipboard({ writeText: writeTextSpy });
document.execCommand = jasmine.createSpy('execCommand').and.returnValue(true);
spyOn(console, 'warn');
spyOn(Log, 'warn');
await component.copy(attachment);
expect(writeTextSpy).toHaveBeenCalledWith('https://example.com/test');
expect(console.warn).toHaveBeenCalled();
expect(Log.warn).toHaveBeenCalled();
expect(document.execCommand).toHaveBeenCalledWith('copy');
expect(snackService.open).toHaveBeenCalledWith(T.GLOBAL_SNACK.COPY_TO_CLIPPBOARD);
});
@ -180,12 +181,12 @@ describe('TaskAttachmentListComponent', () => {
document.execCommand = jasmine
.createSpy('execCommand')
.and.throwError('Command not supported');
spyOn(console, 'error');
spyOn(Log, 'err');
await component.copy(attachment);
expect(document.execCommand).toHaveBeenCalledWith('copy');
expect(console.error).toHaveBeenCalled();
expect(Log.err).toHaveBeenCalled();
expect(snackService.open).toHaveBeenCalledWith({
msg: 'Failed to copy to clipboard. Please copy manually.',
type: 'ERROR',

View file

@ -20,6 +20,7 @@ import { EnlargeImgDirective } from '../../../../ui/enlarge-img/enlarge-img.dire
import { MatAnchor, MatButton } from '@angular/material/button';
import { ClipboardImageService } from '../../../../core/clipboard-image/clipboard-image.service';
import { isPathSafeToOpen } from '../../../../../../electron/shared-with-frontend/is-external-url-allowed';
import { Log } from '../../../../core/log';
interface ResolvedAttachment extends TaskAttachment {
resolvedPath?: string;
@ -141,7 +142,7 @@ export class TaskAttachmentListComponent {
});
}
} catch (error) {
console.error('Error resolving clipboard image:', error);
Log.err('Error resolving clipboard image:', error);
}
});
});
@ -192,7 +193,7 @@ export class TaskAttachmentListComponent {
this._copyWithFallback(attachment.path);
}
} catch (error) {
console.warn('Clipboard write failed, trying fallback method:', error);
Log.warn('Clipboard write failed, trying fallback method:', error);
// Try fallback method if modern API fails
this._copyWithFallback(attachment.path);
}
@ -220,7 +221,7 @@ export class TaskAttachmentListComponent {
});
}
} catch (error) {
console.error('Fallback copy failed:', error);
Log.err('Fallback copy failed:', error);
this._snackService.open({
msg: 'Failed to copy to clipboard. Please copy manually.',
type: 'ERROR',

View file

@ -30,6 +30,6 @@ export const playDoneSound = async (
source.detune.value = pitchFactor;
});
} catch (e) {
console.error('Error playing done sound:', e);
TaskLog.err('Error playing done sound:', e);
}
};

View file

@ -10,6 +10,7 @@ import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { MatDivider } from '@angular/material/divider';
import { TranslatePipe } from '@ngx-translate/core';
import { T } from '../../../t.const';
import { Log } from '../../../core/log';
@Component({
selector: 'user-profile-button',
@ -94,7 +95,7 @@ export class UserProfileButtonComponent {
try {
await this.profileService.switchProfile(profileId);
} catch (error) {
console.error('Failed to switch profile:', error);
Log.err('Failed to switch profile:', error);
} finally {
this.isLoading.set(false);
}

View file

@ -5,6 +5,7 @@ import { Store } from '@ngrx/store';
import { selectTasksWithSubTasksByIds } from '../tasks/store/task.selectors';
import { Task, TaskWithSubTasks } from '../tasks/task.model';
import { first } from 'rxjs/operators';
import { Log } from '../../core/log';
@Injectable({
providedIn: 'root',
@ -167,7 +168,7 @@ export class WorkContextMarkdownService {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.warn('Clipboard write failed, trying fallback method:', err);
Log.warn('Clipboard write failed, trying fallback method:', err);
}
}
@ -189,7 +190,7 @@ export class WorkContextMarkdownService {
try {
isSuccess = document.execCommand('copy');
} catch (err) {
console.error('Fallback copy failed:', err);
Log.err('Fallback copy failed:', err);
isSuccess = false;
} finally {
document.body.removeChild(textarea);

View file

@ -6,6 +6,7 @@ import { SnackService } from '../../core/snack/snack.service';
import { SyncProviderId } from '../../op-log/sync-providers/provider.const';
import { RestorePoint } from '../../op-log/sync-providers/provider.interface';
import { T } from '../../t.const';
import { SyncLog } from '../../core/log';
describe('SuperSyncRestoreService', () => {
let service: SuperSyncRestoreService;
@ -31,9 +32,9 @@ describe('SuperSyncRestoreService', () => {
mockSnackService = jasmine.createSpyObj('SnackService', ['open']);
// Spy on console methods for retry logging tests
spyOn(console, 'warn');
spyOn(console, 'error');
// Spy on logger methods for retry logging tests
spyOn(SyncLog, 'warn');
spyOn(SyncLog, 'err');
TestBed.configureTestingModule({
providers: [
@ -215,7 +216,7 @@ describe('SuperSyncRestoreService', () => {
await Promise.resolve();
await Promise.resolve();
expect(mockProvider.getStateAtSeq).toHaveBeenCalledTimes(1);
expect(console.warn).toHaveBeenCalledWith(
expect(SyncLog.warn).toHaveBeenCalledWith(
jasmine.stringContaining('Restore failed due to network error, retrying (1/2)'),
error,
);
@ -229,7 +230,7 @@ describe('SuperSyncRestoreService', () => {
await Promise.resolve();
await Promise.resolve();
expect(mockProvider.getStateAtSeq).toHaveBeenCalledTimes(2);
expect(console.warn).toHaveBeenCalledWith(
expect(SyncLog.warn).toHaveBeenCalledWith(
jasmine.stringContaining('Restore failed due to network error, retrying (2/2)'),
error,
);

View file

@ -10,6 +10,7 @@ import { AppDataComplete } from '../../op-log/model/model-config';
import { T } from '../../t.const';
import { SyncProviderManager } from '../../op-log/sync-providers/provider-manager.service';
import { BackupService } from '../../op-log/backup/backup.service';
import { SyncLog } from '../../core/log';
/**
* Service for restoring state from Super Sync server history.
@ -81,7 +82,7 @@ export class SuperSyncRestoreService {
if (isNetworkError && retryCount < MAX_RETRIES) {
retryCount++;
console.warn(
SyncLog.warn(
`Restore failed due to network error, retrying (${retryCount}/${MAX_RETRIES}):`,
error,
);
@ -90,7 +91,7 @@ export class SuperSyncRestoreService {
continue;
}
console.error('Failed to restore from point:', error);
SyncLog.err('Failed to restore from point:', error);
// The server can't replay end-to-end-encrypted ops to reconstruct an
// earlier state, so it rejects the restore with a reason mentioning
// encryption (surfaced in the thrown error message). Show an actionable

View file

@ -22,6 +22,7 @@ import { TagService } from '../../features/tag/tag.service';
import { sortTagLabels } from './plugin-tag-utils';
import { getDbDateStr } from '../../util/get-db-date-str';
import { T } from '../../t.const';
import { PluginLog } from '../../core/log';
@Injectable({ providedIn: 'root' })
export class PluginIssueProviderAdapterService implements IssueServiceInterface {
@ -55,7 +56,7 @@ export class PluginIssueProviderAdapterService implements IssueServiceInterface
try {
return await provider.definition.testConnection(pluginCfg.pluginConfig, http);
} catch (e) {
console.error(
PluginLog.err(
`[PluginIssueAdapter] testConnection failed for ${pluginCfg.issueProviderKey}:`,
e,
);
@ -78,7 +79,7 @@ export class PluginIssueProviderAdapterService implements IssueServiceInterface
return link;
}
} catch (e) {
console.error(
PluginLog.err(
`[PluginIssueAdapter] getIssueLink failed for ${cfg.issueProviderKey}:`,
e,
);
@ -90,7 +91,7 @@ export class PluginIssueProviderAdapterService implements IssueServiceInterface
const issue = (await this.getById(issueId, issueProviderId)) as PluginIssue | null;
return issue?.url ?? '';
} catch (e) {
console.error(
PluginLog.err(
`[PluginIssueAdapter] getIssueLink url fallback failed for ${cfg.issueProviderKey}:`,
e,
);
@ -114,7 +115,7 @@ export class PluginIssueProviderAdapterService implements IssueServiceInterface
resolved.http,
);
} catch (e) {
console.error(
PluginLog.err(
`[PluginIssueAdapter] getById failed for ${cfg.issueProviderKey}:`,
e,
);
@ -172,7 +173,7 @@ export class PluginIssueProviderAdapterService implements IssueServiceInterface
issueData: r,
})) as SearchResultItem[];
} catch (e) {
console.error(
PluginLog.err(
`[PluginIssueAdapter] searchIssues failed for ${cfg.issueProviderKey}:`,
e,
);
@ -258,7 +259,7 @@ export class PluginIssueProviderAdapterService implements IssueServiceInterface
this._handleRemoteDeletion(task);
return null;
}
console.error(
PluginLog.err(
`[PluginIssueAdapter] getFreshDataForIssueTask failed for ${cfg.issueProviderKey}:`,
e,
);
@ -299,7 +300,7 @@ export class PluginIssueProviderAdapterService implements IssueServiceInterface
const existingIds = new Set(allExistingIssueIds.map(String));
return results.filter((r) => !existingIds.has(r.id));
} catch (e) {
console.error(
PluginLog.err(
`[PluginIssueAdapter] getNewIssuesToAddToBacklog failed for ${cfg.issueProviderKey}:`,
e,
);

View file

@ -7,6 +7,7 @@ import {
PluginCommentsConfig,
PluginFieldMapping,
} from './plugin-issue-provider.model';
import { PluginLog } from '../../core/log';
const createMockDefinition = (
overrides: Partial<IssueProviderPluginDefinition> = {},
@ -73,7 +74,7 @@ describe('PluginIssueProviderRegistryService', () => {
it('should warn and reject duplicate registrations', () => {
const definition = createMockDefinition();
spyOn(console, 'warn');
spyOn(PluginLog, 'warn');
registerProvider(service, 'dup', definition, 'First', 'First', 'icon1', 1000, {
singular: 'A',
@ -84,7 +85,7 @@ describe('PluginIssueProviderRegistryService', () => {
plural: 'Bs',
});
expect(console.warn).toHaveBeenCalledWith(
expect(PluginLog.warn).toHaveBeenCalledWith(
jasmine.stringContaining('Duplicate registration'),
);
// The first registration should be preserved

View file

@ -7,6 +7,7 @@ import {
PluginFieldMapping,
} from './plugin-issue-provider.model';
import { IssueProviderKey } from '../../features/issue/issue.model';
import { PluginLog } from '../../core/log';
@Injectable({ providedIn: 'root' })
export class PluginIssueProviderRegistryService {
@ -33,7 +34,7 @@ export class PluginIssueProviderRegistryService {
}): void {
const key = opts.issueProviderKey ?? `plugin:${opts.pluginId}`;
if (this._providers.has(key)) {
console.warn(
PluginLog.warn(
`[PluginIssueProviderRegistry] Duplicate registration for '${key}', ignoring.`,
);
return;

View file

@ -566,7 +566,7 @@ export class PluginBridgeService implements OnDestroy {
* Internal method to show plugin index.html as view
*/
private _showIndexHtmlAsView(pluginId: string): void {
console.log('PluginBridge: Navigating to plugin index view', {
PluginLog.log('PluginBridge: Navigating to plugin index view', {
pluginId,
});
// Navigate to the plugin index route
@ -888,12 +888,12 @@ export class PluginBridgeService implements OnDestroy {
// Use the TaskService remove method which handles deletion properly
this._taskService.remove(taskWithSubTasks);
console.log('PluginBridge: Task deleted successfully', {
PluginLog.log('PluginBridge: Task deleted successfully', {
taskId,
hadSubTasks: taskWithSubTasks.subTasks.length > 0,
});
} catch (error) {
console.error('PluginBridge: Failed to delete task:', error);
PluginLog.err('PluginBridge: Failed to delete task:', error);
throw error;
}
}
@ -1169,7 +1169,7 @@ export class PluginBridgeService implements OnDestroy {
// below as a normal Error.
const entityId = composeId(pluginId, key);
this._pluginUserPersistenceService.persistPluginUserData(entityId, dataStr);
console.log('PluginBridge: Plugin data persisted successfully', {
PluginLog.log('PluginBridge: Plugin data persisted successfully', {
pluginId,
keyLen: key?.length ?? 0,
dataSize: new Blob([dataStr]).size,
@ -1225,7 +1225,7 @@ export class PluginBridgeService implements OnDestroy {
*/
private async _triggerSync(pluginId: string): Promise<void> {
try {
console.log('PluginBridge: Triggering sync for plugin', pluginId);
PluginLog.log('PluginBridge: Triggering sync for plugin', pluginId);
await this._syncWrapperService.sync();
PluginLog.log('PluginBridge: Sync completed successfully');
} catch (error) {
@ -1273,7 +1273,7 @@ export class PluginBridgeService implements OnDestroy {
this._syncAdapterRegistry.unregister(registeredKey);
}
console.log('PluginBridge: All hooks unregistered for plugin', { pluginId });
PluginLog.log('PluginBridge: All hooks unregistered for plugin', { pluginId });
}
/**
@ -1293,7 +1293,7 @@ export class PluginBridgeService implements OnDestroy {
const currentButtons = this._headerButtons();
this._headerButtons.set([...currentButtons, newButton]);
console.log('PluginBridge: Header button registered', {
PluginLog.log('PluginBridge: Header button registered', {
pluginId,
headerBtnCfg,
});
@ -1861,7 +1861,7 @@ export class PluginBridgeService implements OnDestroy {
try {
handler(isFocused);
} catch (error) {
console.error('Error in window focus handler:', error);
PluginLog.err('Error in window focus handler:', error);
}
});
}

View file

@ -503,7 +503,7 @@ export class PluginService implements OnDestroy {
}
}, 100);
}).catch((err) => {
console.error('Plugin activation error:', err);
PluginLog.err('Plugin activation error:', err);
});
const updatedState = this._getPluginState(pluginId);
@ -523,7 +523,7 @@ export class PluginService implements OnDestroy {
currentState.manifest,
);
if (!hasConsent) {
console.log(
PluginLog.log(
'Plugin requires Node.js execution permission but no stored consent found:',
state.manifest.id,
);
@ -533,7 +533,7 @@ export class PluginService implements OnDestroy {
}
} else {
// Plugin is not enabled, don't activate it
console.log(`Plugin ${pluginId} is not enabled, skipping activation`);
PluginLog.log(`Plugin ${pluginId} is not enabled, skipping activation`);
return null;
}
@ -1858,7 +1858,7 @@ export class PluginService implements OnDestroy {
// Only check platform availability in Electron environment
if (!this._isElectronRuntime()) {
console.warn(
PluginLog.warn(
`Plugin ${manifest.id} requires nodeExecution permission which is not available in web environment`,
);
return false;

View file

@ -298,7 +298,7 @@ export class PluginIndexComponent implements OnInit, OnDestroy {
// Cleanup blob URL if it exists
if (this._currentIframeUrl) {
cleanupPluginIframeUrl(this._currentIframeUrl);
console.log(`Cleaned up blob URL for plugin: ${currentPluginId}`);
PluginLog.log(`Cleaned up blob URL for plugin: ${currentPluginId}`);
this._currentIframeUrl = null;
}

View file

@ -13,6 +13,7 @@ import { getTag, updateTags, removeTaskFromPlannerDays } from './task-shared-hel
import { unique } from '../../../util/unique';
import { getDeadlineAutoPlanDecision } from '../../../features/tasks/util/get-deadline-auto-plan-fields';
import type { DeadlineAutoPlanContext } from '../../../features/tasks/util/get-deadline-auto-plan-fields';
import { TaskLog } from '../../../core/log';
// =============================================================================
// ACTION HANDLERS
@ -107,15 +108,15 @@ const handleSetDeadline = (
// Input validation
if (deadlineDay && !isDBDateStr(deadlineDay)) {
console.error('Invalid deadlineDay format:', deadlineDay);
TaskLog.err('Invalid deadlineDay format:', deadlineDay);
return state;
}
if (deadlineWithTime !== undefined && !Number.isFinite(deadlineWithTime)) {
console.error('Invalid deadlineWithTime:', deadlineWithTime);
TaskLog.err('Invalid deadlineWithTime:', deadlineWithTime);
return state;
}
if (deadlineRemindAt !== undefined && !Number.isFinite(deadlineRemindAt)) {
console.error('Invalid deadlineRemindAt:', deadlineRemindAt);
TaskLog.err('Invalid deadlineRemindAt:', deadlineRemindAt);
return state;
}

View file

@ -23,6 +23,7 @@ import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autoc
import { MatOption } from '@angular/material/core';
import { MaterialIconsLoaderService } from '../material-icons-loader.service';
import { InputColorPickerComponent } from '../input-color-picker/input-color-picker.component';
import { Log } from '../../core/log';
export interface CreateTagData {
title?: string;
@ -71,7 +72,7 @@ export class DialogCreateTagComponent {
const icons = await this._iconLoader.loadIcons();
this.filteredIcons.set(icons.slice(0, 50));
} catch (error) {
console.error('Failed to load material icons:', error);
Log.err('Failed to load material icons:', error);
this.filteredIcons.set([]);
}
}
@ -86,7 +87,7 @@ export class DialogCreateTagComponent {
filtered.length = Math.min(50, filtered.length);
this.filteredIcons.set(filtered);
} catch (error) {
console.error('Failed to filter icons:', error);
Log.err('Failed to filter icons:', error);
this.filteredIcons.set([]);
}
}

View file

@ -18,6 +18,7 @@ import { UnsplashService } from '../../core/unsplash/unsplash.service';
import { SnackService } from '../../core/snack/snack.service';
import { T } from '../../t.const';
import { IS_ELECTRON } from '../../app.constants';
import { Log } from '../../core/log';
const MAX_BACKGROUND_IMAGE_FILE_SIZE_BYTES = 256 * 1024;
@ -106,7 +107,7 @@ export class FormlyImageInputComponent extends FieldType<FormlyFieldConfig> {
openUnsplashPicker(): void {
if (!this.isUnsplashAvailable) {
console.warn('Unsplash service is not available - no API key configured');
Log.warn('Unsplash service is not available - no API key configured');
return;
}

View file

@ -368,7 +368,7 @@ export class MentionDirective implements OnChanges {
// Check if we have a valid active item before proceeding
if (!this.searchList.activeItem) {
console.warn('MentionDirective: No active item available for selection');
Log.warn('MentionDirective: No active item available for selection');
this.stopSearch();
return false;
}

View file

@ -1,6 +1,7 @@
import { inject, Pipe, PipeTransform } from '@angular/core';
import { DatePipe } from '@angular/common';
import { DateTimeFormatService } from '../../core/date-time-format/date-time-format.service';
import { Log } from '../../core/log';
/**
* Custom date pipe that respects the user's configured locale
@ -38,7 +39,7 @@ export class LocaleDatePipe implements PipeTransform {
try {
return this._datePipe.transform(value, format, timezone, effectiveLocale);
} catch (e) {
console.warn('LocaleDatePipe: failed to format value', value, e);
Log.warn('LocaleDatePipe: failed to format value', value, e);
return null;
}
}

View file

@ -38,6 +38,7 @@ import { TreeIndicatorService } from './tree-indicator.service';
import { TREE_CONSTANTS } from './tree-constants';
import { assertTreeId } from './tree-guards';
import { expandCollapseAni } from './tree.animations';
import { Log } from '../../core/log';
import { dragDelayForTouch } from '../../util/input-intent';
@Component({
@ -169,7 +170,7 @@ export class TreeDndComponent<TData = unknown> {
this._rootDropEl = this._host.nativeElement.querySelector('.root-drop');
this._indicatorService.clear();
} catch (error) {
console.error('Invalid drag start:', error);
Log.err('Invalid drag start:', error);
}
}
@ -411,7 +412,7 @@ export class TreeDndComponent<TData = unknown> {
item.reset();
} catch (error) {
// Ignore reset errors - they can happen if the item is already destroyed
console.debug('Failed to reset drag item:', error);
Log.debug('Failed to reset drag item:', error);
}
};

View file

@ -1,5 +1,6 @@
import { playSound } from './play-sound';
import { closeAudioContext } from './audio-context';
import { Log } from '../core/log';
describe('playSound', () => {
let mockAudioContext: any;
@ -108,12 +109,12 @@ describe('playSound', () => {
});
it('should handle errors gracefully', async () => {
const consoleErrorSpy = spyOn(console, 'error');
const logErrSpy = spyOn(Log, 'err');
fetchSpy.and.returnValue(Promise.reject(new Error('Test error')));
await playSound('nonexistent.mp3');
expect(consoleErrorSpy).toHaveBeenCalled();
expect(logErrSpy).toHaveBeenCalled();
});
it('should reuse the same AudioContext for multiple sounds', async () => {

View file

@ -1,4 +1,5 @@
import { getAudioBuffer, playBuffer } from './audio-context';
import { Log } from '../core/log';
const BASE = './assets/snd';
@ -13,6 +14,6 @@ export const playSound = async (filePath: string, vol = 100): Promise<void> => {
const buffer = await getAudioBuffer(`${BASE}/${filePath}`);
await playBuffer(buffer, vol);
} catch (e) {
console.error('Error playing sound:', e);
Log.err('Error playing sound:', e);
}
};