mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-27 06:25:41 +00:00
feat(rngit_tool): add RNGit Explorer page and navigation integration
This commit is contained in:
@@ -209,6 +209,14 @@ export default {
|
||||
type: "navigation",
|
||||
route: { name: "rncp" },
|
||||
},
|
||||
{
|
||||
id: "nav-rngit-explorer",
|
||||
title: "nav_rngit_explorer",
|
||||
description: "nav_rngit_explorer_desc",
|
||||
icon: "source-branch",
|
||||
type: "navigation",
|
||||
route: { name: "rngit-explorer" },
|
||||
},
|
||||
{
|
||||
id: "nav-rnstatus",
|
||||
title: "nav_rnstatus",
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
<!-- SPDX-License-Identifier: 0BSD -->
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 h-full min-w-0 overflow-hidden bg-slate-50 dark:bg-zinc-950">
|
||||
<div class="bg-slate-50 dark:bg-zinc-950 border-b border-gray-200 dark:border-zinc-800 shadow-sm z-10">
|
||||
<div class="px-4 py-3 md:px-6 md:py-4 flex flex-wrap items-center justify-between gap-3 min-w-0">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="p-2 bg-teal-100 dark:bg-teal-900/30 rounded-xl shrink-0">
|
||||
<MaterialDesignIcon
|
||||
icon-name="source-branch"
|
||||
class="size-5 md:size-6 text-teal-600 dark:text-teal-300"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h1 class="text-lg md:text-xl font-bold text-gray-900 dark:text-white truncate">
|
||||
{{ $t("tools.rngit_explorer.title") }}
|
||||
</h1>
|
||||
<p class="text-[10px] md:text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||
{{ $t("tools.rngit_explorer.description") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<RouterLink
|
||||
to="/tools"
|
||||
class="inline-flex items-center gap-2 text-sm text-teal-600 dark:text-teal-300 hover:underline shrink-0"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="arrow-left" class="size-4" />
|
||||
{{ $t("rngit_explorer.back_tools") }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto min-w-0">
|
||||
<div class="p-3 sm:p-4 md:p-6 max-w-5xl mx-auto space-y-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 p-4 space-y-2"
|
||||
>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ $t("rngit_explorer.heard_heading") }}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-teal-600 dark:text-teal-400 hover:underline"
|
||||
@click="loadHeardAnnounces"
|
||||
>
|
||||
{{ $t("rngit_explorer.refresh_heard") }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-zinc-500">
|
||||
{{ $t("rngit_explorer.heard_hint") }}
|
||||
</p>
|
||||
<div v-if="heardLoading" class="text-xs text-gray-500 py-2">{{ $t("common.loading") }}</div>
|
||||
<ul
|
||||
v-else-if="heardAnnounces.length"
|
||||
class="max-h-48 overflow-y-auto divide-y divide-gray-100 dark:divide-zinc-800 rounded-lg border border-gray-100 dark:border-zinc-800"
|
||||
>
|
||||
<li
|
||||
v-for="h in heardAnnounces"
|
||||
:key="h.destination_hash"
|
||||
class="px-3 py-2 cursor-pointer hover:bg-gray-50 dark:hover:bg-zinc-900/80 text-sm"
|
||||
:class="pickedHeardHash === h.destination_hash ? 'bg-teal-50/80 dark:bg-teal-950/30' : ''"
|
||||
@click="applyHeardAnnounce(h)"
|
||||
>
|
||||
<div class="font-medium text-gray-900 dark:text-white truncate">
|
||||
{{ heardNodeTitle(h) }}
|
||||
</div>
|
||||
<div class="font-mono text-[11px] text-gray-500 dark:text-zinc-400 truncate">
|
||||
{{ h.destination_hash }}
|
||||
</div>
|
||||
<div class="text-[10px] text-gray-400 dark:text-zinc-500">
|
||||
{{ $t("rngit_explorer.hops_label", { n: h.hops != null ? h.hops : "—" }) }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="text-xs text-gray-500 dark:text-zinc-500 py-1">
|
||||
{{ $t("rngit_explorer.heard_empty") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 p-4 space-y-3"
|
||||
>
|
||||
<p class="text-xs text-gray-600 dark:text-zinc-400 leading-relaxed">
|
||||
{{ $t("rngit_explorer.intro") }}
|
||||
</p>
|
||||
<div class="grid sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="glass-label">{{ $t("rngit_explorer.destination_hash") }}</label>
|
||||
<input
|
||||
v-model="destinationHash"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
class="w-full rounded-lg border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-2 text-sm font-mono text-gray-900 dark:text-white"
|
||||
:disabled="busy"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="glass-label">{{ $t("rngit_explorer.group_name") }}</label>
|
||||
<input
|
||||
v-model="groupName"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
class="w-full rounded-lg border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-2 text-sm font-mono text-gray-900 dark:text-white"
|
||||
:disabled="busy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="glass-label">{{ $t("rngit_explorer.repo_names_label") }}</label>
|
||||
<textarea
|
||||
v-model="repoNamesText"
|
||||
rows="6"
|
||||
class="w-full rounded-lg border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-2 text-sm font-mono text-gray-900 dark:text-white"
|
||||
:disabled="busy"
|
||||
:placeholder="$t('rngit_explorer.repo_names_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
||||
<label
|
||||
class="inline-flex items-start gap-2 text-xs text-gray-600 dark:text-zinc-400 max-w-prose"
|
||||
>
|
||||
<input
|
||||
v-model="forPush"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 shrink-0 mt-0.5"
|
||||
:disabled="busy"
|
||||
/>
|
||||
<span>{{ $t("rngit_explorer.for_push") }}</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-2 px-4 py-2 rounded-lg bg-teal-600 hover:bg-teal-500 text-white text-sm font-medium disabled:opacity-50 shrink-0 self-start sm:self-auto"
|
||||
:disabled="busy || !canProbe"
|
||||
@click="runProbe"
|
||||
>
|
||||
<MaterialDesignIcon v-if="!busy" icon-name="radar" class="size-5" />
|
||||
<MaterialDesignIcon v-else icon-name="loading" class="size-5 animate-spin" />
|
||||
{{ $t("rngit_explorer.probe") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="results.length"
|
||||
class="rounded-lg border border-gray-200 dark:border-zinc-800 overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="px-3 py-2 border-b border-gray-100 dark:border-zinc-800 text-xs font-semibold text-gray-600 dark:text-zinc-400"
|
||||
>
|
||||
{{ $t("rngit_explorer.results") }}
|
||||
</div>
|
||||
<ul class="divide-y divide-gray-100 dark:divide-zinc-800">
|
||||
<li
|
||||
v-for="row in results"
|
||||
:key="row.repository"
|
||||
class="px-3 py-2.5 cursor-pointer transition-colors text-sm"
|
||||
:class="
|
||||
selected && selected.repository === row.repository
|
||||
? 'bg-teal-50 dark:bg-teal-950/40'
|
||||
: 'hover:bg-gray-50 dark:hover:bg-zinc-900/80'
|
||||
"
|
||||
@click="selectRow(row)"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2 min-w-0">
|
||||
<span class="font-mono font-medium text-gray-900 dark:text-white truncate">{{
|
||||
row.repository
|
||||
}}</span>
|
||||
<span
|
||||
class="shrink-0 text-xs font-semibold uppercase"
|
||||
:class="
|
||||
row.reachable
|
||||
? 'text-emerald-600 dark:text-emerald-400'
|
||||
: 'text-gray-400 dark:text-zinc-500'
|
||||
"
|
||||
>
|
||||
{{
|
||||
row.reachable
|
||||
? $t("rngit_explorer.reachable")
|
||||
: $t("rngit_explorer.unreachable")
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!row.reachable && row.error"
|
||||
class="text-xs text-red-600 dark:text-red-400 mt-1 font-mono truncate"
|
||||
>
|
||||
{{ row.error }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selected"
|
||||
class="rounded-lg border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 p-4 space-y-3"
|
||||
>
|
||||
<div class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ $t("rngit_explorer.detail_title", { repo: selected.repository }) }}
|
||||
</div>
|
||||
<div v-if="selected.reachable && selected.clone_command" class="space-y-2">
|
||||
<div class="text-xs text-gray-500 dark:text-zinc-400">
|
||||
{{ $t("rngit_explorer.clone_command") }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 items-start">
|
||||
<pre
|
||||
class="flex-1 min-w-0 p-2 rounded bg-gray-100 dark:bg-zinc-900 text-xs font-mono text-gray-800 dark:text-zinc-200 overflow-x-auto whitespace-pre-wrap break-all"
|
||||
>{{ selected.clone_command }}</pre
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 px-3 py-2 rounded-lg border border-gray-300 dark:border-zinc-600 text-sm text-gray-800 dark:text-zinc-200 hover:bg-gray-50 dark:hover:bg-zinc-800"
|
||||
@click.stop="copyClone"
|
||||
>
|
||||
{{ $t("rngit_explorer.copy") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-xs text-gray-500 dark:text-zinc-400">
|
||||
{{ $t("rngit_explorer.no_clone_hint") }}
|
||||
</div>
|
||||
<div v-if="selected.reachable && selected.refs_preview" class="space-y-1">
|
||||
<div class="text-xs text-gray-500 dark:text-zinc-400">
|
||||
{{ $t("rngit_explorer.refs_preview") }}
|
||||
<span v-if="selected.refs_truncated" class="text-amber-600 dark:text-amber-400">{{
|
||||
$t("rngit_explorer.truncated")
|
||||
}}</span>
|
||||
</div>
|
||||
<pre
|
||||
class="p-2 rounded bg-gray-100 dark:bg-zinc-900 text-xs font-mono text-gray-800 dark:text-zinc-200 max-h-48 overflow-y-auto whitespace-pre-wrap break-all"
|
||||
>{{ selected.refs_preview }}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||
import ToastUtils from "../../js/ToastUtils";
|
||||
import WebSocketConnection from "../../js/WebSocketConnection";
|
||||
|
||||
const RNGIT_ASPECT = "git.repositories";
|
||||
|
||||
export default {
|
||||
name: "RNGitExplorerPage",
|
||||
components: { MaterialDesignIcon },
|
||||
data() {
|
||||
return {
|
||||
heardAnnounces: [],
|
||||
heardLoading: false,
|
||||
pickedHeardHash: "",
|
||||
destinationHash: "",
|
||||
groupName: "",
|
||||
repoNamesText: "",
|
||||
forPush: false,
|
||||
busy: false,
|
||||
results: [],
|
||||
selected: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
normalizedDestinationHash() {
|
||||
return (this.destinationHash || "").trim().toLowerCase().replace(/:/g, "");
|
||||
},
|
||||
canProbe() {
|
||||
const h = this.normalizedDestinationHash;
|
||||
return (
|
||||
h.length === 32 &&
|
||||
/^[0-9a-f]+$/.test(h) &&
|
||||
(this.groupName || "").trim().length > 0 &&
|
||||
(this.repoNamesText || "").trim().length > 0
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
WebSocketConnection.on("message", this.onSocketMessage);
|
||||
this.loadHeardAnnounces();
|
||||
},
|
||||
beforeUnmount() {
|
||||
WebSocketConnection.off("message", this.onSocketMessage);
|
||||
},
|
||||
methods: {
|
||||
onSocketMessage(event) {
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(event.data);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (json.type !== "announce" || !json.announce || json.announce.aspect !== RNGIT_ASPECT) {
|
||||
return;
|
||||
}
|
||||
const a = json.announce;
|
||||
const i = this.heardAnnounces.findIndex((x) => x.destination_hash === a.destination_hash);
|
||||
if (i >= 0) {
|
||||
this.heardAnnounces.splice(i, 1, a);
|
||||
} else {
|
||||
this.heardAnnounces.unshift(a);
|
||||
}
|
||||
},
|
||||
async loadHeardAnnounces() {
|
||||
this.heardLoading = true;
|
||||
try {
|
||||
const { data } = await window.api.get("/api/v1/announces", {
|
||||
params: { aspect: RNGIT_ASPECT, limit: 200 },
|
||||
});
|
||||
this.heardAnnounces = Array.isArray(data.announces) ? data.announces : [];
|
||||
} catch (e) {
|
||||
ToastUtils.error(e.response?.data?.message || this.$t("rngit_explorer.heard_load_failed"));
|
||||
} finally {
|
||||
this.heardLoading = false;
|
||||
}
|
||||
},
|
||||
heardNodeTitle(h) {
|
||||
const custom = (h.custom_display_name || "").trim();
|
||||
if (custom) {
|
||||
return custom;
|
||||
}
|
||||
const d = (h.display_name || "").trim();
|
||||
if (d && d !== "Anonymous Peer") {
|
||||
return d;
|
||||
}
|
||||
const hex = (h.destination_hash || "").trim().toLowerCase();
|
||||
if (hex.length >= 8) {
|
||||
return this.$t("rngit_explorer.heard_title_short", { prefix: hex.slice(0, 8) });
|
||||
}
|
||||
return this.$t("rngit_explorer.unnamed_node");
|
||||
},
|
||||
applyHeardAnnounce(h) {
|
||||
this.pickedHeardHash = h.destination_hash || "";
|
||||
this.destinationHash = h.destination_hash || "";
|
||||
},
|
||||
selectRow(row) {
|
||||
this.selected = { ...row };
|
||||
},
|
||||
async copyClone() {
|
||||
if (!this.selected?.clone_command) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.selected.clone_command);
|
||||
ToastUtils.success(this.$t("rngit_explorer.copied"));
|
||||
} catch {
|
||||
ToastUtils.error(this.$t("rngit_explorer.copy_failed"));
|
||||
}
|
||||
},
|
||||
async runProbe() {
|
||||
if (!this.canProbe) {
|
||||
return;
|
||||
}
|
||||
this.busy = true;
|
||||
this.results = [];
|
||||
this.selected = null;
|
||||
ToastUtils.loading(this.$t("rngit_explorer.probing"), 0, "rngit-explorer-probe");
|
||||
try {
|
||||
const { data } = await window.api.post("/api/v1/rngit-tool/probe", {
|
||||
destination_hash: this.normalizedDestinationHash,
|
||||
group_name: (this.groupName || "").trim(),
|
||||
repository_names_text: this.repoNamesText,
|
||||
for_push: this.forPush,
|
||||
});
|
||||
if (data.ok && Array.isArray(data.results)) {
|
||||
this.results = data.results;
|
||||
const firstOk = data.results.find((r) => r.reachable);
|
||||
this.selected = firstOk ? { ...firstOk } : data.results[0] ? { ...data.results[0] } : null;
|
||||
ToastUtils.success(this.$t("rngit_explorer.probe_done"));
|
||||
} else {
|
||||
ToastUtils.error(data.error || this.$t("rngit_explorer.probe_failed"));
|
||||
}
|
||||
} catch (e) {
|
||||
const d = e.response?.data;
|
||||
ToastUtils.error(d?.error || d?.message || this.$t("rngit_explorer.probe_failed"));
|
||||
} finally {
|
||||
ToastUtils.dismiss("rngit-explorer-probe");
|
||||
this.busy = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -150,6 +150,14 @@ export default {
|
||||
titleKey: "tools.rncp.title",
|
||||
descriptionKey: "tools.rncp.description",
|
||||
},
|
||||
{
|
||||
name: "rngit-explorer",
|
||||
route: { name: "rngit-explorer" },
|
||||
icon: "source-branch",
|
||||
iconBg: "tool-card__icon bg-teal-50 text-teal-600 dark:bg-teal-900/30 dark:text-teal-200",
|
||||
titleKey: "tools.rngit_explorer.title",
|
||||
descriptionKey: "tools.rngit_explorer.description",
|
||||
},
|
||||
{
|
||||
name: "rnstatus",
|
||||
route: { name: "rnstatus" },
|
||||
|
||||
@@ -144,6 +144,11 @@ const router = createRouter({
|
||||
path: "/rncp",
|
||||
component: defineAsyncComponent(() => import("./components/rncp/RNCPPage.vue")),
|
||||
},
|
||||
{
|
||||
name: "rngit-explorer",
|
||||
path: "/tools/rngit-explorer",
|
||||
component: defineAsyncComponent(() => import("./components/tools/RNGitExplorerPage.vue")),
|
||||
},
|
||||
{
|
||||
name: "rnstatus",
|
||||
path: "/rnstatus",
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import RNGitExplorerPage from "@/components/tools/RNGitExplorerPage.vue";
|
||||
import ToastUtils from "@/js/ToastUtils";
|
||||
|
||||
vi.mock("@/js/ToastUtils", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
loading: vi.fn(),
|
||||
dismiss: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("RNGitExplorerPage.vue", () => {
|
||||
let axiosMock;
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [{ path: "/tools", name: "tools", component: { template: "div" } }],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = {
|
||||
post: vi.fn(),
|
||||
get: vi.fn().mockResolvedValue({ data: { announces: [] } }),
|
||||
};
|
||||
window.api = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete window.api;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mountPage = () =>
|
||||
mount(RNGitExplorerPage, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
mocks: { $t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key) },
|
||||
stubs: {
|
||||
MaterialDesignIcon: { template: "<span />", props: ["iconName"] },
|
||||
RouterLink: { template: "<a><slot /></a>", props: ["to"] },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it("posts probe with textarea names", async () => {
|
||||
axiosMock.post.mockResolvedValue({
|
||||
data: {
|
||||
ok: true,
|
||||
results: [
|
||||
{
|
||||
repository: "MeshChatX",
|
||||
reachable: true,
|
||||
refs_preview: "abc\tHEAD",
|
||||
refs_truncated: false,
|
||||
clone_command: "git clone rns://h/quad4/MeshChatX",
|
||||
error: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const wrapper = mountPage();
|
||||
await wrapper.setData({
|
||||
destinationHash: "a".repeat(32),
|
||||
groupName: "quad4",
|
||||
repoNamesText: "MeshChatX",
|
||||
});
|
||||
await wrapper.vm.runProbe();
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
"/api/v1/rngit-tool/probe",
|
||||
expect.objectContaining({
|
||||
destination_hash: "a".repeat(32),
|
||||
group_name: "quad4",
|
||||
repository_names_text: "MeshChatX",
|
||||
for_push: false,
|
||||
})
|
||||
);
|
||||
expect(wrapper.vm.results.length).toBe(1);
|
||||
expect(ToastUtils.success).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("heardNodeTitle prefers custom_display_name then display_name", () => {
|
||||
const wrapper = mountPage();
|
||||
expect(
|
||||
wrapper.vm.heardNodeTitle({
|
||||
custom_display_name: " Mine ",
|
||||
display_name: "Other",
|
||||
destination_hash: "a".repeat(32),
|
||||
})
|
||||
).toBe("Mine");
|
||||
expect(
|
||||
wrapper.vm.heardNodeTitle({
|
||||
custom_display_name: "",
|
||||
display_name: " Bob ",
|
||||
destination_hash: "b".repeat(32),
|
||||
})
|
||||
).toBe("Bob");
|
||||
});
|
||||
|
||||
it("heardNodeTitle uses short hash when display is Anonymous Peer", () => {
|
||||
const wrapper = mountPage();
|
||||
expect(
|
||||
wrapper.vm.heardNodeTitle({
|
||||
display_name: "Anonymous Peer",
|
||||
destination_hash: "926baefe13daf5178c174f158dae1b45",
|
||||
})
|
||||
).toBe('rngit_explorer.heard_title_short:{"prefix":"926baefe"}');
|
||||
});
|
||||
|
||||
it("separates for-push checkbox row from probe button", () => {
|
||||
const wrapper = mountPage();
|
||||
const row = wrapper.find(".flex.flex-col.gap-3.sm\\:flex-row");
|
||||
expect(row.exists()).toBe(true);
|
||||
expect(row.find('input[type="checkbox"]').exists()).toBe(true);
|
||||
expect(row.find("button").exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ describe("ToolsPage.vue", () => {
|
||||
{ path: "/ping", name: "ping", component: { template: "div" } },
|
||||
{ path: "/rnprobe", name: "rnprobe", component: { template: "div" } },
|
||||
{ path: "/rncp", name: "rncp", component: { template: "div" } },
|
||||
{ path: "/tools/rngit-explorer", name: "rngit-explorer", component: { template: "div" } },
|
||||
{ path: "/rnstatus", name: "rnstatus", component: { template: "div" } },
|
||||
{ path: "/rnpath", name: "rnpath", component: { template: "div" } },
|
||||
{ path: "/rnpath-trace", name: "rnpath-trace", component: { template: "div" } },
|
||||
@@ -55,7 +56,7 @@ describe("ToolsPage.vue", () => {
|
||||
it("renders all tool rows", () => {
|
||||
const wrapper = mountToolsPage();
|
||||
const toolRows = wrapper.findAll(".tool-row");
|
||||
expect(toolRows.length).toBe(21);
|
||||
expect(toolRows.length).toBe(22);
|
||||
});
|
||||
|
||||
it("filters tools based on search query", async () => {
|
||||
@@ -80,6 +81,6 @@ describe("ToolsPage.vue", () => {
|
||||
await clearButton.trigger("click");
|
||||
|
||||
expect(wrapper.vm.searchQuery).toBe("");
|
||||
expect(wrapper.vm.filteredTools.length).toBe(21);
|
||||
expect(wrapper.vm.filteredTools.length).toBe(22);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user