fix(sync): sync cloud-sync parent checkbox with selection count

This commit is contained in:
Rendegou
2026-10-01 23:58:20 +08:00
committed by GitHub
parent 7410fb8547
commit 046ae4cfb4
2 changed files with 74 additions and 2 deletions
@@ -341,7 +341,7 @@ function confirmSelection() {
<div v-if="catalog" class="min-h-0 flex-1 space-y-2 overflow-y-auto px-5 py-4">
<details open class="border-b pb-2">
<summary class="flex cursor-pointer list-none items-center gap-2 py-2 text-sm font-medium">
<input :checked="allSelected('connections', catalog.connections)" type="checkbox" class="size-4 accent-primary" @click.stop.prevent="toggleAll('connections', catalog.connections)" />
<input :checked="allSelected('connections', catalog.connections)" :disabled="!catalog.connections.length" type="checkbox" class="size-4 accent-primary" @click.stop @change="toggleAll('connections', catalog.connections)" />
<span>{{ t("settings.syncSelectionConnections") }}</span>
<span class="ml-auto text-xs text-muted-foreground">{{ selection.connections?.length ?? 0 }}/{{ catalog.connections.length }}</span>
</summary>
@@ -362,7 +362,7 @@ function confirmSelection() {
<details open class="border-b pb-2">
<summary class="flex cursor-pointer list-none items-center gap-2 py-2 text-sm font-medium">
<input :checked="allSelected('tunnelProfiles', catalog.tunnelProfiles)" type="checkbox" class="size-4 accent-primary" @click.stop.prevent="toggleAll('tunnelProfiles', catalog.tunnelProfiles)" />
<input :checked="allSelected('tunnelProfiles', catalog.tunnelProfiles)" :disabled="!catalog.tunnelProfiles.length" type="checkbox" class="size-4 accent-primary" @click.stop @change="toggleAll('tunnelProfiles', catalog.tunnelProfiles)" />
<span>{{ t("settings.syncSelectionTunnels") }}</span>
<span class="ml-auto text-xs text-muted-foreground">{{ selection.tunnelProfiles?.length ?? 0 }}/{{ catalog.tunnelProfiles.length }}</span>
</summary>
@@ -109,3 +109,75 @@ describe.each([
expect(root.textContent).not.toContain(actionLabel);
});
});
describe.each(["restore", "upload"] as const)("CloudSyncSelectionDialog %s category checkboxes", (mode) => {
it.each(["connections", "tunnelProfiles"] as const)("disables the %s parent checkbox when the category is empty", async (category) => {
root = document.createElement("div");
document.body.append(root);
app = createApp(CloudSyncSelectionDialog, {
open: true,
mode,
catalog: { ...catalog, [category]: [] },
});
app.mount(root);
await nextTick();
const section = root.querySelectorAll("details")[category === "connections" ? 0 : 1];
const parent = section.querySelector<HTMLInputElement>("summary input")!;
expect(parent.disabled).toBe(true);
parent.click();
await nextTick();
expect(parent.checked).toBe(false);
expect(section.textContent).toContain("0/0");
expect(section.open).toBe(true);
});
it.each(["connections", "tunnelProfiles"] as const)("keeps the %s parent checkbox in sync after select-all clicks", async (category) => {
const items = Array.from({ length: 5 }, (_, index) => ({ id: `item-${index}`, label: `Item ${index}` }));
root = document.createElement("div");
document.body.append(root);
app = createApp(CloudSyncSelectionDialog, {
open: true,
mode,
catalog: { ...catalog, [category]: items },
});
app.mount(root);
await nextTick();
const section = root.querySelectorAll("details")[category === "connections" ? 0 : 1];
const parent = section.querySelector<HTMLInputElement>("summary input")!;
const children = Array.from(section.querySelectorAll<HTMLInputElement>("label input"));
expect(parent.checked).toBe(true);
let clickEvent: MouseEvent | undefined;
parent.addEventListener("click", (event) => {
clickEvent = event;
});
parent.click();
await nextTick();
// Canceling native checkbox activation rolls back checked in browsers.
// happy-dom does not emulate that rollback, so also inspect the event.
expect(clickEvent?.defaultPrevented).toBe(false);
expect(section.textContent).toContain("0/5");
expect(parent.checked).toBe(false);
expect(children.every((child) => !child.checked)).toBe(true);
expect(section.open).toBe(true);
parent.click();
await nextTick();
expect(section.textContent).toContain("5/5");
expect(parent.checked).toBe(true);
expect(children.every((child) => child.checked)).toBe(true);
children[0].click();
await nextTick();
expect(section.textContent).toContain("4/5");
expect(parent.checked).toBe(false);
parent.click();
await nextTick();
expect(section.textContent).toContain("5/5");
expect(parent.checked).toBe(true);
expect(children.every((child) => child.checked)).toBe(true);
});
});