From ead906477f1aa4f4056caa63dc033b0df5a0e832 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 11 Sep 2026 11:47:40 +0800 Subject: [PATCH] test(tabs): align cache assertions with bounded keepalive --- .../__tests__/EditorGroup.mount.spec.ts | 57 ++++++++++++++----- .../dataGridTabSwitchScrollRestore.test.ts | 25 ++------ 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/apps/desktop/src/components/layout/__tests__/EditorGroup.mount.spec.ts b/apps/desktop/src/components/layout/__tests__/EditorGroup.mount.spec.ts index 9eeb342d38..74e7217f8e 100644 --- a/apps/desktop/src/components/layout/__tests__/EditorGroup.mount.spec.ts +++ b/apps/desktop/src/components/layout/__tests__/EditorGroup.mount.spec.ts @@ -5,8 +5,8 @@ import { createI18n } from "vue-i18n"; import { beforeEach, describe, expect, it, vi } from "vitest"; const lifecycle = vi.hoisted(() => ({ - queryCreated: 0, - queryUnmounted: 0, + surfaceCreated: 0, + surfaceUnmounted: 0, })); vi.mock("@/components/layout/EditorGroupTabBar.vue", () => ({ @@ -29,10 +29,10 @@ vi.mock("@/components/layout/QueryEditorSurface.vue", () => ({ name: "QueryEditorSurfaceStub", props: ["activeTab", "autoFocus"], created() { - lifecycle.queryCreated += 1; + lifecycle.surfaceCreated += 1; }, unmounted() { - lifecycle.queryUnmounted += 1; + lifecycle.surfaceUnmounted += 1; }, template: `
{{ activeTab.id }}
`, }, @@ -41,6 +41,12 @@ vi.mock("@/components/layout/QueryEditorSurface.vue", () => ({ vi.mock("@/components/layout/ContentArea.vue", () => ({ default: { name: "ContentAreaStub", + created() { + lifecycle.surfaceCreated += 1; + }, + unmounted() { + lifecycle.surfaceUnmounted += 1; + }, template: `
`, }, })); @@ -54,14 +60,14 @@ import EditorGroup from "../EditorGroup.vue"; import { createNoopEditorToolbarActions, EDITOR_TOOLBAR_ACTIONS, type EditorToolbarActions } from "../editorToolbarActions"; import { useQueryStore } from "@/stores/queryStore"; -function tab(id: string) { +function tab(id: string, mode: "query" | "data" = "query") { return { id, title: id, connectionId: "conn-1", database: "db", sql: "SELECT 1", - mode: "query", + mode, } as const; } @@ -77,8 +83,8 @@ describe("EditorGroup mount contract", () => { beforeEach(() => { document.body.innerHTML = ""; - lifecycle.queryCreated = 0; - lifecycle.queryUnmounted = 0; + lifecycle.surfaceCreated = 0; + lifecycle.surfaceUnmounted = 0; pinia = createPinia(); setActivePinia(pinia); i18n = createI18n({ @@ -88,9 +94,10 @@ describe("EditorGroup mount contract", () => { }); }); - it("keeps a bounded hot surface cache while switching query tabs", async () => { + it.each(["query", "data"] as const)("reuses hot %s tabs and remounts evicted surfaces", async (mode) => { const store = useQueryStore(); - store.tabs = [tab("tab-a"), tab("tab-b")]; + const tabIds = ["tab-a", "tab-b", "tab-c", "tab-d"]; + store.tabs = tabIds.map((tabId) => tab(tabId, mode)); const activeTabId = ref("tab-a"); const host = createHost(); const root = defineComponent({ @@ -98,9 +105,9 @@ describe("EditorGroup mount contract", () => { return () => h(EditorGroup, { groupId: "group-1", - tabIds: ["tab-a", "tab-b"], + tabIds, activeTabId: activeTabId.value, - activeTab: tab(activeTabId.value), + activeTab: tab(activeTabId.value, mode), activeConnection: undefined, executableSql: "SELECT 1", activeOutputView: "result", @@ -123,11 +130,31 @@ describe("EditorGroup mount contract", () => { activeTabId.value = "tab-a"; await nextTick(); - expect(lifecycle.queryCreated).toBe(2); - expect(lifecycle.queryUnmounted).toBe(0); + expect(lifecycle.surfaceCreated).toBe(2); + expect(lifecycle.surfaceUnmounted).toBe(0); + + activeTabId.value = "tab-c"; + await nextTick(); + expect(lifecycle.surfaceCreated).toBe(3); + expect(lifecycle.surfaceUnmounted).toBe(0); + + activeTabId.value = "tab-d"; + await nextTick(); + expect(lifecycle.surfaceCreated).toBe(4); + expect(lifecycle.surfaceUnmounted).toBe(1); + + activeTabId.value = "tab-a"; + await nextTick(); + expect(lifecycle.surfaceCreated).toBe(4); + expect(lifecycle.surfaceUnmounted).toBe(1); + + activeTabId.value = "tab-b"; + await nextTick(); + expect(lifecycle.surfaceCreated).toBe(5); + expect(lifecycle.surfaceUnmounted).toBe(2); app.unmount(); - expect(lifecycle.queryUnmounted).toBe(2); + expect(lifecycle.surfaceUnmounted).toBe(5); host.remove(); }); diff --git a/packages/app-tests/dataGridTabSwitchScrollRestore.test.ts b/packages/app-tests/dataGridTabSwitchScrollRestore.test.ts index 92259d1fb3..b4e25b7b9e 100644 --- a/packages/app-tests/dataGridTabSwitchScrollRestore.test.ts +++ b/packages/app-tests/dataGridTabSwitchScrollRestore.test.ts @@ -3,14 +3,6 @@ import { readFileSync } from "node:fs"; import path from "node:path"; import { test } from "vitest"; -/** - * Data tabs render a single active pane (`ContentArea` keyed by `activeTab.id`, - * no KeepAlive), so returning to a tab remounts the grid from scratch. Scroll was - * always snapshotted, but the restore gate threw away scroll-only snapshots — the - * exact read-only-browse case — because it assumed a KeepAlive activate path that - * does not exist. The gate now also honours snapshots written by a tab switch - * (#8524) while a reloaded or re-executed result still starts at row 1 (#7341). - */ function readSource(relativePath: string): string { return readFileSync(path.resolve(relativePath), "utf8"); } @@ -20,11 +12,7 @@ function editorSource(): string { } test("the restore gate honours a tab-switch scroll snapshot", () => { - assert.match( - editorSource(), - /pendingScrollRestore = snapshotHasEditState \|\| cached\.scrollFromTabSwitch === true \? cached\.scroll : undefined;/, - "the scroll-only gate must accept snapshots carrying tab-switch provenance", - ); + assert.match(editorSource(), /pendingScrollRestore = snapshotHasEditState \|\| cached\.scrollFromTabSwitch === true \? cached\.scroll : undefined;/, "the scroll-only gate must accept snapshots carrying tab-switch provenance"); }); test("the tab-switch listener only claims its own tab", () => { @@ -51,10 +39,9 @@ test("an explicit scroll-to-top intent drops the provenance", () => { assert.match(source, /pendingScrollRestore = undefined;\s*\n\s*clearTabSwitchScrollProvenance\(\);/); }); -test("data tab panes stay un-cached so the fix cannot be masked by KeepAlive", () => { - // Introducing KeepAlive here would retain every grid instance in memory; the - // snapshot path is the deliberate alternative. Pin that decision. - for (const file of ["apps/desktop/src/components/layout/EditorGroup.vue", "apps/desktop/src/components/layout/ContentArea.vue"]) { - assert.doesNotMatch(readSource(file), / { + const editorGroup = readSource("apps/desktop/src/components/layout/EditorGroup.vue"); + assert.match(editorGroup, /const HOT_TAB_SURFACE_CACHE_SIZE = 3;/); + assert.match(editorGroup, /]*:max="HOT_TAB_SURFACE_CACHE_SIZE"/); + assert.doesNotMatch(readSource("apps/desktop/src/components/layout/ContentArea.vue"), /