Skip to content

Commit 208eb19

Browse files
authored
chore: add test for db views (#204)
* chore: add test for db views * chore: fix test * chore: fix test * chore: fix test * chore: add hook * chore: add hook
1 parent 095f42a commit 208eb19

File tree

18 files changed

+668
-937
lines changed

18 files changed

+668
-937
lines changed

cypress/e2e/database/database-view-tabs.cy.ts

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,21 @@ describe('Database View Tabs', () => {
132132
}
133133
});
134134

135-
// Verify children exist
135+
// Verify children exist and match the database views (Grid, Board, Calendar)
136136
PageSelectors.itemByName('New Database', { timeout: 10000 }).within(() => {
137137
PageSelectors.names().should('have.length.at.least', 3);
138+
// Verify each view type exists in sidebar
139+
cy.contains('Grid').should('exist');
140+
cy.contains('Board').should('exist');
141+
cy.contains('Calendar').should('exist');
138142
});
139143

144+
// Step 7.1: Verify tab bar and sidebar have matching views
145+
cy.task('log', '[STEP 7.1] Verifying tab bar and sidebar views match');
146+
DatabaseViewSelectors.viewTab().contains('Grid').should('exist');
147+
DatabaseViewSelectors.viewTab().contains('Board').should('exist');
148+
DatabaseViewSelectors.viewTab().contains('Calendar').should('exist');
149+
140150
// Step 8: Navigate away and back to verify tabs persist
141151
cy.task('log', '[STEP 8] Navigating away and back');
142152
AddPageSelectors.inlineAddButton().first().click({ force: true });
@@ -225,4 +235,113 @@ describe('Database View Tabs', () => {
225235
cy.task('log', '[TEST COMPLETE] Tab selection updates sidebar');
226236
});
227237
});
238+
239+
/**
240+
* Regression test for: newly created views should appear immediately in tab bar.
241+
*
242+
* Previously, views wouldn't appear until the folder/outline synced from the server.
243+
* The fix ensures views from Yjs (database_update) are shown immediately without
244+
* waiting for folder sync.
245+
*
246+
* See: selector.ts - useDatabaseViewsSelector now includes non-embedded views from Yjs
247+
*/
248+
it('newly created view appears immediately in tab bar (no sync delay)', () => {
249+
const testEmail = generateRandomEmail();
250+
251+
cy.task('log', `[TEST] Immediate view appearance - Email: ${testEmail}`);
252+
253+
cy.visit('/login', { failOnStatusCode: false });
254+
cy.wait(2000);
255+
256+
const authUtils = new AuthTestUtils();
257+
authUtils.signInWithTestUrl(testEmail).then(() => {
258+
cy.url({ timeout: 30000 }).should('include', '/app');
259+
cy.wait(3000);
260+
261+
// Create a Grid database
262+
cy.task('log', '[STEP 1] Creating Grid database');
263+
AddPageSelectors.inlineAddButton().first().click({ force: true });
264+
waitForReactUpdate(1000);
265+
AddPageSelectors.addGridButton().should('be.visible').click({ force: true });
266+
cy.wait(5000);
267+
268+
// Verify initial state - should have exactly 1 tab (Grid)
269+
cy.task('log', '[STEP 2] Verifying initial tab count');
270+
DatabaseViewSelectors.viewTab().should('have.length.at.least', 1);
271+
DatabaseViewSelectors.viewTab().then(($tabs) => {
272+
cy.wrap($tabs.length).as('initialTabCount');
273+
});
274+
275+
// Click + button to add Board view
276+
cy.task('log', '[STEP 3] Clicking + button to add Board view');
277+
DatabaseViewSelectors.addViewButton().scrollIntoView().click({ force: true });
278+
waitForReactUpdate(300); // Short wait for menu to appear
279+
280+
// Click Board option
281+
cy.get('[role="menu"], [role="menuitem"]', { timeout: 5000 })
282+
.should('be.visible')
283+
.contains('Board')
284+
.click({ force: true });
285+
286+
// CRITICAL: Verify tab appears quickly (within 1s)
287+
// This tests that the view appears from Yjs immediately, not waiting for folder sync
288+
// Previously this would fail because views only appeared after folder sync (3+ seconds)
289+
cy.task('log', '[STEP 4] Verifying Board tab appears quickly (within 1s)');
290+
waitForReactUpdate(200); // Minimal wait for React to process the state update
291+
cy.get('@initialTabCount').then((initialCount) => {
292+
cy.get('[data-testid^="view-tab-"]', { timeout: 1000 }).should(
293+
'have.length',
294+
(initialCount as number) + 1
295+
);
296+
});
297+
298+
// Verify the Board tab is active (selected)
299+
cy.task('log', '[STEP 5] Verifying Board tab is active');
300+
DatabaseViewSelectors.activeViewTab().should('exist');
301+
cy.get('[data-testid^="view-tab-"][data-state="active"]')
302+
.should('contain.text', 'Board');
303+
304+
// Add Calendar view with same immediate check
305+
cy.task('log', '[STEP 6] Adding Calendar view');
306+
DatabaseViewSelectors.addViewButton().scrollIntoView().click({ force: true });
307+
waitForReactUpdate(300);
308+
309+
cy.get('[role="menu"], [role="menuitem"]', { timeout: 5000 })
310+
.should('be.visible')
311+
.contains('Calendar')
312+
.click({ force: true });
313+
314+
// Verify Calendar tab appears immediately
315+
cy.task('log', '[STEP 7] Verifying Calendar tab appears IMMEDIATELY');
316+
cy.get('@initialTabCount').then((initialCount) => {
317+
cy.get('[data-testid^="view-tab-"]', { timeout: 500 }).should(
318+
'have.length',
319+
(initialCount as number) + 2
320+
);
321+
});
322+
323+
// Step 8: Verify sidebar matches tab bar views
324+
cy.task('log', '[STEP 8] Verifying sidebar matches tab bar views');
325+
ensureSpaceExpanded(spaceName);
326+
waitForReactUpdate(500);
327+
328+
// Expand the database to see children
329+
PageSelectors.itemByName('New Database', { timeout: 10000 }).then(($dbItem) => {
330+
const expandToggle = $dbItem.find('[data-testid="outline-toggle-expand"]');
331+
if (expandToggle.length > 0) {
332+
cy.wrap(expandToggle).first().click({ force: true });
333+
waitForReactUpdate(500);
334+
}
335+
});
336+
337+
// Verify sidebar contains all view types
338+
PageSelectors.itemByName('New Database', { timeout: 10000 }).within(() => {
339+
cy.contains('Grid').should('exist');
340+
cy.contains('Board').should('exist');
341+
cy.contains('Calendar').should('exist');
342+
});
343+
344+
cy.task('log', '[TEST COMPLETE] Views appear immediately without sync delay');
345+
});
346+
});
228347
});

src/application/database-yjs/selector.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,15 @@ export function useDatabaseViewsSelector(databasePageId: string, visibleViewIds?
7070
const [viewIds, setViewIds] = useState<string[]>([]);
7171
const [childViews, setChildViews] = useState<ReturnType<typeof views.get>[]>([]);
7272

73+
// Stabilize visibleViewIds reference to avoid unnecessary effect re-runs
74+
const visibleViewIdsKey = visibleViewIds?.join(',') ?? '';
75+
7376
useEffect(() => {
7477
if (!views) return;
7578

79+
// Parse the stabilized key back to array (or undefined)
80+
const stableVisibleViewIds = visibleViewIdsKey ? visibleViewIdsKey.split(',') : undefined;
81+
7682
const observerEvent = () => {
7783
const viewsObj = views.toJSON() as Record<
7884
string,
@@ -81,24 +87,35 @@ export function useDatabaseViewsSelector(databasePageId: string, visibleViewIds?
8187
}
8288
>;
8389

84-
// Get all views from the database and filter out inline views
90+
// Step 1: Get all non-inline views from Yjs (don't filter by embedded yet)
91+
// See: flowy-database2/src/services/database/database_editor.rs:get_database_view_ids()
8592
let allViewIds = Object.keys(viewsObj).filter((viewId) => {
8693
const view = views.get(viewId);
8794

8895
if (!view) return false;
96+
8997
const isInline = view.get(YjsDatabaseKey.is_inline);
9098

9199
return !isInline;
92100
});
93101

94-
// If visibleViewIds is provided (for embedded databases), filter to only show those views
95-
// visibleViewIds is undefined for standalone databases, [] for embedded with no child views yet
96-
if (visibleViewIds !== undefined) {
97-
// Preserve the ordering defined by `visibleViewIds` (folder/outline order), not the
98-
// internal insertion order of the Yjs `views` map.
99-
const availableIds = new Set(allViewIds);
102+
// Step 2: Apply context-specific filtering (separate concerns)
103+
if (stableVisibleViewIds !== undefined && stableVisibleViewIds.length > 0) {
104+
// For embedded databases: show ONLY views in visibleViewIds
105+
// This handles views with embedded: true (created via + button)
106+
// The visibleViewIds list is the source of truth for what to display
107+
const allViewIdsSet = new Set(allViewIds);
100108

101-
allViewIds = visibleViewIds.filter((viewId) => availableIds.has(viewId));
109+
allViewIds = stableVisibleViewIds.filter((viewId) => allViewIdsSet.has(viewId));
110+
} else {
111+
// For standalone databases: exclude embedded views
112+
// Embedded views belong to their respective embedded database blocks
113+
allViewIds = allViewIds.filter((viewId) => {
114+
const view = views.get(viewId);
115+
const isEmbedded = view?.get(YjsDatabaseKey.embedded) === true;
116+
117+
return !isEmbedded;
118+
});
102119
}
103120

104121
setViewIds(allViewIds);
@@ -111,7 +128,7 @@ export function useDatabaseViewsSelector(databasePageId: string, visibleViewIds?
111128
return () => {
112129
views.unobserveDeep(observerEvent);
113130
};
114-
}, [views, visibleViewIds, databasePageId]);
131+
}, [views, visibleViewIdsKey]);
115132

116133
return {
117134
childViews,

src/components/app/DatabaseView.tsx

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Suspense, useCallback, useMemo } from 'react';
22
import { useSearchParams } from 'react-router-dom';
33

44
import { ViewComponentProps, ViewLayout, YDatabase, YjsEditorKey } from '@/application/types';
5-
import { getDatabaseTabViewIds, isDatabaseContainer } from '@/application/view-utils';
65
import { findView } from '@/components/_shared/outline/utils';
76
import ComponentLoading from '@/components/_shared/progress/ComponentLoading';
87
import CalendarSkeleton from '@/components/_shared/skeleton/CalendarSkeleton';
@@ -12,6 +11,7 @@ import KanbanSkeleton from '@/components/_shared/skeleton/KanbanSkeleton';
1211
import { useAppOutline } from '@/components/app/app.hooks';
1312
import { DATABASE_TAB_VIEW_ID_QUERY_PARAM } from '@/components/app/hooks/resolveSidebarSelectedViewId';
1413
import { Database } from '@/components/database';
14+
import { useContainerVisibleViewIds } from '@/components/database/hooks';
1515

1616
import ViewMetaPreview from 'src/components/view-meta/ViewMetaPreview';
1717

@@ -31,36 +31,12 @@ function DatabaseView(props: ViewComponentProps) {
3131
return findView(outline || [], databasePageId);
3232
}, [outline, databasePageId]);
3333

34-
const containerView = useMemo(() => {
35-
if (!outline || !view) return;
36-
37-
if (isDatabaseContainer(view)) {
38-
return view;
39-
}
40-
41-
const parentId = view.parent_view_id;
42-
43-
if (!parentId) {
44-
return;
45-
}
46-
47-
const parent = findView(outline || [], parentId);
48-
49-
return isDatabaseContainer(parent) ? parent : undefined;
50-
}, [outline, view]);
34+
// Use hook to determine container view and visible view IDs
35+
const { containerView, visibleViewIds } = useContainerVisibleViewIds({ view, outline });
5136

5237
// Use container view (if present) as the "page meta" view for naming/icon operations.
5338
const pageView = containerView || view;
5439

55-
const visibleViewIds = useMemo(() => {
56-
if (containerView) {
57-
return getDatabaseTabViewIds(databasePageId, containerView);
58-
}
59-
60-
if (!view) return [];
61-
return [view.view_id, ...(view.children?.map((v) => v.view_id) || [])];
62-
}, [containerView, view, databasePageId]);
63-
6440
const pageMeta = useMemo(() => {
6541
if (!pageView) {
6642
return viewMeta;
@@ -154,10 +130,10 @@ function DatabaseView(props: ViewComponentProps) {
154130
activeViewId={activeViewId}
155131
rowId={rowId}
156132
showActions={true}
157-
visibleViewIds={visibleViewIds}
158133
onChangeView={handleChangeView}
159134
onOpenRowPage={handleNavigateToRow}
160135
modalRowId={modalRowId}
136+
visibleViewIds={visibleViewIds}
161137
/>
162138
</Suspense>
163139
</div>

src/components/database/Database.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ export interface Database2Props {
4444
onChangeView: (viewId: string) => void;
4545
onViewAdded?: (viewId: string) => void;
4646
onOpenRowPage?: (rowId: string) => void;
47-
visibleViewIds: string[];
47+
/**
48+
* For embedded databases: restricts which views are shown (from block data).
49+
* For standalone databases: should be undefined to show all non-embedded views.
50+
*/
51+
visibleViewIds?: string[];
4852
/**
4953
* The database's page ID in the folder/outline structure.
5054
* This is the main entry point for the database and remains constant.

0 commit comments

Comments
 (0)