Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
45 changes: 42 additions & 3 deletions jest.config.cjs
Original file line number Diff line number Diff line change
@@ -1,21 +1,60 @@
/** @type {import('jest').Config} */

// Check if better-sqlite3 native bindings are available
const { readdirSync, readFileSync, statSync } = require('fs');
const { join } = require('path');

// Check if better-sqlite3 native bindings are available. The `midi` native
// module (and better-sqlite3) are often absent in containers/CI where the
// project is installed with `npm install --ignore-scripts` (see CLAUDE.md), so
// the SQLite-backed suites must be skipped rather than fail with a bindings
// error.
let hasBetterSqlite = false;
try {
const Database = require('better-sqlite3');
// Actually try to create an in-memory database
// Actually try to create an in-memory database — requiring the module alone
// does NOT load the native binding; only instantiating a Database does.
const db = new Database(':memory:');
db.close();
hasBetterSqlite = true;
} catch {
// Native bindings not compiled
}

// A suite genuinely needs the native SQLite bindings when it imports
// `better-sqlite3` directly, constructs the top-level persistence
// Database/DatabaseManager (which connects in its constructor), or runs the
// migration runner. Detecting this from the test source keeps the skip list
// self-maintaining: new SQLite-backed suites are picked up automatically
// instead of silently failing once the hard-coded list drifts.
const NEEDS_SQLITE = /better-sqlite3|\bnew DatabaseManager\s*\(|new Database\s*\(\s*\{|runMigrations\s*\(/;

function collectSqliteSuites(dir, acc = []) {
let entries;
try {
entries = readdirSync(dir);
} catch {
return acc;
}
for (const entry of entries) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) {
if (entry !== 'frontend') collectSqliteSuites(full, acc);
} else if (entry.endsWith('.test.js')) {
if (NEEDS_SQLITE.test(readFileSync(full, 'utf8'))) acc.push(full);
}
}
return acc;
}

// `audit-i18n.test.js` uses the Vitest API (it is run by the frontend Vitest
// project, see vitest.config.js), so it must never be collected by Jest.
const ignorePatterns = ['/node_modules/', '/tests/frontend/', '/tests/audit-i18n.test.js'];

if (!hasBetterSqlite) {
ignorePatterns.push('/tests/midi-filter.test.js');
for (const suite of collectSqliteSuites(join(__dirname, 'tests'))) {
// Anchor on the repo-relative path so only this exact file is ignored.
ignorePatterns.push(suite.slice(__dirname.length).replace(/\\/g, '/'));
}
}

module.exports = {
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"node": ">=20.0.0"
},
"overrides": {
"node-gyp": ">=10.0.0"
"node-gyp": ">=10.0.0",
"tar": ">=7.5.21"
}
}
14 changes: 13 additions & 1 deletion public/js/api/BackendAPIClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,20 @@
if (wasConnected) {
this.emit('disconnected');
}
// Clear the in-flight reconnect guard before rescheduling. When a
// reconnect attempt's socket fails asynchronously, `onerror` does not
// reject (it is suppressed while `_reconnecting` is true) and this
// `connect()` promise never settles, so the timer's `.catch` that
// would reset `_reconnecting` never runs. Without resetting it here,
// `attemptReconnect()` early-returns and the retry loop dies after a
// single attempt — contradicting the "retries indefinitely" contract
// and requiring a manual page reload to recover.
this._reconnecting = false;
this.attemptReconnect();
};

this.ws.onerror = (error) => {
console.error('WebSocket error:', error);

Check warning on line 154 in public/js/api/BackendAPIClient.js

View workflow job for this annotation

GitHub Actions / Frontend smoke (no native deps)

Unexpected console statement

Check warning on line 154 in public/js/api/BackendAPIClient.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Unexpected console statement
const errorMessage = error.message || error.type || 'WebSocket connection failed';
this.emit('error', { message: errorMessage, error: error });
// Ne pas reject ici - onclose sera appele ensuite
Expand All @@ -167,7 +176,7 @@
const message = JSON.parse(event.data);
this.handleMessage(message);
} catch (error) {
console.error('Failed to parse message:', error);

Check warning on line 179 in public/js/api/BackendAPIClient.js

View workflow job for this annotation

GitHub Actions / Frontend smoke (no native deps)

Unexpected console statement

Check warning on line 179 in public/js/api/BackendAPIClient.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Unexpected console statement
}
};
} catch (error) {
Expand All @@ -184,7 +193,7 @@
*/
_rejectPendingRequests(reason) {
if (this.pendingRequests.size > 0) {
console.warn(`Rejecting ${this.pendingRequests.size} pending requests: ${reason}`);

Check warning on line 196 in public/js/api/BackendAPIClient.js

View workflow job for this annotation

GitHub Actions / Frontend smoke (no native deps)

Unexpected console statement

Check warning on line 196 in public/js/api/BackendAPIClient.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Unexpected console statement
for (const [, pending] of this.pendingRequests) {
pending.reject(new Error(reason));
}
Expand Down Expand Up @@ -232,7 +241,7 @@
if (this._closed) return;
this.connect().catch((err) => {
if (this._closed) return;
console.warn(`Reconnect attempt ${this.reconnectAttempts} failed:`, err.message);

Check warning on line 244 in public/js/api/BackendAPIClient.js

View workflow job for this annotation

GitHub Actions / Frontend smoke (no native deps)

Unexpected console statement

Check warning on line 244 in public/js/api/BackendAPIClient.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Unexpected console statement
this._reconnecting = false;
this.attemptReconnect();
});
Expand All @@ -257,7 +266,10 @@
if (message.command !== undefined) err.command = message.command;
pending.reject(err);
} else {
pending.resolve(message.data || message);
// Use presence, not truthiness: a handler that legitimately returns
// falsy `data` (0, false, '', null) must not leak the raw protocol
// envelope ({ id, data, timestamp, … }) to the caller.
pending.resolve('data' in message ? message.data : message);
}
return;
}
Expand Down Expand Up @@ -302,7 +314,7 @@
try {
handler(data);
} catch (error) {
console.error(`Error in event handler for ${event}:`, error);

Check warning on line 317 in public/js/api/BackendAPIClient.js

View workflow job for this annotation

GitHub Actions / Frontend smoke (no native deps)

Unexpected console statement

Check warning on line 317 in public/js/api/BackendAPIClient.js

View workflow job for this annotation

GitHub Actions / Lint & Format

Unexpected console statement
}
});
}
Expand Down
4 changes: 2 additions & 2 deletions public/js/audio/MidiSynthesizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

// Constants extracted to MidiSynthesizerConstants.js (P2-F.8).
// Loaded earlier in index.html so window.MidiSynthesizerConstants is available.
const { SOUND_BANKS, DEFAULT_BANK_ID, DEFAULT_BANK_SUFFIX, getAvailableBanks } =

Check warning on line 9 in public/js/audio/MidiSynthesizer.js

View workflow job for this annotation

GitHub Actions / Frontend smoke (no native deps)

'SOUND_BANKS' is assigned a value but never used

Check warning on line 9 in public/js/audio/MidiSynthesizer.js

View workflow job for this annotation

GitHub Actions / Lint & Format

'SOUND_BANKS' is assigned a value but never used
window.MidiSynthesizerConstants;

// GMBP binary preset decoder. Mirrors src/files/SF2PresetCodec.js — kept
Expand Down Expand Up @@ -1278,8 +1278,8 @@
// fully-lazy mode while still saving ~half the eager requests.
if (usedNotes.size === 0) {
const COMMON_DRUMS = [
35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59
35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
58, 59
];
for (const n of COMMON_DRUMS) usedNotes.add(n);
}
Expand Down
68 changes: 43 additions & 25 deletions public/js/audio/MidiSynthesizerConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Exposed on `window.MidiSynthesizerConstants` because the codebase uses
// IIFE+globals (no ES modules in /public/js).

(function() {
(function () {
'use strict';

/**
Expand All @@ -27,7 +27,7 @@
reverbMix: 0.12,
isBuiltInSF2: true,
sf2Id: 'default',
drumKits: [0, 8, 16, 24, 25, 32, 40, 48, 56].map(function(p) {
drumKits: [0, 8, 16, 24, 25, 32, 40, 48, 56].map(function (p) {
return { midiProgram: p, bankIndex: p, verified: true };
})
};
Expand Down Expand Up @@ -60,24 +60,34 @@
// any feature the rest of the codebase depends on.
const WAF_BANKS = [
{
id: 'FluidR3_GM', label: 'FluidR3 GM', suffix: 'FluidR3_GM_sf2_file',
quality: 'high', sizeMB: 141, descKey: 'settings.soundBank.banks.FluidR3_GM', reverbMix: 0.08,
id: 'FluidR3_GM',
label: 'FluidR3 GM',
suffix: 'FluidR3_GM_sf2_file',
quality: 'high',
sizeMB: 141,
descKey: 'settings.soundBank.banks.FluidR3_GM',
reverbMix: 0.08,
requiresExternal: true,
drumKits: [
{ midiProgram: 0, bankIndex: 0, verified: true },
{ midiProgram: 8, bankIndex: 8, verified: true },
{ midiProgram: 16, bankIndex: 16, verified: true },
{ midiProgram: 24, bankIndex: 24, verified: true },
{ midiProgram: 25, bankIndex: 25, verified: true },
{ midiProgram: 0, bankIndex: 0, verified: true },
{ midiProgram: 8, bankIndex: 8, verified: true },
{ midiProgram: 16, bankIndex: 16, verified: true },
{ midiProgram: 24, bankIndex: 24, verified: true },
{ midiProgram: 25, bankIndex: 25, verified: true },
{ midiProgram: 32, bankIndex: 32, verified: false },
{ midiProgram: 40, bankIndex: 40, verified: false },
{ midiProgram: 48, bankIndex: 48, verified: false },
{ midiProgram: 56, bankIndex: 56, verified: false }
]
},
{
id: 'JCLive', label: 'JCLive', suffix: 'JCLive_sf2_file',
quality: 'medium', sizeMB: 26, descKey: 'settings.soundBank.banks.JCLive', reverbMix: 0.10,
id: 'JCLive',
label: 'JCLive',
suffix: 'JCLive_sf2_file',
quality: 'medium',
sizeMB: 26,
descKey: 'settings.soundBank.banks.JCLive',
reverbMix: 0.1,
requiresExternal: true,
drumKits: [{ midiProgram: 0, bankIndex: 12, verified: true }]
}
Expand All @@ -92,19 +102,19 @@
let _customBanks = [];

function setCustomBanks(banks) {
_customBanks = (banks || []).map(function(b) {
_customBanks = (banks || []).map(function (b) {
return {
id: 'sf2:' + b.id,
label: b.label + ' [SF2]',
suffix: null,
quality: 'custom',
sizeMB: Math.round((b.size || 0) / (1024 * 1024)),
reverbMix: b.reverbMix != null ? b.reverbMix : 0.12,
isCustom: true,
sf2Id: b.id,
drumKits: [0, 8, 16, 24, 25, 32, 40, 48, 56].map(function(p) {
id: 'sf2:' + b.id,
label: b.label + ' [SF2]',
suffix: null,
quality: 'custom',
sizeMB: Math.round((b.size || 0) / (1024 * 1024)),
reverbMix: b.reverbMix != null ? b.reverbMix : 0.12,
isCustom: true,
sf2Id: b.id,
drumKits: [0, 8, 16, 24, 25, 32, 40, 48, 56].map(function (p) {
return { midiProgram: p, bankIndex: p, verified: false };
}),
})
};
});
}
Expand All @@ -131,13 +141,21 @@
// for backwards compatibility (InstrumentSettingsModal still reads it),
// but it now resolves to the gated list — never the legacy WAF banks.
get SOUND_BANKS() {
return Object.freeze([BUILT_IN_DEFAULT_SF2_BANK].map(function(b) { return Object.freeze(b); }));
return Object.freeze(
[BUILT_IN_DEFAULT_SF2_BANK].map(function (b) {
return Object.freeze(b);
})
);
},
BUILT_IN_DEFAULT_SF2_BANK: Object.freeze(BUILT_IN_DEFAULT_SF2_BANK),
WAF_BANKS: Object.freeze(WAF_BANKS.map(function(b) { return Object.freeze(b); })),
WAF_BANKS: Object.freeze(
WAF_BANKS.map(function (b) {
return Object.freeze(b);
})
),
DEFAULT_BANK_ID,
DEFAULT_BANK_SUFFIX,
setCustomBanks,
getAvailableBanks,
getAvailableBanks
};
})();
5 changes: 3 additions & 2 deletions public/js/audio/MidiSynthesizerTempoMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// from MidiSynthesizer.js (P2-F.8b).
// Exposed on `window.MidiSynthesizerTempoMap` (IIFE+globals convention).

(function() {
(function () {
'use strict';

/**
Expand Down Expand Up @@ -79,7 +79,8 @@
* The sequence is assumed sorted by `t` ascending.
*/
function findNoteIndex(sequence, tick) {
let lo = 0, hi = sequence.length;
let lo = 0,
hi = sequence.length;
while (lo < hi) {
const mid = (lo + hi) >>> 1;
if (sequence[mid].t <= tick) lo = mid + 1;
Expand Down
Loading
Loading