Skip to content

Commit 67535be

Browse files
refactor(frontend): complete tauriInvoke migration and extract shared helpers
Replace all direct window.__TAURI__.core.invoke calls outside api/ with tauriInvoke. Add tauriConfirm to api/shared.js and replace 5 inline confirm-dialog patterns across settings-view, context-menu-actions, column-settings, and playlist-crud. Extract formatDurationShorthand from stats-view into utils/formatting.js. Update metadata-modal and now-playing-context-menu tests to mock tauriInvoke via vi.mock instead of patching window.__TAURI__. Closes task-336.2
1 parent a020644 commit 67535be

14 files changed

Lines changed: 115 additions & 136 deletions

app/frontend/__tests__/metadata-modal.test.js

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
*/
99

1010
import { describe, it, expect, beforeEach, vi } from 'vitest';
11+
import { tauriInvoke } from '../js/api/shared.js';
12+
13+
vi.mock('../js/api/shared.js', () => ({
14+
tauriInvoke: vi.fn(),
15+
}));
1116

1217
// Build a minimal component context that mirrors the Alpine data shape
1318
function createModalContext(overrides = {}) {
@@ -52,13 +57,9 @@ function createModalContext(overrides = {}) {
5257
}
5358

5459
describe('metadata-modal batch loading', () => {
55-
let mockInvoke;
56-
5760
beforeEach(() => {
58-
mockInvoke = vi.fn();
59-
global.window = {
60-
__TAURI__: { core: { invoke: mockInvoke } },
61-
};
61+
vi.mocked(tauriInvoke).mockReset();
62+
global.window = { __TAURI__: {} };
6263
});
6364

6465
it('loadBatchMetadata makes a single batch IPC call', async () => {
@@ -68,7 +69,7 @@ describe('metadata-modal batch loading', () => {
6869
{ id: 3, path: '/music/c.mp3', duration: 220 },
6970
];
7071

71-
mockInvoke.mockResolvedValueOnce([
72+
vi.mocked(tauriInvoke).mockResolvedValueOnce([
7273
{ title: 'Song A', artist: 'Artist 1', album: 'Album', album_artist: null, track_number: 1, track_total: 10, disc_number: 1, disc_total: 1, year: 2020, genre: 'Rock' },
7374
{ title: 'Song B', artist: 'Artist 1', album: 'Album', album_artist: null, track_number: 2, track_total: 10, disc_number: 1, disc_total: 1, year: 2020, genre: 'Rock' },
7475
{ title: 'Song C', artist: 'Artist 1', album: 'Album', album_artist: null, track_number: 3, track_total: 10, disc_number: 1, disc_total: 1, year: 2020, genre: 'Rock' },
@@ -92,8 +93,8 @@ describe('metadata-modal batch loading', () => {
9293

9394
await component.loadBatchMetadata();
9495

95-
expect(mockInvoke).toHaveBeenCalledTimes(1);
96-
expect(mockInvoke).toHaveBeenCalledWith('get_tracks_metadata_batch', {
96+
expect(tauriInvoke).toHaveBeenCalledTimes(1);
97+
expect(tauriInvoke).toHaveBeenCalledWith('get_tracks_metadata_batch', {
9798
paths: ['/music/a.mp3', '/music/b.mp3', '/music/c.mp3'],
9899
});
99100

@@ -114,7 +115,7 @@ describe('metadata-modal batch loading', () => {
114115
{ id: 2, path: '/music/b.mp3', duration: 100 },
115116
];
116117

117-
mockInvoke.mockResolvedValueOnce([
118+
vi.mocked(tauriInvoke).mockResolvedValueOnce([
118119
{ title: 'Same', artist: 'Different A', album: 'Same Album', album_artist: null, track_number: 1, track_total: 2, disc_number: null, disc_total: null, year: 2024, genre: 'Pop' },
119120
{ title: 'Same', artist: 'Different B', album: 'Same Album', album_artist: null, track_number: 2, track_total: 2, disc_number: null, disc_total: null, year: 2024, genre: 'Pop' },
120121
]);
@@ -139,15 +140,12 @@ describe('metadata-modal batch loading', () => {
139140
});
140141

141142
describe('metadata-modal parallel save', () => {
142-
let mockInvoke;
143143
let mockRescan;
144144

145145
beforeEach(() => {
146-
mockInvoke = vi.fn().mockResolvedValue({});
146+
vi.mocked(tauriInvoke).mockReset().mockResolvedValue({});
147147
mockRescan = vi.fn().mockResolvedValue(undefined);
148-
global.window = {
149-
__TAURI__: { core: { invoke: mockInvoke } },
150-
};
148+
global.window = { __TAURI__: {} };
151149
});
152150

153151
it('saveCurrentEdits calls all saves in parallel via Promise.all', async () => {
@@ -178,13 +176,13 @@ describe('metadata-modal parallel save', () => {
178176

179177
expect(result).toBe(true);
180178
// All 3 save calls should have been made
181-
expect(mockInvoke).toHaveBeenCalledTimes(3);
179+
expect(tauriInvoke).toHaveBeenCalledTimes(3);
182180
// All 3 rescan calls should have been made
183181
expect(mockRescan).toHaveBeenCalledTimes(3);
184182

185183
// Verify save was called with correct update structures
186184
for (const track of tracks) {
187-
expect(mockInvoke).toHaveBeenCalledWith('save_track_metadata', {
185+
expect(tauriInvoke).toHaveBeenCalledWith('save_track_metadata', {
188186
update: { path: track.path, title: 'New Title' },
189187
});
190188
}
@@ -214,7 +212,7 @@ describe('metadata-modal parallel save', () => {
214212
const result = await component.saveCurrentEdits({ close: true, silent: true });
215213

216214
expect(result).toBe(true);
217-
expect(mockInvoke).not.toHaveBeenCalled();
215+
expect(tauriInvoke).not.toHaveBeenCalled();
218216
expect(mockRescan).not.toHaveBeenCalled();
219217
});
220218
});

app/frontend/__tests__/now-playing-context-menu.test.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@ vi.mock('../js/api/lyrics.js', () => ({
3737
},
3838
}));
3939

40+
vi.mock('../js/api/shared.js', () => ({
41+
tauriInvoke: vi.fn(),
42+
tauriConfirm: vi.fn(),
43+
}));
44+
4045
import { favorites } from '../js/api/favorites.js';
46+
import { tauriInvoke } from '../js/api/shared.js';
4147

4248
function createAlpineMock() {
4349
const stores = {};
@@ -407,8 +413,7 @@ describe('Now Playing Context Menu', () => {
407413

408414
describe('Show in Finder action', () => {
409415
it('calls Tauri invoke with the track filepath', async () => {
410-
const mockInvoke = vi.fn().mockResolvedValue(undefined);
411-
globalThis.window.__TAURI__ = { core: { invoke: mockInvoke } };
416+
vi.mocked(tauriInvoke).mockResolvedValueOnce(undefined);
412417

413418
const track = createMockTrack(2);
414419
component.handleContextMenu(createMockEvent(), track, 1);
@@ -418,11 +423,9 @@ describe('Now Playing Context Menu', () => {
418423
);
419424
await showInFinder.action();
420425

421-
expect(mockInvoke).toHaveBeenCalledWith('show_in_folder', {
426+
expect(tauriInvoke).toHaveBeenCalledWith('show_in_folder', {
422427
path: '/music/track2.mp3',
423428
});
424-
425-
delete globalThis.window.__TAURI__;
426429
});
427430
});
428431
});

app/frontend/js/api/shared.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,14 @@ export async function tauriInvoke(cmd, params = {}) {
7373
throw new ApiError(500, error.toString());
7474
}
7575
}
76+
77+
/**
78+
* Show a native Tauri confirmation dialog, falling back to window.confirm
79+
* @param {string} message - Confirmation message
80+
* @param {object} options - Dialog options (title, kind)
81+
* @returns {Promise<boolean>} Whether the user confirmed
82+
*/
83+
export async function tauriConfirm(message, options = {}) {
84+
return (await window.__TAURI__?.dialog?.confirm(message, options)) ??
85+
window.confirm(message);
86+
}

app/frontend/js/components/metadata-modal.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { tauriInvoke } from '../api/shared.js';
12
import { formatDuration } from '../utils/formatting.js';
23

34
export function createMetadataModal(Alpine) {
@@ -200,8 +201,7 @@ export function createMetadataModal(Alpine) {
200201
return;
201202
}
202203

203-
const { invoke } = window.__TAURI__.core;
204-
const data = await invoke('get_track_metadata', { path: trackPath });
204+
const data = await tauriInvoke('get_track_metadata', { path: trackPath });
205205

206206
this.metadata = {
207207
title: data.title || '',
@@ -262,8 +262,7 @@ export function createMetadataModal(Alpine) {
262262
genre: track.genre || '',
263263
}));
264264
} else {
265-
const { invoke } = window.__TAURI__.core;
266-
const results = await invoke('get_tracks_metadata_batch', { paths });
265+
const results = await tauriInvoke('get_tracks_metadata_batch', { paths });
267266
allMetadata = results.map((data) => ({
268267
title: data.title || '',
269268
artist: data.artist || '',
@@ -357,8 +356,6 @@ export function createMetadataModal(Alpine) {
357356
this.isSaving = true;
358357

359358
try {
360-
const { invoke } = window.__TAURI__.core;
361-
362359
const updates = [];
363360
for (const track of this.tracks) {
364361
const trackPath = this.getTrackPath(track);
@@ -390,7 +387,7 @@ export function createMetadataModal(Alpine) {
390387

391388
if (updates.length > 0) {
392389
await Promise.all(
393-
updates.map(({ update }) => invoke('save_track_metadata', { update })),
390+
updates.map(({ update }) => tauriInvoke('save_track_metadata', { update })),
394391
);
395392

396393
if (!silent) {

app/frontend/js/components/settings-view.js

Lines changed: 23 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { audio } from '../api/audio.js';
22
import { lastfm } from '../api/lastfm.js';
33
import { settings } from '../api/settings.js';
4+
import { tauriConfirm, tauriInvoke } from '../api/shared.js';
45
import { modLabel, SHORTCUT_DEFINITIONS } from '../shortcuts.js';
56

67
export function createSettingsView(Alpine) {
@@ -238,8 +239,7 @@ export function createSettingsView(Alpine) {
238239
}
239240

240241
try {
241-
const { invoke } = window.__TAURI__.core;
242-
const info = await invoke('app_get_info');
242+
const info = await tauriInvoke('app_get_info');
243243
this.appInfo = {
244244
version: info.version || '—',
245245
build: info.build || '—',
@@ -292,8 +292,7 @@ export function createSettingsView(Alpine) {
292292

293293
this.watchedFoldersLoading = true;
294294
try {
295-
const { invoke } = window.__TAURI__.core;
296-
this.watchedFolders = await invoke('watched_folders_list');
295+
this.watchedFolders = await tauriInvoke('watched_folders_list');
297296
} catch (error) {
298297
console.error('[settings] Failed to load watched folders:', error);
299298
Alpine.store('ui').toast('Failed to load watched folders', 'error');
@@ -313,8 +312,7 @@ export function createSettingsView(Alpine) {
313312
const path = await open({ directory: true, multiple: false });
314313
if (!path) return;
315314

316-
const { invoke } = window.__TAURI__.core;
317-
const folder = await invoke('watched_folders_add', {
315+
const folder = await tauriInvoke('watched_folders_add', {
318316
request: { path, mode: 'continuous', cadence_minutes: 10, enabled: true },
319317
});
320318
this.watchedFolders.push(folder);
@@ -329,8 +327,7 @@ export function createSettingsView(Alpine) {
329327
if (!window.__TAURI__) return;
330328

331329
try {
332-
const { invoke } = window.__TAURI__.core;
333-
await invoke('watched_folders_remove', { id });
330+
await tauriInvoke('watched_folders_remove', { id });
334331
} catch (error) {
335332
// If the folder was already removed (e.g. by delete-all), just clean up the UI
336333
if (!error?.toString().includes('not found')) {
@@ -347,8 +344,7 @@ export function createSettingsView(Alpine) {
347344
if (!window.__TAURI__) return;
348345

349346
try {
350-
const { invoke } = window.__TAURI__.core;
351-
const updated = await invoke('watched_folders_update', { id, request: updates });
347+
const updated = await tauriInvoke('watched_folders_update', { id, request: updates });
352348
const index = this.watchedFolders.findIndex((f) => f.id === id);
353349
if (index !== -1) {
354350
this.watchedFolders[index] = updated;
@@ -364,8 +360,7 @@ export function createSettingsView(Alpine) {
364360

365361
this.scanningFolders.add(id);
366362
try {
367-
const { invoke } = window.__TAURI__.core;
368-
await invoke('watched_folders_rescan', { id });
363+
await tauriInvoke('watched_folders_rescan', { id });
369364
Alpine.store('ui').toast('Rescan started', 'success');
370365
} catch (error) {
371366
console.error('[settings] Failed to rescan folder:', error);
@@ -387,18 +382,10 @@ export function createSettingsView(Alpine) {
387382
},
388383

389384
async resetSettings() {
390-
let confirmed = false;
391-
392-
if (window.__TAURI__?.dialog?.confirm) {
393-
confirmed = await window.__TAURI__.dialog.confirm(
394-
'This will reset all settings to their defaults. Your library and playlists will not be affected.',
395-
{ title: 'Reset Settings', kind: 'warning' },
396-
);
397-
} else {
398-
confirmed = confirm(
399-
'This will reset all settings to their defaults. Your library and playlists will not be affected.',
400-
);
401-
}
385+
const confirmed = await tauriConfirm(
386+
'This will reset all settings to their defaults. Your library and playlists will not be affected.',
387+
{ title: 'Reset Settings', kind: 'warning' },
388+
);
402389

403390
if (!confirmed) return;
404391

@@ -421,7 +408,6 @@ export function createSettingsView(Alpine) {
421408

422409
this.isExportingLogs = true;
423410
try {
424-
const { invoke } = window.__TAURI__.core;
425411
const { save } = window.__TAURI__.dialog;
426412

427413
const path = await save({
@@ -434,7 +420,7 @@ export function createSettingsView(Alpine) {
434420
return;
435421
}
436422

437-
await invoke('export_diagnostics', { path });
423+
await tauriInvoke('export_diagnostics', { path });
438424
Alpine.store('ui').toast('Diagnostics exported successfully', 'success');
439425
} catch (error) {
440426
console.error('[settings] Failed to export logs:', error);
@@ -452,8 +438,7 @@ export function createSettingsView(Alpine) {
452438
if (!window.__TAURI__) return;
453439

454440
try {
455-
const { invoke } = window.__TAURI__.core;
456-
const status = await invoke('network_cache_status');
441+
const status = await tauriInvoke('network_cache_status');
457442
this.networkCache.enabled = status.enabled;
458443
this.networkCache.persistent = status.persistent;
459444
this.networkCache.maxGb = status.max_bytes / 1_073_741_824;
@@ -519,8 +504,7 @@ export function createSettingsView(Alpine) {
519504

520505
this.networkCache.isPurging = true;
521506
try {
522-
const { invoke } = window.__TAURI__.core;
523-
await invoke('network_cache_purge');
507+
await tauriInvoke('network_cache_purge');
524508
this.networkCache.usedBytes = 0;
525509
this.networkCache.fileCount = 0;
526510
Alpine.store('ui').toast('Network cache cleared', 'success');
@@ -792,18 +776,10 @@ export function createSettingsView(Alpine) {
792776
},
793777

794778
async resetLovedCache() {
795-
let confirmed = false;
796-
797-
if (window.__TAURI__?.dialog?.confirm) {
798-
confirmed = await window.__TAURI__.dialog.confirm(
799-
'This will clear the loved tracks cache and remove auto-favorited tracks (synced from Last.fm). Manually favorited tracks are kept.\n\nYou can re-sync from Last.fm afterward to rebuild the cache.',
800-
{ title: 'Reset Loved Tracks Cache', kind: 'warning' },
801-
);
802-
} else {
803-
confirmed = confirm(
804-
'This will clear the loved tracks cache and remove auto-favorited tracks (synced from Last.fm). Manually favorited tracks are kept.\n\nYou can re-sync from Last.fm afterward to rebuild the cache.',
805-
);
806-
}
779+
const confirmed = await tauriConfirm(
780+
'This will clear the loved tracks cache and remove auto-favorited tracks (synced from Last.fm). Manually favorited tracks are kept.\n\nYou can re-sync from Last.fm afterward to rebuild the cache.',
781+
{ title: 'Reset Loved Tracks Cache', kind: 'warning' },
782+
);
807783

808784
if (!confirmed) return;
809785

@@ -850,14 +826,13 @@ export function createSettingsView(Alpine) {
850826

851827
let unlisten = null;
852828
try {
853-
const { invoke } = window.__TAURI__.core;
854829
const { listen } = window.__TAURI__.event;
855830

856831
unlisten = await listen('reconcile:progress', (e) => {
857832
this.reconcileScan.progress = e.payload;
858833
});
859834

860-
const result = await invoke('library_reconcile_scan');
835+
const result = await tauriInvoke('library_reconcile_scan');
861836
this.reconcileScan.lastResult = result;
862837

863838
const total = result.backfilled + result.duplicates_merged;
@@ -980,16 +955,10 @@ export function createSettingsView(Alpine) {
980955
if (this.columnSettings.resetSort) parts.push('sort settings');
981956

982957
const message = `Reset column ${parts.join(', ')}?`;
983-
984-
let confirmed = false;
985-
if (window.__TAURI__?.dialog?.confirm) {
986-
confirmed = await window.__TAURI__.dialog.confirm(message, {
987-
title: 'Reset Column Settings',
988-
kind: 'warning',
989-
});
990-
} else {
991-
confirmed = confirm(message);
992-
}
958+
const confirmed = await tauriConfirm(message, {
959+
title: 'Reset Column Settings',
960+
kind: 'warning',
961+
});
993962

994963
if (!confirmed) return;
995964
}

0 commit comments

Comments
 (0)