Compare commits

...

6 Commits

Author SHA1 Message Date
MasterAcnolo
d0bdd887e3 fix: exclude dist, release and build folder from jest config 2026-08-02 21:40:39 +02:00
MasterAcnolo
609ac2eadb fix: better dev mode comparaison, useful for tests 2026-08-02 09:25:39 +02:00
MasterAcnolo
c484d322bf feat: unit tests for buildArgs and Notify 2026-08-02 09:25:05 +02:00
Axel Nicolas
b321a1d164 Merge pull request #45 from MasterAcnolo/linux-version
1.6.0 - Linux version
2026-07-28 21:38:03 +02:00
MasterAcnolo
89e6fcf9bc fix: snap Store package.json arguments 2026-07-28 20:43:04 +02:00
MasterAcnolo
ba2f317551 fix: snap Store mention in SECURITY.md 2026-07-28 20:42:51 +02:00
8 changed files with 4711 additions and 1758 deletions

View File

@@ -7,7 +7,7 @@
### **A clean, open-source multimedia downloader for Windows and Linux**
[![Release](https://img.shields.io/badge/Release-1.6.0-blue?style=for-the-badge)](https://github.com/MasterAcnolo/Freedom-Loader/releases)
[![Release](https://img.shields.io/github/v/release/MasterAcnolo/Freedom-Loader?style=for-the-badge&color=blue)](https://github.com/MasterAcnolo/Freedom-Loader/releases)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-red.svg?style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0)
[![Website](https://img.shields.io/badge/Website-Visit-404040?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Site/)
[![Workshop](https://img.shields.io/badge/Worshop-Visit-0E05A1?style=for-the-badge)](https://masteracnolo.github.io/Freedom-Loader-Workshop/)
@@ -88,8 +88,7 @@ The primary goal is to make media downloading accessible to users who want offli
[![Fedora Copr](https://img.shields.io/badge/Fedora-Copr-294172?style=for-the-badge&logo=fedora&logoColor=white)](https://copr.fedorainfracloud.org/coprs/masteracnolo/freedom-loader/)
[![Debian/Ubuntu](https://img.shields.io/badge/.deb-Debian%2FUbuntu-A81D33?style=for-the-badge&logo=debian&logoColor=white)](https://github.com/MasterAcnolo/Freedom-Loader/releases)
[//]: # ([![Snap Store](https://img.shields.io/badge/Snap-Store-82BEA0?style=for-the-badge&logo=snapcraft&logoColor=white)](https://snapcraft.io/freedom-loader))
[![Snap Store](https://img.shields.io/badge/Snap-Store-82BEA0?style=for-the-badge&logo=snapcraft&logoColor=white)](https://snapcraft.io/freedom-loader)
[![AppImage](https://img.shields.io/badge/AppImage-Universal-1E88E5?style=for-the-badge&logo=linux&logoColor=white)](https://github.com/MasterAcnolo/Freedom-Loader/releases)
### Requirements
@@ -122,14 +121,11 @@ sudo dnf install freedom-loader
sudo apt install ./freedom-loader_1.x.x_amd64.deb
```
[//]: # (#### Snap (any distro with snapd))
#### Snap (any distro with snapd)
[//]: # ()
[//]: # (```bash)
[//]: # (sudo snap install freedom-loader)
[//]: # (```)
```bash
sudo snap install freedom-loader
```
#### RPM (Fedora/RHEL/openSUSE, without Copr)
@@ -543,7 +539,7 @@ You are free to use, modify, and redistribute this software under the terms of t
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) for the robust download engine
- [FFmpeg](https://ffmpeg.org/) for media processing capabilities
- [Deno](https://deno.com/) for helping me bypassing challenges
- [Deno](https://deno.com/) for helping me to bypass challenges
- [Electron](https://www.electronjs.org/) for the desktop application framework
- The open-source community for continuous support and contributions
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one

View File

@@ -47,7 +47,7 @@ When using Freedom Loader, I recommend:
- **Official Sources**: Download only from official sources:
- [GitHub Releases](https://github.com/MasterAcnolo/Freedom-Loader/releases)
- [Fedora COPR](https://copr.fedorainfracloud.org/coprs/masteracnolo/freedom-loader/)
<!-- - [Snap Store](https://snapcraft.io/freedom-loader) -->
- [Snap Store](https://snapcraft.io/freedom-loader)
- **Verify Downloads**: Check that installers are from one of the above sources
- **Antivirus**: Keep your antivirus software up to date

View File

@@ -7,7 +7,7 @@ const path = require("path");
* Indicates whether the application is running in development mode.
* Determined by Electron's packaging state.
*/
const devMode = !app.isPackaged;
const devMode = process.env.NODE_ENV === "test" || !app?.isPackaged;
/**
* Resolves the configuration file path depending on runtime environment.

21
jest.config.js Normal file
View File

@@ -0,0 +1,21 @@
module.exports = {
testEnvironment: "node",
testMatch: ["**/test/unit/**/*.test.js"],
collectCoverageFrom: [
"server/**/*.js",
"app/**/*.js",
"!app/autoUpdater.js",
],
modulePathIgnorePatterns: [
"<rootDir>/dist",
"<rootDir>/release",
"<rootDir>/build",
],
watchPathIgnorePatterns: [
"<rootDir>/dist",
"<rootDir>/release",
"<rootDir>/build",
],
};

6092
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,12 +16,16 @@
"main": "main.js",
"scripts": {
"start": "electron --trace-warnings .",
"postinstall": "chmod +x resources/binaries/linux/* 2>/dev/null || true",
"build": "electron-builder",
"build:win": "electron-builder --win",
"build:linux": "electron-builder --linux",
"build:rpm": "bash scripts/create-copr-package.sh",
"build:all": "electron-builder -wl",
"test": "jest",
"test:unit": "jest tests/unit",
"update": "npm update"
},
"dependencies": {
@@ -38,8 +42,12 @@
},
"devDependencies": {
"@electron/devtron": "^2.1.1",
"@jest/globals": "^30.4.1",
"@playwright/test": "^1.62.1",
"electron": "^41.2.0",
"electron-builder": "^25.1.8"
"electron-builder": "^26.15.3",
"jest": "^30.4.2",
"playwright": "^1.62.1"
},
"build": {
"appId": "com.masteracnolo.freedomloader",
@@ -164,14 +172,7 @@
"confinement": "strict",
"plugs": [
"default",
"home",
"removable-media",
"network",
"audio-playback",
"desktop",
"desktop-legacy",
"x11",
"wayland"
"removable-media"
]
}
}

182
test/unit/buildArgs.test.js Normal file
View File

@@ -0,0 +1,182 @@
const path = require("path");
jest.mock("../../server/helpers/getBrowser.helpers.js", () =>
jest.fn(() => "firefox")
);
jest.mock("../../server/helpers/path.helpers.js", () => ({
ffmpegPath: "/mock/ffmpeg",
denoPath: "/mock/deno",
}));
jest.mock("../../config.js", () => ({
configFeatures: {
autoUpdate: false,
discordRPC: false,
customTopBar: false,
autoCheckInfo: false,
addThumbnail: true,
addMetadata: true,
verboseLogs: false,
autoDownloadPlaylist: false,
customCodec: "h264",
},
}));
jest.mock("../../server/logger.js", () => ({
logger: {
info: jest.fn(),
error: jest.fn(),
},
}));
const { buildYtDlpArgs } = require("../../server/helpers/buildArgs.helpers");
const { configFeatures } = require("../../config");
const { logger } = require("../../server/logger");
describe("buildYtDlpArgs", () => {
beforeEach(() => {
jest.clearAllMocks();
});
test("builds default video arguments", () => {
const args = buildYtDlpArgs({
url: "https://youtube.com/watch?v=123",
audioOnly: false,
quality: "best",
outputFolder: "/downloads",
});
expect(args).toContain("--cookies-from-browser");
expect(args).toContain("firefox");
expect(args).toContain("--ffmpeg-location");
expect(args).toContain("/mock/ffmpeg");
expect(args).toContain("--js-runtimes");
expect(args).toContain("deno:/mock/deno");
expect(args).toContain("--merge-output");
expect(args).toContain("mp4");
expect(args).toContain("-f");
expect(args).toContain("bestvideo+bestaudio/best/mp4");
expect(args).toContain(
path.join("/downloads", "%(title)s.%(ext)s")
);
expect(args.at(-1)).toBe("https://youtube.com/watch?v=123");
});
test("builds audio-only arguments", () => {
const args = buildYtDlpArgs({
url: "https://youtube.com/watch?v=123",
audioOnly: true,
quality: "best",
outputFolder: "/downloads",
});
expect(args).toContain("--extract-audio");
expect(args).toContain("--audio-format");
expect(args).toContain("mp3");
expect(args).toContain("bestaudio");
expect(args).not.toContain("--merge-output");
});
test("uses requested quality", () => {
const args = buildYtDlpArgs({
url: "https://youtube.com/watch?v=123",
audioOnly: false,
quality: "720",
outputFolder: "/downloads",
});
expect(args).toContain(
"bestvideo[height<=720]+bestaudio/best[height<=720]/mp4"
);
});
test("falls back to best for unknown quality", () => {
const args = buildYtDlpArgs({
url: "https://youtube.com/watch?v=123",
audioOnly: false,
quality: "unknown",
outputFolder: "/downloads",
});
expect(args).toContain("best");
});
test("uses --no-playlist by default", () => {
const args = buildYtDlpArgs({
url: "https://youtube.com/watch?v=123",
audioOnly: false,
quality: "best",
outputFolder: "/downloads",
});
expect(args).toContain("--no-playlist");
expect(args).not.toContain("--yes-playlist");
});
test("uses --yes-playlist when enabled", () => {
configFeatures.autoDownloadPlaylist = true;
const args = buildYtDlpArgs({
url: "https://youtube.com/watch?v=123",
audioOnly: false,
quality: "best",
outputFolder: "/downloads",
});
expect(args).toContain("--yes-playlist");
expect(args).not.toContain("--no-playlist");
configFeatures.autoDownloadPlaylist = false;
});
test("embeds thumbnail and metadata when enabled", () => {
const args = buildYtDlpArgs({
url: "https://youtube.com/watch?v=123",
audioOnly: false,
quality: "best",
outputFolder: "/downloads",
});
expect(args).toContain("--embed-thumbnail");
expect(args).toContain("--add-metadata");
});
test("logs codec validation", () => {
buildYtDlpArgs({
url: "https://youtube.com/watch?v=123",
audioOnly: false,
quality: "best",
outputFolder: "/downloads",
});
expect(logger.info).toHaveBeenCalledWith("Codec Valid: h264");
});
test("falls back to h264 when codec is invalid", () => {
configFeatures.customCodec = "invalid";
const args = buildYtDlpArgs({
url: "https://youtube.com/watch?v=123",
audioOnly: false,
quality: "best",
outputFolder: "/downloads",
});
expect(logger.error).toHaveBeenCalledWith(
"Codec not valid: invalid. Using default codec"
);
const sortIndex = args.indexOf("-S");
expect(args[sortIndex + 1]).toContain("vcodec:h264");
configFeatures.customCodec = "h264";
});
});

View File

@@ -0,0 +1,133 @@
const mockShow = jest.fn();
const mockOn = jest.fn();
const mockOpenPath = jest.fn();
const mockOpenExternal = jest.fn();
jest.mock("electron", () => ({
Notification: jest.fn().mockImplementation(() => ({
show: mockShow,
on: mockOn,
})),
shell: {
openPath: mockOpenPath,
openExternal: mockOpenExternal,
},
}));
jest.mock("../../server/helpers/path.helpers", () => ({
iconPaths: {
confirm: "/icons/confirm.png",
error: "/icons/error.png",
},
}));
jest.mock("../../server/logger", () => ({
logger: {
info: jest.fn(),
error: jest.fn(),
},
}));
const { Notification, shell } = require("electron");
const {
notifyDownloadFinished,
notifyCookiesBrowserError,
notifyFirefoxBrowserMissing,
} = require("../../server/helpers/notify.helpers");
describe("notify.helpers", () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe("notifyDownloadFinished", () => {
test("does nothing when notifications are disabled", () => {
notifyDownloadFinished("/downloads", false);
expect(Notification).not.toHaveBeenCalled();
});
test("does nothing when folder is missing", () => {
notifyDownloadFinished(undefined, true);
expect(Notification).not.toHaveBeenCalled();
});
test("creates and shows notification", () => {
notifyDownloadFinished("/downloads");
expect(Notification).toHaveBeenCalledWith({
title: "Freedom Loader",
body: "Your download is complete, click here to open it.",
icon: "/icons/confirm.png",
});
expect(mockOn).toHaveBeenCalledWith(
"click",
expect.any(Function)
);
expect(mockShow).toHaveBeenCalled();
});
test("opens folder when notification is clicked", () => {
notifyDownloadFinished("/downloads");
const callback = mockOn.mock.calls[0][1];
callback();
expect(shell.openPath).toHaveBeenCalledWith("/downloads");
});
});
describe("notifyCookiesBrowserError", () => {
test("creates notification", () => {
notifyCookiesBrowserError();
expect(Notification).toHaveBeenCalledWith({
title: "Cookies Error",
body: "Unable to retrieve cookies. Please log in to your browser and click here to view the tutorial.",
icon: "/icons/error.png",
});
expect(mockShow).toHaveBeenCalled();
});
test("opens tutorial when clicked", () => {
notifyCookiesBrowserError();
const callback = mockOn.mock.calls[0][1];
callback();
expect(shell.openExternal).toHaveBeenCalledWith(
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
);
});
});
describe("notifyFirefoxBrowserMissing", () => {
test("creates notification", () => {
notifyFirefoxBrowserMissing();
expect(Notification).toHaveBeenCalledWith({
title: "Firefox Missing",
body: "Firefox was not found on your system. Click here to follow the installation guide",
icon: "/icons/error.png",
});
expect(mockShow).toHaveBeenCalled();
});
test("opens installation guide when clicked", () => {
notifyFirefoxBrowserMissing();
const callback = mockOn.mock.calls[0][1];
callback();
expect(shell.openExternal).toHaveBeenCalledWith(
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
);
});
});
});