fix(mqtt): resolve console labels at render time

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-08-11 21:36:10 +08:00
1 parent 801a96a003
commit 8d084cc655
9 files changed
+296 -3

No files matched your search

@@ -369,7 +369,7 @@ onUnmounted(stopPolling);
</label>
<label class="flex items-center gap-2 text-xs font-medium">
<input v-model="formNoLocal" type="checkbox" :disabled="savingSubscription || !mqtt5" />
<span>{{ t("connection.mqttNoLocal") }}(No Local)</span>
<span>{{ t("connection.mqttNoLocal") }}</span>
<span v-if="!mqtt5" class="font-normal text-muted-foreground">{{ t("connection.mqttNoLocalMqtt5Only") }}</span>
</label>
<label class="flex items-start gap-2 text-xs font-medium">
@@ -0,0 +1,21 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const source = readFileSync(new URL("../MqttAdminConsole.vue", import.meta.url), "utf8");
describe("MqttAdminConsole i18n regression guards", () => {
it("does not append a hardcoded (No Local) suffix to the No Local label", () => {
// Issue #5858 follow-up: the subscription dialog used to render
// `t("connection.mqttNoLocal")(No Local)`. The CJK-bracket suffix was
// locale-invariant and leaked Chinese punctuation into every language.
expect(source).not.toContain(")No Local)");
expect(source).not.toContain("(No Local)");
});
it("renders the No Local label purely through vue-i18n", () => {
// The en locale value for connection.mqttNoLocal is already "No Local", so
// dropping the hardcoded suffix loses no information. Guard the intent.
expect(source).toContain('t("connection.mqttNoLocal")');
expect(source).not.toContain('t("connection.mqttNoLocal")(No Local)');
});
});
@@ -350,6 +350,7 @@ function displayLabel(node: TreeNode): string {
if (node.type === "object-browser") return t(node.label, { count: node.objectCount ?? 0 });
if (node.type === "user-admin" || node.type === "dameng-job-admin") return t(node.label);
if (node.type === "linked-server-root") return t(node.label);
if (node.type === "mqtt-topic" && node.id.endsWith(":mqtt-topic:__console__")) return t(node.label);
if (node.label === "tree.defaultDatabase") return t(node.label);
return isGroupLabel(node) ? t(node.label) : node.label;
}
@@ -0,0 +1,118 @@
// @vitest-environment happy-dom
import { createApp, defineComponent, h, nextTick, type App } from "vue";
import { afterEach, describe, expect, it, vi } from "vitest";
import i18n, { setLocale } from "@/i18n";
import TreeItem from "@/components/sidebar/TreeItem.vue";
import { createSidebarTreeRuntime, sidebarTreeRuntimeKey, type SidebarTreeRuntimeHost } from "@/lib/sidebar/sidebarTreeRuntime";
import type { TreeNode } from "@/types/database";
const connectionStore = {
activeConnectionId: "mqtt-1",
connectedIds: new Set(["mqtt-1"]),
connectingIds: new Set<string>(),
connectionMultiSelectActive: false,
connections: [],
getConfig: () => ({ id: "mqtt-1", name: "test-mqtt", db_type: "mqtt" }),
isDefaultDatabase: () => false,
isDefaultSchema: () => false,
isPinnedTreeNodeReorderTarget: () => false,
isTreeNodeChildrenLoaded: () => false,
isTreeNodePinned: () => false,
selectedTreeNodeId: null as string | null,
selectedTreeNodeIds: [] as string[],
selectedTreeNodeIdsSet: new Set<string>(),
sidebarTableSearchQueries: {},
tableNameFilterForScope: () => undefined,
treeNodes: [],
treeSelectionAnchorId: null as string | null,
};
vi.mock("@/stores/connectionStore", () => ({
useConnectionStore: () => connectionStore,
}));
vi.mock("@/stores/queryStore", () => ({
useQueryStore: () => ({ openDatabaseKeys: new Set<string>() }),
}));
vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => ({
editorSettings: {
shortcuts: { openDataInNewTab: "" },
sidebarActivation: "double",
sidebarAllowHorizontalScroll: false,
sidebarHiddenTablePrefixes: [],
sidebarObjectInfoMode: "none",
},
}),
}));
vi.mock("@/composables/useToast", () => ({
useToast: () => ({ toast: vi.fn() }),
}));
const mountedApps: App[] = [];
function runtimeHost(): SidebarTreeRuntimeHost {
return {
buildContextMenu: vi.fn(() => []),
handleRowClick: vi.fn(),
handleRowDoubleClick: vi.fn(),
handleRowKeydown: vi.fn(),
openPrimaryVisibleFilter: vi.fn(),
openDataInNewTab: vi.fn(),
requestPaste: vi.fn(() => false),
toggleNode: vi.fn(),
};
}
async function mountMqttConsoleNode() {
const container = document.createElement("div");
document.body.append(container);
const node: TreeNode = {
id: "mqtt-1:mqtt-topic:__console__",
label: "connection.mqttConsoleTitle",
type: "mqtt-topic",
connectionId: "mqtt-1",
children: [],
};
const runtime = createSidebarTreeRuntime();
runtime.bindHost(runtimeHost());
const app = createApp(
defineComponent({
setup: () => () => h(TreeItem, { node, depth: 1 }),
}),
);
mountedApps.push(app);
app.use(i18n);
app.provide(sidebarTreeRuntimeKey, runtime);
app.mount(container);
await nextTick();
const row = container.querySelector<HTMLElement>("[tabindex]");
if (!row) throw new Error(`MQTT console row was not rendered: ${container.innerHTML}`);
return row;
}
afterEach(async () => {
for (const app of mountedApps.splice(0)) app.unmount();
document.body.replaceChildren();
await setLocale("en");
vi.restoreAllMocks();
});
describe("TreeItem MQTT console i18n", () => {
it("updates the existing synthetic node when the locale changes", async () => {
await setLocale("en");
const row = await mountMqttConsoleNode();
expect(row.textContent).toContain("MQTT Console");
await setLocale("zh-CN");
await nextTick();
expect(row.textContent).toContain("MQTT 控制台");
expect(row.textContent).not.toContain("MQTT Console");
});
});