lots of fixes, changes, styling, fixing outbound calls, rnode-flasher.
Some checks failed
CI / test-backend (push) Successful in 4s
CI / build-frontend (push) Successful in 1m49s
CI / test-lang (push) Successful in 1m47s
CI / test-backend (pull_request) Successful in 24s
Build and Publish Docker Image / build (pull_request) Has been skipped
CI / test-lang (pull_request) Successful in 52s
OSV-Scanner PR Scan / scan-pr (pull_request) Successful in 24s
CI / lint (push) Failing after 5m14s
CI / lint (pull_request) Failing after 5m8s
Tests / test (push) Failing after 9m17s
CI / build-frontend (pull_request) Successful in 9m48s
Benchmarks / benchmark (push) Successful in 14m52s
Benchmarks / benchmark (pull_request) Successful in 15m9s
Build and Publish Docker Image / build-dev (pull_request) Successful in 13m47s
Tests / test (pull_request) Failing after 25m50s
Build Test / Build and Test (pull_request) Successful in 53m37s
Build Test / Build and Test (push) Successful in 56m30s

This commit is contained in:
2026-01-04 15:57:49 -06:00
parent f3ec20b14e
commit c4674992e0
34 changed files with 6540 additions and 286 deletions

View File

@@ -5,7 +5,6 @@ import json
from unittest.mock import MagicMock, patch, AsyncMock
from meshchatx.meshchat import ReticulumMeshChat
import RNS
import asyncio
@pytest.fixture

View File

@@ -14,7 +14,7 @@ def temp_dir(tmp_path):
def mock_rns_minimal():
with (
patch("RNS.Reticulum") as mock_rns,
patch("RNS.Transport"),
patch("RNS.Transport") as mock_transport,
patch("LXMF.LXMRouter"),
patch("meshchatx.meshchat.get_file_path", return_value="/tmp/mock_path"),
):
@@ -23,6 +23,13 @@ def mock_rns_minimal():
mock_rns_instance.is_connected_to_shared_instance = False
mock_rns_instance.transport_enabled.return_value = True
# Setup RNS.Transport mock constants and tables
mock_transport.path_table = {}
mock_transport.path_states = {}
mock_transport.STATE_UNKNOWN = 0
mock_transport.STATE_RESPONSIVE = 1
mock_transport.STATE_UNRESPONSIVE = 2
# Path management mocks
mock_rns_instance.get_path_table.return_value = []
mock_rns_instance.get_rate_table.return_value = []

View File

@@ -122,7 +122,8 @@ describe("ChangelogModal.vue", () => {
await wrapper.vm.show();
await wrapper.vm.$nextTick();
const closeBtn = wrapper.find(".v-btn");
const closeBtn = wrapper.find("button.v-btn");
expect(closeBtn.exists()).toBe(true);
expect(closeBtn.attributes("class")).toContain("dark:hover:bg-white/10");
expect(closeBtn.attributes("class")).toContain("hover:bg-black/5");
});

View File

@@ -1,11 +1,13 @@
import { mount } from "@vue/test-utils";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import ConversationViewer from "@/components/messages/ConversationViewer.vue";
import WebSocketConnection from "@/js/WebSocketConnection";
describe("ConversationViewer.vue", () => {
let axiosMock;
beforeEach(() => {
WebSocketConnection.connect();
axiosMock = {
get: vi.fn().mockImplementation((url) => {
if (url.includes("/path")) return Promise.resolve({ data: { path: [] } });
@@ -44,6 +46,7 @@ describe("ConversationViewer.vue", () => {
afterEach(() => {
delete window.axios;
vi.unstubAllGlobals();
WebSocketConnection.destroy();
});
const mountConversationViewer = (props = {}) => {

View File

@@ -32,11 +32,16 @@ describe("DocsPage.vue", () => {
});
afterEach(() => {
delete window.axios;
if (wrapper) {
wrapper.unmount();
}
// Do not delete window.axios, as it might be used by async operations
// and it is globally defined in setup.js anyway.
});
let wrapper;
const mountDocsPage = () => {
return mount(DocsPage, {
wrapper = mount(DocsPage, {
global: {
directives: {
"click-outside": vi.fn(),
@@ -50,6 +55,7 @@ describe("DocsPage.vue", () => {
},
},
});
return wrapper;
};
it("renders download button when no docs are present", async () => {

View File

@@ -62,8 +62,9 @@ describe("MicronEditorPage.vue", () => {
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick(); // Wait for loadContent
expect(wrapper.vm.tabs.length).toBe(1);
expect(wrapper.vm.tabs.length).toBe(2);
expect(wrapper.vm.tabs[0].name).toBe("tools.micron_editor.main_tab");
expect(wrapper.vm.tabs[1].name).toBe("tools.micron_editor.guide_tab");
expect(wrapper.text()).toContain("tools.micron_editor.title");
});
@@ -88,8 +89,7 @@ describe("MicronEditorPage.vue", () => {
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
// Add a second tab so we can remove one (close button only shows if tabs.length > 1)
await wrapper.vm.addTab();
// Already have 2 tabs (Main + Guide)
expect(wrapper.vm.tabs.length).toBe(2);
// Find close button on the second tab
@@ -105,14 +105,14 @@ describe("MicronEditorPage.vue", () => {
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.addTab();
expect(wrapper.vm.activeTabIndex).toBe(1);
// Click first tab
const tabs = wrapper.findAll(".group.flex.items-center");
await tabs[0].trigger("click");
// Initially on first tab
expect(wrapper.vm.activeTabIndex).toBe(0);
// Click second tab (Guide)
const tabs = wrapper.findAll(".group.flex.items-center");
await tabs[1].trigger("click");
expect(wrapper.vm.activeTabIndex).toBe(1);
});
it("resets all tabs when clicking reset button", async () => {
@@ -120,8 +120,9 @@ describe("MicronEditorPage.vue", () => {
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
const initialTabCount = wrapper.vm.tabs.length;
await wrapper.vm.addTab();
expect(wrapper.vm.tabs.length).toBe(2);
expect(wrapper.vm.tabs.length).toBe(initialTabCount + 1);
// Find reset button
const resetButton = wrapper.find('.mdi-stub[data-icon-name="refresh"]').element.parentElement;
@@ -129,7 +130,7 @@ describe("MicronEditorPage.vue", () => {
expect(window.confirm).toHaveBeenCalled();
expect(micronStorage.clearAll).toHaveBeenCalled();
expect(wrapper.vm.tabs.length).toBe(1);
expect(wrapper.vm.tabs.length).toBe(2); // Resets to Main + Guide
expect(wrapper.vm.activeTabIndex).toBe(0);
});

View File

@@ -4,29 +4,42 @@ import Toast from "@/components/Toast.vue";
import GlobalEmitter from "@/js/GlobalEmitter";
describe("Toast.vue", () => {
let wrapper;
beforeEach(() => {
vi.useFakeTimers();
wrapper = mount(Toast, {
global: {
stubs: {
TransitionGroup: { template: "<div><slot /></div>" },
MaterialDesignIcon: {
name: "MaterialDesignIcon",
template: '<div class="mdi-stub"></div>',
props: ["iconName"],
},
},
},
});
});
afterEach(() => {
if (wrapper) {
wrapper.unmount();
}
vi.useRealTimers();
// Clear all listeners from GlobalEmitter to avoid test pollution
GlobalEmitter.off("toast");
});
it("adds a toast when GlobalEmitter emits 'toast'", async () => {
const wrapper = mount(Toast);
GlobalEmitter.emit("toast", { message: "Test Message", type: "success" });
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain("Test Message");
expect(wrapper.findComponent({ name: "MaterialDesignIcon" }).props("iconName")).toBe("check-circle");
const icon = wrapper.findComponent({ name: "MaterialDesignIcon" });
expect(icon.exists()).toBe(true);
expect(icon.props("iconName")).toBe("check-circle");
});
it("removes a toast after duration", async () => {
const wrapper = mount(Toast);
GlobalEmitter.emit("toast", { message: "Test Message", duration: 1000 });
await wrapper.vm.$nextTick();
@@ -39,8 +52,6 @@ describe("Toast.vue", () => {
});
it("removes a toast when clicking the close button", async () => {
const wrapper = mount(Toast);
GlobalEmitter.emit("toast", { message: "Test Message", duration: 0 });
await wrapper.vm.$nextTick();
@@ -48,13 +59,12 @@ describe("Toast.vue", () => {
const closeButton = wrapper.find("button");
await closeButton.trigger("click");
await wrapper.vm.$nextTick();
expect(wrapper.text()).not.toContain("Test Message");
});
it("assigns correct classes for different toast types", async () => {
const wrapper = mount(Toast);
GlobalEmitter.emit("toast", { message: "Success", type: "success" });
GlobalEmitter.emit("toast", { message: "Error", type: "error" });
await wrapper.vm.$nextTick();

83
tests/frontend/setup.js Normal file
View File

@@ -0,0 +1,83 @@
import { vi } from "vitest";
import { config } from "@vue/test-utils";
// Global mocks
global.performance.mark = vi.fn();
global.performance.measure = vi.fn();
global.performance.getEntriesByName = vi.fn(() => []);
global.performance.clearMarks = vi.fn();
global.performance.clearMeasures = vi.fn();
// Mock window.axios by default to prevent TypeErrors
global.axios = {
get: vi.fn().mockResolvedValue({ data: {} }),
post: vi.fn().mockResolvedValue({ data: {} }),
put: vi.fn().mockResolvedValue({ data: {} }),
patch: vi.fn().mockResolvedValue({ data: {} }),
delete: vi.fn().mockResolvedValue({ data: {} }),
};
window.axios = global.axios;
// Stub all Vuetify components to avoid warnings and CSS issues
config.global.stubs = {
MaterialDesignIcon: { template: '<div class="mdi-stub"><slot /></div>' },
RouterLink: { template: "<a><slot /></a>" },
RouterView: { template: "<div><slot /></div>" },
// Common Vuetify components
"v-app": true,
"v-main": true,
"v-container": true,
"v-row": true,
"v-col": true,
"v-btn": true,
"v-icon": true,
"v-card": true,
"v-card-title": true,
"v-card-text": true,
"v-card-actions": true,
"v-dialog": true,
"v-text-field": true,
"v-textarea": true,
"v-select": true,
"v-switch": true,
"v-checkbox": true,
"v-list": true,
"v-list-item": true,
"v-list-item-title": true,
"v-list-item-subtitle": true,
"v-menu": true,
"v-divider": true,
"v-spacer": true,
"v-progress-circular": true,
"v-progress-linear": true,
"v-tabs": true,
"v-tab": true,
"v-window": true,
"v-window-item": true,
"v-expansion-panels": true,
"v-expansion-panel": true,
"v-expansion-panel-title": true,
"v-expansion-panel-text": true,
"v-chip": true,
"v-toolbar": true,
"v-toolbar-title": true,
"v-tooltip": true,
"v-alert": true,
"v-snackbar": true,
"v-badge": true,
};
// Mock window.matchMedia
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});