mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-03-31 19:05:47 +00:00
Cleanup
Some checks failed
Build Test / Build and Test (push) Failing after 6m1s
CI / lint (push) Failing after 1m27s
CI / build-frontend (push) Failing after 1m30s
CI / test-backend (push) Successful in 1m17s
CI / test-lang (push) Failing after 1m15s
Build and Publish Docker Image / build (push) Failing after 4m28s
Build and Publish Docker Image / build-dev (push) Failing after 5m38s
Security Scans / scan (push) Successful in 2m12s
Tests / test (push) Failing after 1m17s
Some checks failed
Build Test / Build and Test (push) Failing after 6m1s
CI / lint (push) Failing after 1m27s
CI / build-frontend (push) Failing after 1m30s
CI / test-backend (push) Successful in 1m17s
CI / test-lang (push) Failing after 1m15s
Build and Publish Docker Image / build (push) Failing after 4m28s
Build and Publish Docker Image / build-dev (push) Failing after 5m38s
Security Scans / scan (push) Successful in 2m12s
Tests / test (push) Failing after 1m17s
This commit is contained in:
61
README.md
61
README.md
@@ -45,6 +45,7 @@ docker compose up -d
|
||||
```
|
||||
|
||||
### Docker Permissions Note
|
||||
|
||||
If you encounter a `PermissionError` when running the Docker container, it's likely because the container's user (UID 1000) doesn't have permission to write to your host's `./meshchat-config` folder. You can fix this by running:
|
||||
|
||||
```bash
|
||||
@@ -246,36 +247,36 @@ MeshChatX can be configured via command-line arguments or environment variables.
|
||||
|
||||
We use [Task](https://taskfile.dev/) for automation.
|
||||
|
||||
| Task | Description |
|
||||
| :----------------------------- | :--------------------------------------------- |
|
||||
| `task install` | Install all dependencies |
|
||||
| `task run` | Run the application |
|
||||
| `task dev` | Run the application in development mode |
|
||||
| `task lint:all` | Run all linters (Python & Frontend) |
|
||||
| `task lint:be` | Lint Python code only |
|
||||
| `task lint:fe` | Lint frontend code only |
|
||||
| `task fmt:all` | Format all code (Python & Frontend) |
|
||||
| `task fmt:be` | Format Python code only |
|
||||
| `task fmt:fe` | Format frontend code only |
|
||||
| `task test:all` | Run all tests |
|
||||
| `task test:cov` | Run tests with coverage reports |
|
||||
| `task test:be` | Run Python tests only |
|
||||
| `task test:fe` | Run frontend tests only |
|
||||
| `task build:all` | Build frontend and backend |
|
||||
| `task build:fe` | Build only the frontend |
|
||||
| `task build:wheel` | Build Python wheel package |
|
||||
| `task compile` | Compile Python code to check for syntax errors |
|
||||
| `task docker:build` | Build Docker image using buildx |
|
||||
| `task docker:run` | Run Docker container using docker-compose |
|
||||
| `task dist:linux:appimage` | Build Linux AppImage |
|
||||
| `task dist:win:exe` | Build Windows portable executable |
|
||||
| `task dist:win:wine` | Build Windows portable (Wine cross-build) |
|
||||
| `task dist:all` | Build all Electron apps |
|
||||
| `task dist:all:wine` | Build all Electron apps (Wine cross-build) |
|
||||
| `task android:prepare` | Prepare Android build |
|
||||
| `task android:build` | Build Android APK |
|
||||
| `task dist:fe:flatpak` | Build Flatpak package |
|
||||
| `task clean` | Clean build artifacts and dependencies |
|
||||
| Task | Description |
|
||||
| :------------------------- | :--------------------------------------------- |
|
||||
| `task install` | Install all dependencies |
|
||||
| `task run` | Run the application |
|
||||
| `task dev` | Run the application in development mode |
|
||||
| `task lint:all` | Run all linters (Python & Frontend) |
|
||||
| `task lint:be` | Lint Python code only |
|
||||
| `task lint:fe` | Lint frontend code only |
|
||||
| `task fmt:all` | Format all code (Python & Frontend) |
|
||||
| `task fmt:be` | Format Python code only |
|
||||
| `task fmt:fe` | Format frontend code only |
|
||||
| `task test:all` | Run all tests |
|
||||
| `task test:cov` | Run tests with coverage reports |
|
||||
| `task test:be` | Run Python tests only |
|
||||
| `task test:fe` | Run frontend tests only |
|
||||
| `task build:all` | Build frontend and backend |
|
||||
| `task build:fe` | Build only the frontend |
|
||||
| `task build:wheel` | Build Python wheel package |
|
||||
| `task compile` | Compile Python code to check for syntax errors |
|
||||
| `task docker:build` | Build Docker image using buildx |
|
||||
| `task docker:run` | Run Docker container using docker-compose |
|
||||
| `task dist:linux:appimage` | Build Linux AppImage |
|
||||
| `task dist:win:exe` | Build Windows portable executable |
|
||||
| `task dist:win:wine` | Build Windows portable (Wine cross-build) |
|
||||
| `task dist:all` | Build all Electron apps |
|
||||
| `task dist:all:wine` | Build all Electron apps (Wine cross-build) |
|
||||
| `task android:prepare` | Prepare Android build |
|
||||
| `task android:build` | Build Android APK |
|
||||
| `task dist:fe:flatpak` | Build Flatpak package |
|
||||
| `task clean` | Clean build artifacts and dependencies |
|
||||
|
||||
## Security
|
||||
|
||||
|
||||
@@ -1095,8 +1095,7 @@ export default {
|
||||
const status = this.propagationNodeStatus?.state;
|
||||
const messagesReceived = this.propagationNodeStatus?.messages_received ?? 0;
|
||||
const messagesStored = this.propagationNodeStatus?.messages_stored ?? 0;
|
||||
const deliveryConfirmations =
|
||||
this.propagationNodeStatus?.delivery_confirmations ?? 0;
|
||||
const deliveryConfirmations = this.propagationNodeStatus?.delivery_confirmations ?? 0;
|
||||
const messagesHidden = this.propagationNodeStatus?.messages_hidden ?? 0;
|
||||
if (status === "complete" || status === "idle") {
|
||||
const base = this.$t("app.sync_complete", { count: messagesReceived });
|
||||
|
||||
@@ -38,7 +38,9 @@
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 space-y-2 overflow-hidden">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<div class="text-lg font-semibold text-gray-900 dark:text-white truncate min-w-0">{{ iface._name }}</div>
|
||||
<div class="text-lg font-semibold text-gray-900 dark:text-white truncate min-w-0">
|
||||
{{ iface._name }}
|
||||
</div>
|
||||
<span class="type-chip shrink-0">{{ iface.type }}</span>
|
||||
<span :class="statusChipClass" class="shrink-0">{{
|
||||
isInterfaceEnabled(iface) ? $t("app.enabled") : $t("app.disabled")
|
||||
@@ -48,34 +50,40 @@
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300 break-words min-w-0">
|
||||
{{ description }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 text-xs text-gray-600 dark:text-gray-300">
|
||||
<span v-if="iface._stats?.bitrate" class="stat-chip"
|
||||
>{{ $t("interface.bitrate") }} {{ formatBitsPerSecond(iface._stats?.bitrate ?? 0) }}</span
|
||||
>
|
||||
<span class="stat-chip">{{ $t("interface.tx") }} {{ formatBytes(iface._stats?.txb ?? 0) }}</span>
|
||||
<span class="stat-chip">{{ $t("interface.rx") }} {{ formatBytes(iface._stats?.rxb ?? 0) }}</span>
|
||||
<span v-if="iface.type === 'RNodeInterface' && iface._stats?.noise_floor" class="stat-chip"
|
||||
>{{ $t("interface.noise") }} {{ iface._stats?.noise_floor }} dBm</span
|
||||
>
|
||||
<span v-if="iface._stats?.clients != null" class="stat-chip"
|
||||
>{{ $t("interface.clients") }} {{ iface._stats?.clients }}</span
|
||||
>
|
||||
</div>
|
||||
<div v-if="iface._stats?.ifac_signature" class="ifac-line">
|
||||
<span class="text-emerald-500 font-semibold">{{ iface._stats.ifac_size * 8 }}-bit IFAC</span>
|
||||
<span v-if="iface._stats?.ifac_netname">• {{ iface._stats.ifac_netname }}</span>
|
||||
<span>•</span>
|
||||
<button
|
||||
type="button"
|
||||
class="text-blue-500 hover:underline"
|
||||
@click="onIFACSignatureClick(iface._stats.ifac_signature)"
|
||||
>
|
||||
{{ iface._stats.ifac_signature.slice(0, 8) }}…{{ iface._stats.ifac_signature.slice(-8) }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 text-xs text-gray-600 dark:text-gray-300">
|
||||
<span v-if="iface._stats?.bitrate" class="stat-chip"
|
||||
>{{ $t("interface.bitrate") }} {{ formatBitsPerSecond(iface._stats?.bitrate ?? 0) }}</span
|
||||
>
|
||||
<span class="stat-chip"
|
||||
>{{ $t("interface.tx") }} {{ formatBytes(iface._stats?.txb ?? 0) }}</span
|
||||
>
|
||||
<span class="stat-chip"
|
||||
>{{ $t("interface.rx") }} {{ formatBytes(iface._stats?.rxb ?? 0) }}</span
|
||||
>
|
||||
<span v-if="iface.type === 'RNodeInterface' && iface._stats?.noise_floor" class="stat-chip"
|
||||
>{{ $t("interface.noise") }} {{ iface._stats?.noise_floor }} dBm</span
|
||||
>
|
||||
<span v-if="iface._stats?.clients != null" class="stat-chip"
|
||||
>{{ $t("interface.clients") }} {{ iface._stats?.clients }}</span
|
||||
>
|
||||
</div>
|
||||
<div v-if="iface._stats?.ifac_signature" class="ifac-line">
|
||||
<span class="text-emerald-500 font-semibold">{{ iface._stats.ifac_size * 8 }}-bit IFAC</span>
|
||||
<span v-if="iface._stats?.ifac_netname">• {{ iface._stats.ifac_netname }}</span>
|
||||
<span>•</span>
|
||||
<button
|
||||
type="button"
|
||||
class="text-blue-500 hover:underline"
|
||||
@click="onIFACSignatureClick(iface._stats.ifac_signature)"
|
||||
>
|
||||
{{ iface._stats.ifac_signature.slice(0, 8) }}…{{ iface._stats.ifac_signature.slice(-8) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row gap-2 sm:items-center items-end sm:shrink-0 justify-end sm:justify-end relative">
|
||||
<div
|
||||
class="flex flex-col sm:flex-row gap-2 sm:items-center items-end sm:shrink-0 justify-end sm:justify-end relative"
|
||||
>
|
||||
<button
|
||||
v-if="isInterfaceEnabled(iface)"
|
||||
type="button"
|
||||
|
||||
@@ -210,8 +210,8 @@ def test_identity_loading_fallback(mock_rns, temp_dir):
|
||||
mock_gen_id = MagicMock()
|
||||
mock_gen_id.hash.hex.return_value = "generated_hash"
|
||||
mock_gen_id.get_private_key.return_value = b"private_key"
|
||||
mock_id_class.side_effect = (
|
||||
lambda create_keys=False: mock_gen_id if create_keys else MagicMock()
|
||||
mock_id_class.side_effect = lambda create_keys=False: (
|
||||
mock_gen_id if create_keys else MagicMock()
|
||||
)
|
||||
|
||||
# Mock sys.argv to use default behavior (random generation)
|
||||
|
||||
@@ -82,7 +82,7 @@ describe("App propagation sync metrics", () => {
|
||||
vi.advanceTimersByTime(600);
|
||||
|
||||
expect(ToastUtils.success).toHaveBeenCalledWith(
|
||||
"Sync complete. 8 messages received. (3 stored, 2 confirmations, 3 hidden)",
|
||||
"Sync complete. 8 messages received. (3 stored, 2 confirmations, 3 hidden)"
|
||||
);
|
||||
expect(ToastUtils.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -2,13 +2,15 @@ import { mount, flushPromises } from "@vue/test-utils";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import BlockedPage from "../../meshchatx/src/frontend/components/blocked/BlockedPage.vue";
|
||||
|
||||
vi.mock("../../meshchatx/src/frontend/js/DialogUtils", () => ({ default: { confirm: vi.fn().mockResolvedValue(true) } }));
|
||||
vi.mock("../../meshchatx/src/frontend/js/DialogUtils", () => ({
|
||||
default: { confirm: vi.fn().mockResolvedValue(true) },
|
||||
}));
|
||||
vi.mock("../../meshchatx/src/frontend/js/ToastUtils", () => ({ default: { success: vi.fn(), error: vi.fn() } }));
|
||||
vi.mock("../../meshchatx/src/frontend/js/Utils", () => ({
|
||||
default: { formatTimeAgo: (d) => "1h ago" },
|
||||
}));
|
||||
|
||||
const MaterialDesignIcon = { template: "<div class=\"mdi\"></div>", props: ["iconName"] };
|
||||
const MaterialDesignIcon = { template: '<div class="mdi"></div>', props: ["iconName"] };
|
||||
|
||||
function mountBlockedPage() {
|
||||
return mount(BlockedPage, {
|
||||
@@ -23,10 +25,8 @@ describe("BlockedPage UI", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
global.axios.get = vi.fn().mockImplementation((url) => {
|
||||
if (url === "/api/v1/blocked-destinations")
|
||||
return Promise.resolve({ data: { blocked_destinations: [] } });
|
||||
if (url === "/api/v1/reticulum/blackhole")
|
||||
return Promise.resolve({ data: { blackholed_identities: {} } });
|
||||
if (url === "/api/v1/blocked-destinations") return Promise.resolve({ data: { blocked_destinations: [] } });
|
||||
if (url === "/api/v1/reticulum/blackhole") return Promise.resolve({ data: { blackholed_identities: {} } });
|
||||
return Promise.resolve({ data: {} });
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,7 @@ describe("BlockedPage UI", () => {
|
||||
it("renders search input and refresh button", async () => {
|
||||
const wrapper = mountBlockedPage();
|
||||
await flushPromises();
|
||||
expect(wrapper.find("input[type=\"text\"]").exists()).toBe(true);
|
||||
expect(wrapper.find('input[type="text"]').exists()).toBe(true);
|
||||
expect(wrapper.find("button").exists()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -56,8 +56,7 @@ describe("BlockedPage UI", () => {
|
||||
global.axios.get = vi.fn().mockImplementation((url, opts) => {
|
||||
if (url === "/api/v1/blocked-destinations")
|
||||
return Promise.resolve({ data: { blocked_destinations: ["abc123"] } });
|
||||
if (url === "/api/v1/reticulum/blackhole")
|
||||
return Promise.resolve({ data: { blackholed_identities: {} } });
|
||||
if (url === "/api/v1/reticulum/blackhole") return Promise.resolve({ data: { blackholed_identities: {} } });
|
||||
if (url === "/api/v1/announces" && opts?.params?.identity_hash === "abc123")
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
@@ -82,7 +81,7 @@ describe("BlockedPage UI", () => {
|
||||
it("search input binds to searchQuery", async () => {
|
||||
const wrapper = mountBlockedPage();
|
||||
await flushPromises();
|
||||
const input = wrapper.find("input[type=\"text\"]");
|
||||
const input = wrapper.find('input[type="text"]');
|
||||
await input.setValue("test");
|
||||
expect(wrapper.vm.searchQuery).toBe("test");
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ vi.mock("../../meshchatx/src/frontend/js/GlobalEmitter", () => ({
|
||||
|
||||
import GlobalEmitter from "../../meshchatx/src/frontend/js/GlobalEmitter";
|
||||
|
||||
const MaterialDesignIcon = { template: "<div class=\"mdi\"></div>", props: ["iconName"] };
|
||||
const MaterialDesignIcon = { template: '<div class="mdi"></div>', props: ["iconName"] };
|
||||
|
||||
function mountDialog() {
|
||||
return mount(ConfirmDialog, {
|
||||
|
||||
@@ -5,8 +5,8 @@ import DropDownMenu from "../../meshchatx/src/frontend/components/DropDownMenu.v
|
||||
function mountDropDown(slots = {}) {
|
||||
return mount(DropDownMenu, {
|
||||
slots: {
|
||||
button: "<button type=\"button\">Menu</button>",
|
||||
items: "<div class=\"menu-item\">Item 1</div>",
|
||||
button: '<button type="button">Menu</button>',
|
||||
items: '<div class="menu-item">Item 1</div>',
|
||||
...slots,
|
||||
},
|
||||
global: {
|
||||
@@ -38,7 +38,7 @@ describe("DropDownMenu UI", () => {
|
||||
});
|
||||
|
||||
it("renders items slot when open", async () => {
|
||||
const wrapper = mountDropDown({ items: "<div class=\"custom-item\">Custom</div>" });
|
||||
const wrapper = mountDropDown({ items: '<div class="custom-item">Custom</div>' });
|
||||
await wrapper.find("button").trigger("click");
|
||||
expect(wrapper.find(".custom-item").exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain("Custom");
|
||||
|
||||
@@ -4,13 +4,13 @@ import IconButton from "../../meshchatx/src/frontend/components/IconButton.vue";
|
||||
|
||||
function mountIconButton(slots = {}) {
|
||||
return mount(IconButton, {
|
||||
slots: { default: slots.default ?? "<span class=\"icon\">X</span>" },
|
||||
slots: { default: slots.default ?? '<span class="icon">X</span>' },
|
||||
});
|
||||
}
|
||||
|
||||
describe("IconButton UI", () => {
|
||||
it("renders button with slot content", () => {
|
||||
const wrapper = mountIconButton({ default: "<span class=\"icon\">+</span>" });
|
||||
const wrapper = mountIconButton({ default: '<span class="icon">+</span>' });
|
||||
expect(wrapper.find("button").exists()).toBe(true);
|
||||
expect(wrapper.find(".icon").exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain("+");
|
||||
|
||||
@@ -34,8 +34,8 @@ vi.mock("../../meshchatx/src/frontend/js/GlobalEmitter", () => ({
|
||||
default: { on: vi.fn(), off: vi.fn(), emit: vi.fn() },
|
||||
}));
|
||||
|
||||
const MaterialDesignIcon = { template: "<div class=\"mdi\"></div>", props: ["iconName"] };
|
||||
const LxmfUserIcon = { template: "<div class=\"lxmf-icon\"></div>" };
|
||||
const MaterialDesignIcon = { template: '<div class="mdi"></div>', props: ["iconName"] };
|
||||
const LxmfUserIcon = { template: '<div class="lxmf-icon"></div>' };
|
||||
|
||||
function makePropagationNode(i) {
|
||||
return {
|
||||
@@ -107,7 +107,9 @@ describe("Load time with prefilled data", () => {
|
||||
expect(wrapper.vm.paginatedNodes.length).toBe(20);
|
||||
expect(loadMs).toBeLessThan(MAX_PROP_NODES_MS);
|
||||
if (process.env.CI !== "true") {
|
||||
console.log(`Propagation nodes: ${count} nodes loaded in ${loadMs.toFixed(0)}ms (max ${MAX_PROP_NODES_MS}ms)`);
|
||||
console.log(
|
||||
`Propagation nodes: ${count} nodes loaded in ${loadMs.toFixed(0)}ms (max ${MAX_PROP_NODES_MS}ms)`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -148,7 +150,9 @@ describe("Load time with prefilled data", () => {
|
||||
expect(wrapper.vm.displayedConversations.length).toBe(count);
|
||||
expect(end - start).toBeLessThan(MAX_MESSAGES_ANNOUNCES_MS);
|
||||
if (process.env.CI !== "true") {
|
||||
console.log(`Messages: ${count} conversations in ${(end - start).toFixed(0)}ms (max ${MAX_MESSAGES_ANNOUNCES_MS}ms)`);
|
||||
console.log(
|
||||
`Messages: ${count} conversations in ${(end - start).toFixed(0)}ms (max ${MAX_MESSAGES_ANNOUNCES_MS}ms)`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -190,7 +194,9 @@ describe("Load time with prefilled data", () => {
|
||||
expect(wrapper.vm.searchedPeers.length).toBe(count);
|
||||
expect(end - start).toBeLessThan(MAX_MESSAGES_ANNOUNCES_MS);
|
||||
if (process.env.CI !== "true") {
|
||||
console.log(`Announces (messages): ${count} peers in ${(end - start).toFixed(0)}ms (max ${MAX_MESSAGES_ANNOUNCES_MS}ms)`);
|
||||
console.log(
|
||||
`Announces (messages): ${count} peers in ${(end - start).toFixed(0)}ms (max ${MAX_MESSAGES_ANNOUNCES_MS}ms)`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -220,7 +226,7 @@ describe("Load time with prefilled data", () => {
|
||||
components: {
|
||||
MaterialDesignIcon,
|
||||
IconButton: { template: "<button></button>" },
|
||||
DropDownMenu: { template: "<div><slot name=\"button\"/><slot name=\"items\"/></div>" },
|
||||
DropDownMenu: { template: '<div><slot name="button"/><slot name="items"/></div>' },
|
||||
DropDownMenuItem: { template: "<div></div>" },
|
||||
},
|
||||
mocks: { $t: (key) => key },
|
||||
@@ -233,7 +239,9 @@ describe("Load time with prefilled data", () => {
|
||||
expect(wrapper.vm.searchedNodes.length).toBe(count);
|
||||
expect(end - start).toBeLessThan(MAX_NOMADNET_NODES_MS);
|
||||
if (process.env.CI !== "true") {
|
||||
console.log(`NomadNet nodes: ${count} nodes in ${(end - start).toFixed(0)}ms (max ${MAX_NOMADNET_NODES_MS}ms)`);
|
||||
console.log(
|
||||
`NomadNet nodes: ${count} nodes in ${(end - start).toFixed(0)}ms (max ${MAX_NOMADNET_NODES_MS}ms)`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,8 +20,8 @@ vi.mock("../../meshchatx/src/frontend/js/Utils", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const MaterialDesignIcon = { template: "<div class=\"mdi\"></div>", props: ["iconName"] };
|
||||
const LxmfUserIcon = { template: "<div class=\"lxmf-icon\"></div>" };
|
||||
const MaterialDesignIcon = { template: '<div class="mdi"></div>', props: ["iconName"] };
|
||||
const LxmfUserIcon = { template: '<div class="lxmf-icon"></div>' };
|
||||
|
||||
function defaultProps(overrides = {}) {
|
||||
return {
|
||||
@@ -89,7 +89,9 @@ describe("MessagesSidebar UI", () => {
|
||||
await announcesTab.trigger("click");
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.vm.tab).toBe("announces");
|
||||
expect(wrapper.text()).toMatch(/messages\.search_placeholder_announces|messages\.no_peers_discovered|messages\.waiting_for_announce/);
|
||||
expect(wrapper.text()).toMatch(
|
||||
/messages\.search_placeholder_announces|messages\.no_peers_discovered|messages\.waiting_for_announce/
|
||||
);
|
||||
});
|
||||
|
||||
it("emits folder-click when All Messages is clicked", async () => {
|
||||
@@ -154,7 +156,7 @@ describe("MessagesSidebar UI", () => {
|
||||
],
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
const selectionBtn = wrapper.find("button[title=\"Selection Mode\"]");
|
||||
const selectionBtn = wrapper.find('button[title="Selection Mode"]');
|
||||
expect(selectionBtn.exists()).toBe(true);
|
||||
await selectionBtn.trigger("click");
|
||||
await wrapper.vm.$nextTick();
|
||||
@@ -182,6 +184,9 @@ describe("MessagesSidebar UI", () => {
|
||||
const row = wrapper.find(".conversation-item");
|
||||
await row.trigger("click");
|
||||
expect(wrapper.emitted("conversation-click")).toBeTruthy();
|
||||
expect(wrapper.emitted("conversation-click")[0][0]).toMatchObject({ destination_hash: "dest1", display_name: "Bob" });
|
||||
expect(wrapper.emitted("conversation-click")[0][0]).toMatchObject({
|
||||
destination_hash: "dest1",
|
||||
display_name: "Bob",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ vi.mock("../../meshchatx/src/frontend/js/Utils", () => ({
|
||||
default: { formatTimeAgo: (d) => "1h ago" },
|
||||
}));
|
||||
|
||||
const MaterialDesignIcon = { template: "<div class=\"mdi\"></div>", props: ["iconName"] };
|
||||
const MaterialDesignIcon = { template: '<div class="mdi"></div>', props: ["iconName"] };
|
||||
|
||||
function mountBell(options = {}) {
|
||||
return mount(NotificationBell, {
|
||||
|
||||
@@ -5,8 +5,7 @@ import SidebarLink from "../../meshchatx/src/frontend/components/SidebarLink.vue
|
||||
const RouterLinkStub = {
|
||||
name: "RouterLinkStub",
|
||||
props: ["to"],
|
||||
template:
|
||||
'<a href="#" @click.prevent><slot :href="\'#\'" :navigate="navigate" :isActive="false"/></a>',
|
||||
template: '<a href="#" @click.prevent><slot :href="\'#\'" :navigate="navigate" :isActive="false"/></a>',
|
||||
methods: {
|
||||
navigate(e) {
|
||||
if (e) e.preventDefault();
|
||||
@@ -18,7 +17,7 @@ function mountSidebarLink(props = {}, slots = {}) {
|
||||
return mount(SidebarLink, {
|
||||
props: { to: { name: "messages" }, ...props },
|
||||
slots: {
|
||||
icon: "<span class=\"icon-slot\">I</span>",
|
||||
icon: '<span class="icon-slot">I</span>',
|
||||
text: "Messages",
|
||||
...slots,
|
||||
},
|
||||
|
||||
@@ -243,51 +243,51 @@ describe("NetworkVisualiser Optimization and Abort", () => {
|
||||
expect(end - start).toBeLessThan(100); // Should be very fast
|
||||
});
|
||||
|
||||
it("performance: icon cache hit vs miss for 500 nodes", async () => {
|
||||
vi.spyOn(NetworkVisualiser.methods, "init").mockImplementation(() => {});
|
||||
const wrapper = mountVisualiser();
|
||||
it("performance: icon cache hit vs miss for 500 nodes", async () => {
|
||||
vi.spyOn(NetworkVisualiser.methods, "init").mockImplementation(() => {});
|
||||
const wrapper = mountVisualiser();
|
||||
|
||||
// Setup 500 nodes with the same icon
|
||||
const iconInfo = { icon_name: "test", foreground_colour: "#000", background_colour: "#fff" };
|
||||
wrapper.vm.pathTable = Array.from({ length: 500 }, (_, i) => ({ hash: `h${i}`, interface: "eth0", hops: 1 }));
|
||||
wrapper.vm.announces = wrapper.vm.pathTable.reduce((acc, cur) => {
|
||||
acc[cur.hash] = {
|
||||
destination_hash: cur.hash,
|
||||
aspect: "lxmf.delivery",
|
||||
display_name: "node",
|
||||
lxmf_user_icon: iconInfo,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
wrapper.vm.conversations = wrapper.vm.pathTable.reduce((acc, cur) => {
|
||||
acc[cur.hash] = { lxmf_user_icon: iconInfo };
|
||||
return acc;
|
||||
}, {});
|
||||
// Setup 500 nodes with the same icon
|
||||
const iconInfo = { icon_name: "test", foreground_colour: "#000", background_colour: "#fff" };
|
||||
wrapper.vm.pathTable = Array.from({ length: 500 }, (_, i) => ({ hash: `h${i}`, interface: "eth0", hops: 1 }));
|
||||
wrapper.vm.announces = wrapper.vm.pathTable.reduce((acc, cur) => {
|
||||
acc[cur.hash] = {
|
||||
destination_hash: cur.hash,
|
||||
aspect: "lxmf.delivery",
|
||||
display_name: "node",
|
||||
lxmf_user_icon: iconInfo,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
wrapper.vm.conversations = wrapper.vm.pathTable.reduce((acc, cur) => {
|
||||
acc[cur.hash] = { lxmf_user_icon: iconInfo };
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Mock createIconImage to have some delay for the "miss" case
|
||||
wrapper.vm.createIconImage = vi.fn().mockImplementation(async () => {
|
||||
// Add a tiny delay to ensure "miss" is always measurable
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
return "blob:mock-icon";
|
||||
});
|
||||
|
||||
const startMiss = performance.now();
|
||||
await wrapper.vm.processVisualization();
|
||||
const endMiss = performance.now();
|
||||
const missTime = endMiss - startMiss;
|
||||
|
||||
// Second run will hit the cache check in processVisualization
|
||||
// so it won't even call createIconImage.
|
||||
const startHit = performance.now();
|
||||
await wrapper.vm.processVisualization();
|
||||
const endHit = performance.now();
|
||||
const hitTime = endHit - startHit;
|
||||
|
||||
console.log(`Icon cache MISS for 500 nodes: ${missTime.toFixed(2)}ms`);
|
||||
console.log(`Icon cache HIT for 500 nodes: ${hitTime.toFixed(2)}ms`);
|
||||
|
||||
// Cache hit should be significantly faster, but we allow for some
|
||||
// environmental noise in CI environments.
|
||||
expect(hitTime).toBeLessThan(missTime + 200);
|
||||
// Mock createIconImage to have some delay for the "miss" case
|
||||
wrapper.vm.createIconImage = vi.fn().mockImplementation(async () => {
|
||||
// Add a tiny delay to ensure "miss" is always measurable
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
return "blob:mock-icon";
|
||||
});
|
||||
|
||||
const startMiss = performance.now();
|
||||
await wrapper.vm.processVisualization();
|
||||
const endMiss = performance.now();
|
||||
const missTime = endMiss - startMiss;
|
||||
|
||||
// Second run will hit the cache check in processVisualization
|
||||
// so it won't even call createIconImage.
|
||||
const startHit = performance.now();
|
||||
await wrapper.vm.processVisualization();
|
||||
const endHit = performance.now();
|
||||
const hitTime = endHit - startHit;
|
||||
|
||||
console.log(`Icon cache MISS for 500 nodes: ${missTime.toFixed(2)}ms`);
|
||||
console.log(`Icon cache HIT for 500 nodes: ${hitTime.toFixed(2)}ms`);
|
||||
|
||||
// Cache hit should be significantly faster, but we allow for some
|
||||
// environmental noise in CI environments.
|
||||
expect(hitTime).toBeLessThan(missTime + 200);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user