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"), /