Skip to content
This repository was archived by the owner on Mar 28, 2026. It is now read-only.
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: test

on:
pull_request:
push:
branches:
- master

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install build dependencies from debian/control
run: |
sudo apt-get update
sudo apt-get install --yes devscripts equivs
sudo mk-build-deps -i -r -t "apt-get --yes" debian/control

- name: Configure
run: cmake -B build -DCMAKE_BUILD_TYPE=Debug

- name: Build
run: cmake --build build

- name: Run tests
run: |
cd build
ctest --output-on-failure
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ endif(Qt5_POSITION_INDEPENDENT_CODE)
add_subdirectory(external)
add_subdirectory(src)

enable_testing()
add_subdirectory(tests)

include(CPackConfiguration)

add_custom_target(install_app_bundle COMMAND ${CMAKE_COMMAND} -P cmake_install.cmake DEPENDS JellyfinMediaPlayer)
77 changes: 77 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Contributing to Jellyfin Media Player

## Running Tests

Jellyfin Media Player uses Qt Test for unit testing.

### Building Tests

Tests are built automatically when you build the project:

```sh
cmake -B build
cmake --build build
```

### Running Tests

Run all tests using CTest:

```sh
cd build
ctest
```

Or run individual test executables directly:

```sh
cd build
./tests/test_systemcomponent
```

### Writing Tests

Tests are located in the `tests/` directory. To add a new test:

1. Create a test file in `tests/` (e.g., `test_mycomponent.cpp`)
2. Use `QTEST_APPLESS_MAIN` for headless unit tests
3. Add the test to `tests/CMakeLists.txt`

Example test structure:

```cpp
#include <QtTest/QtTest>
#include "../src/mycomponent/MyComponent.h"

class TestMyComponent : public QObject
{
Q_OBJECT

private slots:
void testMyFunction_data();
void testMyFunction();
};

void TestMyComponent::testMyFunction_data()
{
QTest::addColumn<QString>("input");
QTest::addColumn<QString>("expected");

QTest::newRow("test case 1") << "input1" << "output1";
QTest::newRow("test case 2") << "input2" << "output2";
}

void TestMyComponent::testMyFunction()
{
QFETCH(QString, input);
QFETCH(QString, expected);

QString result = MyComponent::myFunction(input);
QCOMPARE(result, expected);
}

QTEST_APPLESS_MAIN(TestMyComponent)
#include "test_mycomponent.moc"
```

For more information on Qt Test, see the [Qt Test documentation](https://doc.qt.io/qt-6/qtest-overview.html).
43 changes: 34 additions & 9 deletions native/connectivityHelper.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
window.jmpCheckServerConnectivity = (() => {
let checkInProgress = false;
let activeController = null;

return async function(url) {
if (checkInProgress) {
throw new Error('Connectivity check already in progress');
const checkFunc = async function(url) {
// Abort any in-progress check
if (activeController) {
activeController.abort();
}

// Wait for API
Expand All @@ -16,24 +17,48 @@ window.jmpCheckServerConnectivity = (() => {
throw new Error('WebChannel not available');
}

checkInProgress = true;
// Create abort controller for this check
const controller = new AbortController();
activeController = controller;

return new Promise((resolve, reject) => {
const handler = (resultUrl, success) => {
if (resultUrl === url) {
// Handle abort
controller.signal.addEventListener('abort', () => {
if (handler) {
window.api.system.serverConnectivityResult.disconnect(handler);
checkInProgress = false;
}
reject(new Error('Connection cancelled'));
});

let handler = (resultUrl, success, resolvedUrl) => {
if (resultUrl === url && !controller.signal.aborted) {
window.api.system.serverConnectivityResult.disconnect(handler);
handler = null;
if (activeController === controller) {
activeController = null;
}
if (success) {
resolve();
resolve(resolvedUrl);
} else {
reject(new Error('Connection failed'));
}
}
};

window.api.system.serverConnectivityResult.connect(handler);
window.api.system.checkServerConnectivity(url);
});
};

// Expose abort function for cancellation
checkFunc.abort = () => {
if (activeController) {
activeController.abort();
activeController = null;
}
};

return checkFunc;
})();

window.jmpFetchPage = (() => {
Expand Down
43 changes: 20 additions & 23 deletions native/find-webclient.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ async function tryConnect(server) {
if (!server.startsWith("http")) {
server = "http://" + server;
}
serverBaseURL = server.replace(/\/+$/, "");

console.log("Checking connectivity to:", server);

await window.jmpCheckServerConnectivity(server);
const resolvedUrl = await window.jmpCheckServerConnectivity(server);
console.log("Server connectivity check passed");
console.log("Resolved URL:", resolvedUrl);

// Save original URL but navigate to fully-resolved redirect
window.jmpInfo.settings.main.userWebClient = server;
window.location = server;

// Navigation will clean up handlers, but do it explicitly
window.location = resolvedUrl;

return true;
} catch (e) {
Expand Down Expand Up @@ -54,16 +58,8 @@ const startConnecting = async () => {
button.style.visibility = 'hidden';
document.addEventListener('keydown', cancelOnEscape);

let connected = false;

while (!connected && isConnecting) {
connected = await tryConnect(server);

if (!connected && isConnecting) {
// Wait 5 seconds before retrying
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
// C++ handles retries, just wait for result
const connected = await tryConnect(server);

if (!connected) {
isConnecting = false;
Expand All @@ -82,8 +78,17 @@ const startConnecting = async () => {
const cancelConnection = () => {
if (!isConnecting) return;

console.log("Cancelling connection");
isConnecting = false;

// Cancel C++ connectivity check and abort JS promise
if (window.api && window.api.system) {
window.api.system.cancelServerConnectivity();
}
if (window.jmpCheckServerConnectivity.abort) {
window.jmpCheckServerConnectivity.abort();
}

const address = document.getElementById('address');
const title = document.getElementById('title');
const spinner = document.getElementById('spinner');
Expand Down Expand Up @@ -160,16 +165,8 @@ document.addEventListener('keydown', (e) => {
button.style.visibility = 'hidden';
document.addEventListener('keydown', cancelOnEscape);

let connected = false;

while (!connected && isConnecting) {
connected = await tryConnect(savedServer);

if (!connected && isConnecting) {
// Wait 5 seconds before retrying
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
// C++ handles retries, just wait for result
const connected = await tryConnect(savedServer);

if (!connected) {
// User cancelled or error - show UI
Expand Down
Loading