-
{{ GET_TITLE }}
-
{{ GET_SECONDARY_TITLE }}
+
+ {{ GET_TITLE }}
+
+
+ {{ GET_SECONDARY_TITLE }}
+
Playing from {{ GET_PLEX_SERVER?.name }}
diff --git a/test/cache.test.js b/test/cache.test.js
index 57a2f7aca9..df71d3a0b2 100644
--- a/test/cache.test.js
+++ b/test/cache.test.js
@@ -74,7 +74,7 @@ describe('cache', () => {
const { setMetadata, getMetadata, metadataCache } = createCache();
// Fill cache to max
- for (let i = 0; i < METADATA_MAX_SIZE; i++) {
+ for (let i = 0; i < METADATA_MAX_SIZE; i += 1) {
setMetadata(`key${i}`, { title: `Entry ${i}` });
}
assert.equal(metadataCache.size, METADATA_MAX_SIZE);
@@ -90,7 +90,7 @@ describe('cache', () => {
it('does not evict when updating an existing key', () => {
const { setMetadata, getMetadata, metadataCache } = createCache();
- for (let i = 0; i < METADATA_MAX_SIZE; i++) {
+ for (let i = 0; i < METADATA_MAX_SIZE; i += 1) {
setMetadata(`key${i}`, { title: `Entry ${i}` });
}
diff --git a/test/combineurl.test.js b/test/combineurl.test.js
index b792a0d50e..0630e3387d 100644
--- a/test/combineurl.test.js
+++ b/test/combineurl.test.js
@@ -7,6 +7,8 @@ let combineRelativeUrlParts;
describe('combineurl', () => {
before(async () => {
+ // Node ESM resolution requires the extension for this CommonJS dynamic import.
+ // eslint-disable-next-line import/extensions
const mod = await import('../src/utils/combineurl.js');
combineUrl = mod.combineUrl;
combineRelativeUrlParts = mod.combineRelativeUrlParts;
diff --git a/test/kick.test.js b/test/kick.test.js
index 4552942b21..8d7ff697a6 100644
--- a/test/kick.test.js
+++ b/test/kick.test.js
@@ -1,4 +1,6 @@
-const { describe, it, before, after } = require('node:test');
+const {
+ describe, it, before, after,
+} = require('node:test');
const assert = require('node:assert/strict');
const { spawn } = require('node:child_process');
const { io } = require('socket.io-client');
@@ -10,11 +12,12 @@ const wait = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
function waitForSocketEvent(socket, eventName, timeoutMs = 2000) {
return new Promise((resolve, reject) => {
+ let onEvent;
const timeout = setTimeout(() => {
socket.off(eventName, onEvent);
reject(new Error(`Timed out waiting for ${eventName}`));
}, timeoutMs);
- const onEvent = (data) => {
+ onEvent = (data) => {
clearTimeout(timeout);
resolve(data);
};
@@ -23,17 +26,55 @@ function waitForSocketEvent(socket, eventName, timeoutMs = 2000) {
}
async function waitForServer(url, retries = 30, delay = 200) {
- for (let i = 0; i < retries; i++) {
+ for (let i = 0; i < retries; i += 1) {
try {
- await fetch(url);
- return;
+ // Sequential polling is intentional: each request observes a later server state.
+ // eslint-disable-next-line no-await-in-loop
+ const response = await fetch(url, { signal: AbortSignal.timeout(500) });
+ const healthy = response.ok;
+ // eslint-disable-next-line no-await-in-loop
+ await response.body?.cancel();
+ if (healthy) return;
} catch {
- await wait(delay);
+ // Retry connection failures and per-attempt timeouts.
}
+ // eslint-disable-next-line no-await-in-loop
+ await wait(delay);
}
throw new Error('Server did not start in time');
}
+async function stopServer(processToStop) {
+ if (!processToStop
+ || processToStop.exitCode !== null
+ || processToStop.signalCode !== null) return;
+
+ const exited = new Promise((resolve) => {
+ processToStop.once('error', resolve);
+ processToStop.once('close', resolve);
+ });
+ const waitForExit = async () => {
+ let timeout;
+ const didExit = await Promise.race([
+ exited.then(() => true),
+ new Promise((resolve) => {
+ timeout = setTimeout(() => resolve(false), 2000);
+ }),
+ ]);
+ clearTimeout(timeout);
+ return didExit;
+ };
+
+ if (processToStop.exitCode !== null || processToStop.signalCode !== null) return;
+ processToStop.kill('SIGTERM');
+ if (!await waitForExit()
+ && processToStop.exitCode === null
+ && processToStop.signalCode === null) {
+ processToStop.kill('SIGKILL');
+ assert.ok(await waitForExit(), 'server did not exit after SIGKILL');
+ }
+}
+
function joinClient({ roomId, username }) {
return new Promise((resolve, reject) => {
const socket = io(BASE, {
@@ -76,7 +117,7 @@ function joinClient({ roomId, username }) {
describe('kick socket event', () => {
before(async () => {
serverProcess = spawn('node', ['server.js'], {
- cwd: __dirname + '/..',
+ cwd: `${__dirname}/..`,
env: {
...process.env,
PORT: '18089',
@@ -89,10 +130,8 @@ describe('kick socket event', () => {
await waitForServer(`${BASE}/health`);
});
- after(() => {
- if (serverProcess) {
- serverProcess.kill('SIGTERM');
- }
+ after(async () => {
+ await stopServer(serverProcess);
});
it('server removes kicked users even if the kicked client does not disconnect itself', async () => {
diff --git a/test/server.test.js b/test/server.test.js
index e6058a7653..d08e62614b 100644
--- a/test/server.test.js
+++ b/test/server.test.js
@@ -1,5 +1,8 @@
-const { describe, it, before, after } = require('node:test');
+const {
+ describe, it, before, after,
+} = require('node:test');
const assert = require('node:assert/strict');
+const { spawn } = require('node:child_process');
const http = require('node:http');
let baseUrl;
@@ -13,9 +16,13 @@ let resolveSlowPosterClosed;
async function getFreePort() {
const server = http.createServer();
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
+ await new Promise((resolve) => {
+ server.listen(0, '127.0.0.1', resolve);
+ });
const { port } = server.address();
- await new Promise((resolve) => server.close(resolve));
+ await new Promise((resolve) => {
+ server.close(resolve);
+ });
return port;
}
@@ -38,17 +45,68 @@ function metadataBodyWithExactSize(size) {
}
async function waitForServer(url, retries = 30, delay = 200) {
- for (let i = 0; i < retries; i++) {
+ for (let i = 0; i < retries; i += 1) {
try {
- await fetch(url);
- return;
+ // Sequential polling is intentional: each request observes a later server state.
+ // eslint-disable-next-line no-await-in-loop
+ const response = await fetch(url, { signal: AbortSignal.timeout(500) });
+ const healthy = response.ok;
+ // eslint-disable-next-line no-await-in-loop
+ await response.body?.cancel();
+ if (healthy) return;
} catch {
- await new Promise((r) => setTimeout(r, delay));
+ // Retry connection failures and per-attempt timeouts.
}
+ // eslint-disable-next-line no-await-in-loop
+ await new Promise((resolve) => {
+ setTimeout(resolve, delay);
+ });
}
throw new Error('Server did not start in time');
}
+async function stopServer(processToStop) {
+ if (!processToStop
+ || processToStop.exitCode !== null
+ || processToStop.signalCode !== null) return;
+
+ const exited = new Promise((resolve) => {
+ processToStop.once('error', resolve);
+ processToStop.once('close', resolve);
+ });
+ const waitForExit = async () => {
+ let timeout;
+ const didExit = await Promise.race([
+ exited.then(() => true),
+ new Promise((resolve) => {
+ timeout = setTimeout(() => resolve(false), 2000);
+ }),
+ ]);
+ clearTimeout(timeout);
+ return didExit;
+ };
+
+ if (processToStop.exitCode !== null || processToStop.signalCode !== null) return;
+ processToStop.kill('SIGTERM');
+ if (!await waitForExit()
+ && processToStop.exitCode === null
+ && processToStop.signalCode === null) {
+ processToStop.kill('SIGKILL');
+ assert.ok(await waitForExit(), 'server did not exit after SIGKILL');
+ }
+}
+
+const closeServer = (server) => new Promise((resolve, reject) => {
+ if (!server?.listening) {
+ resolve();
+ return;
+ }
+ server.close((error) => {
+ if (error) reject(error);
+ else resolve();
+ });
+});
+
describe('server', () => {
before(async () => {
slowPosterStarted = new Promise((resolve) => {
@@ -104,11 +162,10 @@ describe('server', () => {
});
posterFixtureBase = `http://127.0.0.1:${posterFixtureServer.address().port}`;
- const { spawn } = require('node:child_process');
const port = await getFreePort();
baseUrl = `http://127.0.0.1:${port}`;
serverProcess = spawn('node', ['server.js'], {
- cwd: __dirname + '/..',
+ cwd: `${__dirname}/..`,
env: {
...process.env,
PORT: String(port),
@@ -125,13 +182,13 @@ describe('server', () => {
await waitForServer(`${baseUrl}/health`);
});
- after(() => {
- if (serverProcess) {
- serverProcess.kill('SIGTERM');
- }
- if (posterFixtureServer) {
- posterFixtureServer.close();
- }
+ after(async () => {
+ const outcomes = await Promise.allSettled([
+ stopServer(serverProcess),
+ closeServer(posterFixtureServer),
+ ]);
+ const failedCleanup = outcomes.find(({ status }) => status === 'rejected');
+ if (failedCleanup) throw failedCleanup.reason;
});
// --- SPA fallback ---
diff --git a/test/socket-validation.test.js b/test/socket-validation.test.js
index 6d27093fdb..897b0cda0c 100644
--- a/test/socket-validation.test.js
+++ b/test/socket-validation.test.js
@@ -29,13 +29,13 @@ async function waitForServer(url, retries = 30, delay = 200) {
try {
// Sequential polling is intentional: each request observes a later server state.
// eslint-disable-next-line no-await-in-loop
- const response = await fetch(url);
+ const response = await fetch(url, { signal: AbortSignal.timeout(500) });
const healthy = response.ok;
// eslint-disable-next-line no-await-in-loop
await response.body?.cancel();
if (healthy) return;
} catch {
- // Retry until the child process starts listening.
+ // Retry connection failures and per-attempt timeouts.
}
// eslint-disable-next-line no-await-in-loop
await wait(delay);
@@ -43,6 +43,37 @@ async function waitForServer(url, retries = 30, delay = 200) {
throw new Error('Server did not start in time');
}
+async function stopServer(processToStop) {
+ if (!processToStop
+ || processToStop.exitCode !== null
+ || processToStop.signalCode !== null) return;
+
+ const exited = new Promise((resolve) => {
+ processToStop.once('error', resolve);
+ processToStop.once('close', resolve);
+ });
+ const waitForExit = async () => {
+ let timeout;
+ const didExit = await Promise.race([
+ exited.then(() => true),
+ new Promise((resolve) => {
+ timeout = setTimeout(() => resolve(false), 2000);
+ }),
+ ]);
+ clearTimeout(timeout);
+ return didExit;
+ };
+
+ if (processToStop.exitCode !== null || processToStop.signalCode !== null) return;
+ processToStop.kill('SIGTERM');
+ if (!await waitForExit()
+ && processToStop.exitCode === null
+ && processToStop.signalCode === null) {
+ processToStop.kill('SIGKILL');
+ assert.ok(await waitForExit(), 'server did not exit after SIGKILL');
+ }
+}
+
function connectClient() {
return io(baseUrl, {
path: '/socket.io',
@@ -110,8 +141,8 @@ describe('socket event validation', () => {
await waitForServer(`${baseUrl}/health`);
});
- after(() => {
- if (serverProcess) serverProcess.kill('SIGTERM');
+ after(async () => {
+ await stopServer(serverProcess);
});
it('disconnects a client that does not answer the application ping', async () => {
diff --git a/vite.config.js b/vite.config.js
index 3575afe841..cff0339215 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -39,8 +39,6 @@ export function generateConfigPlugin({
};
}
-const pkg = require('./package.json');
-
export default defineConfig({
plugins: [
patchLibjassPlugin(),