mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(browser): preserve database browser scroll position
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { computed, nextTick, onActivated, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { ArrowDown, ArrowUp, Check, Copy, Database, GripVertical, LayoutGrid, List, Loader2, RefreshCw, Search, TerminalSquare, X } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -55,6 +55,8 @@ interface DatabaseBrowserTabUiState {
|
||||
sortKey?: DatabaseBrowserSortKey;
|
||||
sortDirection?: SortDirection;
|
||||
columnWidths?: Partial<Record<DatabaseBrowserColumnKey, number>>;
|
||||
scrollTop?: number;
|
||||
scrollLeft?: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -82,13 +84,27 @@ const columnWidths = ref<Record<DatabaseBrowserColumnKey, number>>(
|
||||
}),
|
||||
) as Record<DatabaseBrowserColumnKey, number>,
|
||||
);
|
||||
const scrollTop = ref(typeof restoredUiState.scrollTop === "number" && Number.isFinite(restoredUiState.scrollTop) ? Math.max(0, restoredUiState.scrollTop) : 0);
|
||||
const scrollLeft = ref(typeof restoredUiState.scrollLeft === "number" && Number.isFinite(restoredUiState.scrollLeft) ? Math.max(0, restoredUiState.scrollLeft) : 0);
|
||||
const listScrollerRef = ref<HTMLElement | null>(null);
|
||||
const listOuterRef = ref<HTMLElement | null>(null);
|
||||
const gridScrollerRef = ref<HTMLElement | null>(null);
|
||||
let stopColumnResize: (() => void) | null = null;
|
||||
let refreshGeneration = 0;
|
||||
let restoreScrollFrame = 0;
|
||||
let isRestoringScroll = false;
|
||||
const sidebarRuntimeHost = ref<InstanceType<typeof SidebarTreeRuntimeHost> | null>(null);
|
||||
const sidebarRuntimeNode: TreeNode = { id: "__database-browser-runtime__", label: "", type: "connection-group" };
|
||||
const sidebarDialogController = ref<Record<string, any> | null>(null);
|
||||
|
||||
trackUiState(() => ({ search: search.value, sortKey: sortKey.value, sortDirection: sortDirection.value, columnWidths: columnWidths.value }));
|
||||
trackUiState(() => ({
|
||||
search: search.value,
|
||||
sortKey: sortKey.value,
|
||||
sortDirection: sortDirection.value,
|
||||
columnWidths: columnWidths.value,
|
||||
scrollTop: scrollTop.value,
|
||||
scrollLeft: scrollLeft.value,
|
||||
}));
|
||||
|
||||
const hasSize = computed(() => rows.value.some((row) => row.sizeBytes != null));
|
||||
const hasCreatedAt = computed(() => rows.value.some((row) => !!row.createdAt?.trim()));
|
||||
@@ -233,6 +249,7 @@ function toggleSort(key: DatabaseBrowserSortKey) {
|
||||
|
||||
function setViewMode(mode: "list" | "grid") {
|
||||
settingsStore.updateEditorSettings({ objectBrowserViewMode: mode });
|
||||
scrollToTop();
|
||||
}
|
||||
|
||||
function minimumColumnWidth(key: DatabaseBrowserColumnKey): number {
|
||||
@@ -267,6 +284,88 @@ function resetColumnWidth(key: DatabaseBrowserColumnKey, width: number, event: M
|
||||
columnWidths.value = { ...columnWidths.value, [key]: width };
|
||||
}
|
||||
|
||||
function onListVerticalScroll(event: Event) {
|
||||
if (isRestoringScroll) return;
|
||||
const target = event.currentTarget;
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
scrollTop.value = Math.max(0, Math.round(target.scrollTop));
|
||||
if (target.scrollLeft > 0) {
|
||||
scrollLeft.value = Math.max(0, Math.round(target.scrollLeft));
|
||||
}
|
||||
}
|
||||
|
||||
function onListHorizontalScroll(event: Event) {
|
||||
if (isRestoringScroll) return;
|
||||
const target = event.currentTarget;
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
scrollLeft.value = Math.max(0, Math.round(target.scrollLeft));
|
||||
}
|
||||
|
||||
function onGridScroll(event: Event) {
|
||||
if (isRestoringScroll) return;
|
||||
const target = event.currentTarget;
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
scrollTop.value = Math.max(0, Math.round(target.scrollTop));
|
||||
scrollLeft.value = Math.max(0, Math.round(target.scrollLeft));
|
||||
}
|
||||
|
||||
function applyScrollPosition() {
|
||||
const targetTop = scrollTop.value;
|
||||
const targetLeft = scrollLeft.value;
|
||||
if (!targetTop && !targetLeft) return;
|
||||
if (isListView.value) {
|
||||
if (listScrollerRef.value) {
|
||||
if (targetTop) listScrollerRef.value.scrollTop = targetTop;
|
||||
if (targetLeft) listScrollerRef.value.scrollLeft = targetLeft;
|
||||
}
|
||||
if (listOuterRef.value && targetLeft) {
|
||||
listOuterRef.value.scrollLeft = targetLeft;
|
||||
}
|
||||
} else if (gridScrollerRef.value) {
|
||||
if (targetTop) gridScrollerRef.value.scrollTop = targetTop;
|
||||
if (targetLeft) gridScrollerRef.value.scrollLeft = targetLeft;
|
||||
}
|
||||
}
|
||||
|
||||
function restoreScrollPosition() {
|
||||
const targetTop = scrollTop.value;
|
||||
const targetLeft = scrollLeft.value;
|
||||
if (!targetTop && !targetLeft) return;
|
||||
if (restoreScrollFrame) window.cancelAnimationFrame(restoreScrollFrame);
|
||||
isRestoringScroll = true;
|
||||
void nextTick(() => {
|
||||
applyScrollPosition();
|
||||
restoreScrollFrame = window.requestAnimationFrame(() => {
|
||||
restoreScrollFrame = 0;
|
||||
applyScrollPosition();
|
||||
window.requestAnimationFrame(() => {
|
||||
isRestoringScroll = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function scrollToTop() {
|
||||
if (restoreScrollFrame) {
|
||||
window.cancelAnimationFrame(restoreScrollFrame);
|
||||
restoreScrollFrame = 0;
|
||||
}
|
||||
isRestoringScroll = false;
|
||||
scrollTop.value = 0;
|
||||
scrollLeft.value = 0;
|
||||
if (listScrollerRef.value) {
|
||||
listScrollerRef.value.scrollTop = 0;
|
||||
listScrollerRef.value.scrollLeft = 0;
|
||||
}
|
||||
if (listOuterRef.value) {
|
||||
listOuterRef.value.scrollLeft = 0;
|
||||
}
|
||||
if (gridScrollerRef.value) {
|
||||
gridScrollerRef.value.scrollTop = 0;
|
||||
gridScrollerRef.value.scrollLeft = 0;
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh(): Promise<boolean> {
|
||||
const generation = ++refreshGeneration;
|
||||
loading.value = true;
|
||||
@@ -291,6 +390,7 @@ async function refresh(): Promise<boolean> {
|
||||
defaultCollation: database.default_collation ?? null,
|
||||
}));
|
||||
loading.value = false;
|
||||
restoreScrollPosition();
|
||||
|
||||
// Name enumeration is cheap and should render immediately. Enrich the
|
||||
// rows separately because MySQL metadata calculates sizes by aggregating
|
||||
@@ -333,6 +433,7 @@ function focusSearch(): boolean {
|
||||
|
||||
function clearDatabaseSearch() {
|
||||
search.value = "";
|
||||
scrollToTop();
|
||||
searchInput.value?.$el?.focus();
|
||||
}
|
||||
|
||||
@@ -343,12 +444,33 @@ watch(sortKeyOptions, (options) => {
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
[listScrollerRef, gridScrollerRef, isListView],
|
||||
() => {
|
||||
restoreScrollPosition();
|
||||
},
|
||||
{ flush: "post" },
|
||||
);
|
||||
|
||||
watch(search, (next, prev) => {
|
||||
if (next !== prev) {
|
||||
scrollToTop();
|
||||
}
|
||||
});
|
||||
|
||||
watch([sortKey, sortDirection], () => {
|
||||
scrollToTop();
|
||||
});
|
||||
|
||||
let initialConnectionWatch = true;
|
||||
watch(
|
||||
() => props.connection.id,
|
||||
() => {
|
||||
if (initialConnectionWatch) initialConnectionWatch = false;
|
||||
else search.value = "";
|
||||
else {
|
||||
search.value = "";
|
||||
scrollToTop();
|
||||
}
|
||||
void refresh();
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -358,7 +480,14 @@ useDatabaseBrowserMutation(({ connectionId }) => {
|
||||
if (connectionId === props.connection.id) void refresh();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => stopColumnResize?.());
|
||||
onActivated(() => {
|
||||
restoreScrollPosition();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopColumnResize?.();
|
||||
if (restoreScrollFrame) window.cancelAnimationFrame(restoreScrollFrame);
|
||||
});
|
||||
|
||||
defineExpose({ focusSearch, refresh });
|
||||
</script>
|
||||
@@ -435,7 +564,7 @@ defineExpose({ focusSearch, refresh });
|
||||
<div v-else-if="error" class="flex flex-1 items-center justify-center px-6 text-center text-sm text-destructive">{{ t("databaseBrowser.loadFailed", { message: error }) }}</div>
|
||||
<div v-else-if="visibleRows.length === 0" class="flex flex-1 items-center justify-center text-sm text-muted-foreground">{{ t("databaseBrowser.empty") }}</div>
|
||||
<div v-else class="flex min-h-0 min-w-0 flex-1">
|
||||
<div v-if="isListView" class="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-auto overflow-y-hidden">
|
||||
<div v-if="isListView" ref="listOuterRef" class="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-auto overflow-y-hidden" @scroll.passive="onListHorizontalScroll">
|
||||
<div class="grid h-7 shrink-0 items-center gap-3 border-b bg-muted/40 px-3 text-xs font-medium text-muted-foreground" :style="{ gridTemplateColumns, minWidth: `${gridMinWidth}px` }">
|
||||
<div v-for="key in columns" :key="key" class="relative flex min-w-0 items-center">
|
||||
<button class="flex min-w-0 items-center gap-1 truncate pr-4 text-left" type="button" @click="toggleSort(key)">
|
||||
@@ -451,7 +580,7 @@ defineExpose({ focusSearch, refresh });
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-auto">
|
||||
<div ref="listScrollerRef" class="min-h-0 flex-1 overflow-auto" @scroll.passive="onListVerticalScroll">
|
||||
<div
|
||||
v-for="row in visibleRows"
|
||||
:key="row.name"
|
||||
@@ -478,7 +607,7 @@ defineExpose({ focusSearch, refresh });
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="grid min-h-0 flex-1 grid-cols-[repeat(auto-fill,minmax(160px,1fr))] content-start gap-3 overflow-auto p-3">
|
||||
<div v-else ref="gridScrollerRef" class="grid min-h-0 flex-1 grid-cols-[repeat(auto-fill,minmax(160px,1fr))] content-start gap-3 overflow-auto p-3" @scroll.passive="onGridScroll">
|
||||
<div
|
||||
v-for="row in visibleRows"
|
||||
:key="row.name"
|
||||
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, nextTick, ref, type App } from "vue";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import DatabaseBrowser from "../DatabaseBrowser.vue";
|
||||
import { provideTabUiState, TAB_UI_STATE_TRACK_DEBOUNCE_MS } from "@/lib/tabs/tabUiState";
|
||||
import type { ConnectionConfig } from "@/types/database";
|
||||
|
||||
const testDatabases = Array.from({ length: 30 }, (_, index) => ({
|
||||
name: `db_${String(index + 1).padStart(2, "0")}`,
|
||||
size_bytes: 1024 * (index + 1),
|
||||
created_at: "2026-01-01 00:00:00",
|
||||
updated_at: "2026-01-02 00:00:00",
|
||||
comment: `Database ${index + 1}`,
|
||||
default_charset: "utf8mb4",
|
||||
default_collation: "utf8mb4_0900_ai_ci",
|
||||
}));
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
listDatabases: vi.fn(),
|
||||
listDatabaseMetadata: vi.fn(),
|
||||
editorSettings: {
|
||||
objectBrowserViewMode: "list" as "list" | "grid",
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", () => ({
|
||||
listDatabases: (...args: unknown[]) => mocks.listDatabases(...args),
|
||||
listDatabaseMetadata: (...args: unknown[]) => mocks.listDatabaseMetadata(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/connectionStore", () => ({
|
||||
useConnectionStore: () => ({
|
||||
setDefaultDatabase: vi.fn(),
|
||||
clearDefaultDatabase: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/queryStore", () => ({
|
||||
useQueryStore: () => ({
|
||||
openObjectBrowser: vi.fn(),
|
||||
createTab: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/settingsStore", () => ({
|
||||
useSettingsStore: () => ({
|
||||
editorSettings: mocks.editorSettings,
|
||||
updateEditorSettings: (patch: Partial<typeof mocks.editorSettings>) => {
|
||||
Object.assign(mocks.editorSettings, patch);
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("vue-i18n", () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key,
|
||||
locale: ref("en-US"),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/sidebar/SidebarTreeRuntimeHost.vue", () => ({
|
||||
default: defineComponent({
|
||||
setup() {
|
||||
return () => null;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/sidebar/SidebarTreeItemDialogs.vue", () => ({
|
||||
default: defineComponent({
|
||||
setup() {
|
||||
return () => null;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/composables/useToolbarOverflow", () => ({
|
||||
useToolbarOverflow: () => ({
|
||||
tier: ref(0),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("DatabaseBrowser scroll position preservation (#12432)", () => {
|
||||
let app: App | undefined;
|
||||
let host: HTMLElement | undefined;
|
||||
|
||||
const testConnection: ConnectionConfig = {
|
||||
id: "conn-mysql-1",
|
||||
name: "MySQL Prod",
|
||||
db_type: "mysql",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.listDatabases.mockResolvedValue(testDatabases);
|
||||
mocks.listDatabaseMetadata.mockResolvedValue([]);
|
||||
mocks.editorSettings.objectBrowserViewMode = "list";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
app?.unmount();
|
||||
host?.remove();
|
||||
app = undefined;
|
||||
host = undefined;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("restores scrollTop from tab UI state when mounting in list view", async () => {
|
||||
let capturedState: Record<string, any> = {};
|
||||
const Parent = defineComponent({
|
||||
setup() {
|
||||
provideTabUiState(() => ({
|
||||
snapshot: {
|
||||
DatabaseBrowser: {
|
||||
scrollTop: 350,
|
||||
scrollLeft: 0,
|
||||
},
|
||||
},
|
||||
update: (patch) => {
|
||||
Object.assign(capturedState, patch);
|
||||
},
|
||||
}));
|
||||
return () => h(DatabaseBrowser, { connection: testConnection });
|
||||
},
|
||||
});
|
||||
|
||||
host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
app = createApp(Parent);
|
||||
app.mount(host);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const rows = host!.querySelectorAll(".grid.h-\\[34px\\]");
|
||||
expect(rows.length).toBe(testDatabases.length);
|
||||
});
|
||||
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
await nextTick();
|
||||
|
||||
const scroller = host.querySelector<HTMLElement>(".overflow-auto");
|
||||
expect(scroller).not.toBeNull();
|
||||
expect(scroller?.scrollTop).toBe(350);
|
||||
});
|
||||
|
||||
it("tracks vertical scrolling and debounces updates to tab UI state", async () => {
|
||||
let trackedUiState: Record<string, any> = {};
|
||||
|
||||
const Parent = defineComponent({
|
||||
setup() {
|
||||
provideTabUiState(() => ({
|
||||
snapshot: { DatabaseBrowser: {} },
|
||||
update: (patch) => {
|
||||
Object.assign(trackedUiState, patch);
|
||||
},
|
||||
}));
|
||||
return () => h(DatabaseBrowser, { connection: testConnection });
|
||||
},
|
||||
});
|
||||
|
||||
host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
app = createApp(Parent);
|
||||
app.mount(host);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const rows = host!.querySelectorAll(".grid.h-\\[34px\\]");
|
||||
expect(rows.length).toBe(testDatabases.length);
|
||||
});
|
||||
|
||||
const scroller = host.querySelector<HTMLElement>(".overflow-auto");
|
||||
expect(scroller).not.toBeNull();
|
||||
|
||||
vi.useFakeTimers();
|
||||
|
||||
// User scrolls down
|
||||
Object.defineProperty(scroller, "scrollTop", { value: 480, writable: true });
|
||||
scroller!.dispatchEvent(new Event("scroll"));
|
||||
|
||||
await nextTick();
|
||||
expect(trackedUiState.DatabaseBrowser?.scrollTop).toBeUndefined();
|
||||
|
||||
// Advance past debounce timer
|
||||
vi.advanceTimersByTime(TAB_UI_STATE_TRACK_DEBOUNCE_MS + 20);
|
||||
await nextTick();
|
||||
|
||||
expect(trackedUiState.DatabaseBrowser?.scrollTop).toBe(480);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("resets scrollTop when user enters a search query", async () => {
|
||||
let capturedState: Record<string, any> = {};
|
||||
const Parent = defineComponent({
|
||||
setup() {
|
||||
provideTabUiState(() => ({
|
||||
snapshot: {
|
||||
DatabaseBrowser: {
|
||||
scrollTop: 200,
|
||||
},
|
||||
},
|
||||
update: (patch) => {
|
||||
Object.assign(capturedState, patch);
|
||||
},
|
||||
}));
|
||||
return () => h(DatabaseBrowser, { connection: testConnection });
|
||||
},
|
||||
});
|
||||
|
||||
host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
app = createApp(Parent);
|
||||
app.mount(host);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const rows = host!.querySelectorAll(".grid.h-\\[34px\\]");
|
||||
expect(rows.length).toBe(testDatabases.length);
|
||||
});
|
||||
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
await nextTick();
|
||||
|
||||
const scroller = host.querySelector<HTMLElement>(".overflow-auto");
|
||||
expect(scroller?.scrollTop).toBe(200);
|
||||
|
||||
// Enter search query
|
||||
const input = host.querySelector<HTMLInputElement>("input");
|
||||
expect(input).not.toBeNull();
|
||||
input!.value = "db_01";
|
||||
input!.dispatchEvent(new Event("input"));
|
||||
|
||||
await nextTick();
|
||||
expect(scroller?.scrollTop).toBe(0);
|
||||
});
|
||||
|
||||
it("restores scrollTop from tab UI state when mounting in grid view", async () => {
|
||||
mocks.editorSettings.objectBrowserViewMode = "grid";
|
||||
|
||||
const Parent = defineComponent({
|
||||
setup() {
|
||||
provideTabUiState(() => ({
|
||||
snapshot: {
|
||||
DatabaseBrowser: {
|
||||
scrollTop: 180,
|
||||
},
|
||||
},
|
||||
update: vi.fn(),
|
||||
}));
|
||||
return () => h(DatabaseBrowser, { connection: testConnection });
|
||||
},
|
||||
});
|
||||
|
||||
host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
app = createApp(Parent);
|
||||
app.mount(host);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const cards = host!.querySelectorAll(".flex.min-h-28");
|
||||
expect(cards.length).toBe(testDatabases.length);
|
||||
});
|
||||
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
await nextTick();
|
||||
|
||||
const gridScroller = host.querySelector<HTMLElement>(".grid-cols-\\[repeat\\(auto-fill\\,minmax\\(160px\\,1fr\\)\\)\\]");
|
||||
expect(gridScroller).not.toBeNull();
|
||||
expect(gridScroller?.scrollTop).toBe(180);
|
||||
});
|
||||
|
||||
it("resets scrollTop when sort column or direction changes", async () => {
|
||||
const Parent = defineComponent({
|
||||
setup() {
|
||||
provideTabUiState(() => ({
|
||||
snapshot: {
|
||||
DatabaseBrowser: {
|
||||
scrollTop: 240,
|
||||
},
|
||||
},
|
||||
update: vi.fn(),
|
||||
}));
|
||||
return () => h(DatabaseBrowser, { connection: testConnection });
|
||||
},
|
||||
});
|
||||
|
||||
host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
app = createApp(Parent);
|
||||
app.mount(host);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const rows = host!.querySelectorAll(".grid.h-\\[34px\\]");
|
||||
expect(rows.length).toBe(testDatabases.length);
|
||||
});
|
||||
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
await nextTick();
|
||||
|
||||
const scroller = host.querySelector<HTMLElement>(".overflow-auto");
|
||||
expect(scroller?.scrollTop).toBe(240);
|
||||
|
||||
// Click sort header button
|
||||
const sortBtn = host.querySelector<HTMLButtonElement>("button.gap-1");
|
||||
expect(sortBtn).not.toBeNull();
|
||||
sortBtn!.click();
|
||||
|
||||
await nextTick();
|
||||
expect(scroller?.scrollTop).toBe(0);
|
||||
});
|
||||
|
||||
it("restores scrollLeft from tab UI state when mounting in list view", async () => {
|
||||
const Parent = defineComponent({
|
||||
setup() {
|
||||
provideTabUiState(() => ({
|
||||
snapshot: {
|
||||
DatabaseBrowser: {
|
||||
scrollTop: 100,
|
||||
scrollLeft: 150,
|
||||
},
|
||||
},
|
||||
update: vi.fn(),
|
||||
}));
|
||||
return () => h(DatabaseBrowser, { connection: testConnection });
|
||||
},
|
||||
});
|
||||
|
||||
host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
app = createApp(Parent);
|
||||
app.mount(host);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const rows = host!.querySelectorAll(".grid.h-\\[34px\\]");
|
||||
expect(rows.length).toBe(testDatabases.length);
|
||||
});
|
||||
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
await nextTick();
|
||||
|
||||
const outer = host.querySelector<HTMLElement>(".overflow-x-auto");
|
||||
const scroller = host.querySelector<HTMLElement>(".overflow-auto");
|
||||
expect(scroller?.scrollTop).toBe(100);
|
||||
expect(outer?.scrollLeft).toBe(150);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user