Skip to content

Commit d6ef63b

Browse files
test: add E2E tests for database table column resize (#1100) (#1104)
* test: add E2E tests for database table column resize (#1100) Co-authored-by: Ona <no-reply@ona.com> * fix: [ci-fix] update WorkspaceHome visual regression baselines The WorkspaceHome stories render at 1280x800 after the Import Markdown action was added (616ee28), but baselines were still 1280x720. Regenerated all 5 WorkspaceHome snapshots to match current rendering. Co-authored-by: Ona <no-reply@ona.com> --------- Co-authored-by: Ona <no-reply@ona.com>
1 parent 27a2c1d commit d6ef63b

8 files changed

Lines changed: 355 additions & 4 deletions

.agents/quality.md

Lines changed: 5 additions & 4 deletions
Large diffs are not rendered by default.

e2e/database-column-resize.spec.ts

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
import { test, expect } from "./fixtures/auth";
2+
import { createClient } from "@supabase/supabase-js";
3+
4+
// ---------------------------------------------------------------------------
5+
// Admin client for setup and cleanup
6+
// ---------------------------------------------------------------------------
7+
8+
function getAdminClient() {
9+
return createClient(
10+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
11+
process.env.SUPABASE_SECRET_KEY!,
12+
{ auth: { autoRefreshToken: false, persistSession: false } },
13+
);
14+
}
15+
16+
// ---------------------------------------------------------------------------
17+
// Test data
18+
// ---------------------------------------------------------------------------
19+
20+
let databasePageId: string;
21+
let workspaceSlug: string;
22+
const propertyIds: string[] = [];
23+
const rowPageIds: string[] = [];
24+
25+
// ---------------------------------------------------------------------------
26+
// Setup: create a database with 2 properties and 1 row
27+
// ---------------------------------------------------------------------------
28+
29+
test.beforeAll(async () => {
30+
const admin = getAdminClient();
31+
const email = process.env.TEST_USER_EMAIL!;
32+
33+
// Find the test user
34+
const { data: userList } = await admin.auth.admin.listUsers({
35+
perPage: 1000,
36+
});
37+
let testUserId: string | undefined;
38+
if (userList?.users) {
39+
testUserId = userList.users.find((u) => u.email === email)?.id;
40+
}
41+
if (!testUserId) {
42+
const { data: profileRows } = await admin
43+
.from("profiles")
44+
.select("id")
45+
.limit(50);
46+
if (profileRows) {
47+
for (const p of profileRows) {
48+
const { data: authUser } = await admin.auth.admin.getUserById(p.id);
49+
if (authUser?.user?.email === email) {
50+
testUserId = p.id;
51+
break;
52+
}
53+
}
54+
}
55+
}
56+
if (!testUserId) throw new Error(`Test user ${email} not found`);
57+
58+
// Get workspace
59+
const { data: memberships } = await admin
60+
.from("members")
61+
.select("workspace_id, workspaces(id, slug, is_personal)")
62+
.eq("user_id", testUserId)
63+
.limit(10);
64+
65+
if (!memberships || memberships.length === 0) {
66+
throw new Error("No workspace found for test user");
67+
}
68+
69+
const ws = memberships[0].workspaces as unknown as {
70+
id: string;
71+
slug: string;
72+
};
73+
workspaceSlug = ws.slug;
74+
75+
// Create a database page
76+
const { data: dbPage, error: dbErr } = await admin
77+
.from("pages")
78+
.insert({
79+
workspace_id: ws.id,
80+
title: "Column Resize Test DB",
81+
is_database: true,
82+
position: 9992,
83+
created_by: testUserId,
84+
})
85+
.select()
86+
.single();
87+
88+
if (dbErr || !dbPage)
89+
throw new Error(`Failed to create database: ${dbErr?.message}`);
90+
databasePageId = dbPage.id;
91+
92+
// Create two properties: ColA and ColB
93+
const propNames = ["ColA", "ColB"];
94+
for (let i = 0; i < propNames.length; i++) {
95+
const { data: prop, error: propErr } = await admin
96+
.from("database_properties")
97+
.insert({
98+
database_id: databasePageId,
99+
name: propNames[i],
100+
type: "text",
101+
config: {},
102+
position: i,
103+
})
104+
.select()
105+
.single();
106+
107+
if (propErr || !prop)
108+
throw new Error(`Failed to create property: ${propErr?.message}`);
109+
propertyIds.push(prop.id);
110+
}
111+
112+
// Create a table view
113+
const { error: tvErr } = await admin
114+
.from("database_views")
115+
.insert({
116+
database_id: databasePageId,
117+
name: "Table view",
118+
type: "table",
119+
config: {},
120+
position: 0,
121+
})
122+
.select()
123+
.single();
124+
125+
if (tvErr) throw new Error(`Failed to create table view: ${tvErr.message}`);
126+
127+
// Create one row so the table renders data rows
128+
const { data: rowPage, error: rowErr } = await admin
129+
.from("pages")
130+
.insert({
131+
workspace_id: ws.id,
132+
parent_id: databasePageId,
133+
title: "Resize Test Row",
134+
is_database: false,
135+
position: 0,
136+
created_by: testUserId,
137+
})
138+
.select()
139+
.single();
140+
141+
if (rowErr || !rowPage)
142+
throw new Error(`Failed to create row: ${rowErr?.message}`);
143+
rowPageIds.push(rowPage.id);
144+
});
145+
146+
// ---------------------------------------------------------------------------
147+
// Cleanup
148+
// ---------------------------------------------------------------------------
149+
150+
test.afterAll(async () => {
151+
const admin = getAdminClient();
152+
153+
const { data: allRows } = await admin
154+
.from("pages")
155+
.select("id")
156+
.eq("parent_id", databasePageId);
157+
if (allRows) {
158+
for (const row of allRows) {
159+
await admin.from("row_values").delete().eq("row_id", row.id);
160+
}
161+
for (const row of allRows) {
162+
await admin.from("pages").delete().eq("id", row.id);
163+
}
164+
}
165+
166+
await admin
167+
.from("database_views")
168+
.delete()
169+
.eq("database_id", databasePageId);
170+
await admin
171+
.from("database_properties")
172+
.delete()
173+
.eq("database_id", databasePageId);
174+
await admin.from("pages").delete().eq("id", databasePageId);
175+
});
176+
177+
// ---------------------------------------------------------------------------
178+
// Helpers
179+
// ---------------------------------------------------------------------------
180+
181+
async function navigateToDatabase(page: import("@playwright/test").Page) {
182+
await page.goto(`/${workspaceSlug}/${databasePageId}`);
183+
await expect(
184+
page.getByRole("button", { name: /Table view/i }),
185+
).toBeVisible({ timeout: 15_000 });
186+
}
187+
188+
/** Get the computed pixel width of a column header by its colIndex. */
189+
async function getColumnWidth(
190+
page: import("@playwright/test").Page,
191+
colIndex: number,
192+
): Promise<number> {
193+
const header = page.getByTestId(`db-table-column-header-${colIndex}`);
194+
const box = await header.boundingBox();
195+
if (!box) throw new Error(`Column header ${colIndex} not visible`);
196+
return box.width;
197+
}
198+
199+
// ---------------------------------------------------------------------------
200+
// Tests
201+
// ---------------------------------------------------------------------------
202+
203+
test.describe("Table view column resize", () => {
204+
test("drag resize handle to increase column width", async ({
205+
authenticatedPage: page,
206+
}) => {
207+
await navigateToDatabase(page);
208+
209+
// Wait for the first property column header (ColA at index 0)
210+
const colAHeader = page.getByTestId("db-table-column-header-0");
211+
await expect(colAHeader).toBeVisible({ timeout: 10_000 });
212+
213+
// Record initial widths of both columns
214+
const initialWidthA = await getColumnWidth(page, 0);
215+
const initialWidthB = await getColumnWidth(page, 1);
216+
217+
// Get the resize handle for ColA (index 0)
218+
const resizeHandle = page.getByTestId("db-table-resize-handle-0");
219+
await expect(resizeHandle).toBeVisible();
220+
221+
const handleBox = await resizeHandle.boundingBox();
222+
if (!handleBox) throw new Error("Resize handle not visible");
223+
224+
// Drag the handle 100px to the right to widen ColA
225+
const startX = handleBox.x + handleBox.width / 2;
226+
const startY = handleBox.y + handleBox.height / 2;
227+
const dragDistance = 100;
228+
229+
await page.mouse.move(startX, startY);
230+
await page.mouse.down();
231+
// Move in steps to trigger mousemove events
232+
for (let i = 1; i <= 5; i++) {
233+
await page.mouse.move(startX + (dragDistance * i) / 5, startY);
234+
}
235+
await page.mouse.up();
236+
237+
// Verify ColA width increased
238+
await expect(async () => {
239+
const newWidthA = await getColumnWidth(page, 0);
240+
expect(newWidthA).toBeGreaterThan(initialWidthA + 50);
241+
}).toPass({ timeout: 5_000 });
242+
243+
// Verify ColB width is not affected
244+
const newWidthB = await getColumnWidth(page, 1);
245+
expect(newWidthB).toBeCloseTo(initialWidthB, -1);
246+
});
247+
248+
test("drag resize handle to decrease column width", async ({
249+
authenticatedPage: page,
250+
}) => {
251+
await navigateToDatabase(page);
252+
253+
const colAHeader = page.getByTestId("db-table-column-header-0");
254+
await expect(colAHeader).toBeVisible({ timeout: 10_000 });
255+
256+
const initialWidthA = await getColumnWidth(page, 0);
257+
258+
const resizeHandle = page.getByTestId("db-table-resize-handle-0");
259+
const handleBox = await resizeHandle.boundingBox();
260+
if (!handleBox) throw new Error("Resize handle not visible");
261+
262+
// Drag the handle 60px to the left to shrink ColA
263+
const startX = handleBox.x + handleBox.width / 2;
264+
const startY = handleBox.y + handleBox.height / 2;
265+
const dragDistance = -60;
266+
267+
await page.mouse.move(startX, startY);
268+
await page.mouse.down();
269+
for (let i = 1; i <= 5; i++) {
270+
await page.mouse.move(startX + (dragDistance * i) / 5, startY);
271+
}
272+
await page.mouse.up();
273+
274+
// Verify ColA width decreased
275+
await expect(async () => {
276+
const newWidthA = await getColumnWidth(page, 0);
277+
expect(newWidthA).toBeLessThan(initialWidthA);
278+
}).toPass({ timeout: 5_000 });
279+
});
280+
281+
test("minimum column width constraint is respected", async ({
282+
authenticatedPage: page,
283+
}) => {
284+
await navigateToDatabase(page);
285+
286+
const colAHeader = page.getByTestId("db-table-column-header-0");
287+
await expect(colAHeader).toBeVisible({ timeout: 10_000 });
288+
289+
const resizeHandle = page.getByTestId("db-table-resize-handle-0");
290+
const handleBox = await resizeHandle.boundingBox();
291+
if (!handleBox) throw new Error("Resize handle not visible");
292+
293+
// Drag the handle far to the left (500px) to try to shrink below minimum
294+
const startX = handleBox.x + handleBox.width / 2;
295+
const startY = handleBox.y + handleBox.height / 2;
296+
const dragDistance = -500;
297+
298+
await page.mouse.move(startX, startY);
299+
await page.mouse.down();
300+
for (let i = 1; i <= 10; i++) {
301+
await page.mouse.move(startX + (dragDistance * i) / 10, startY);
302+
}
303+
await page.mouse.up();
304+
305+
// Verify column width does not go below minimum (80px)
306+
await expect(async () => {
307+
const newWidthA = await getColumnWidth(page, 0);
308+
expect(newWidthA).toBeGreaterThanOrEqual(80);
309+
}).toPass({ timeout: 5_000 });
310+
});
311+
312+
test("resizing one column does not affect other columns", async ({
313+
authenticatedPage: page,
314+
}) => {
315+
await navigateToDatabase(page);
316+
317+
const colBHeader = page.getByTestId("db-table-column-header-1");
318+
await expect(colBHeader).toBeVisible({ timeout: 10_000 });
319+
320+
// Record initial width of ColA
321+
const initialWidthA = await getColumnWidth(page, 0);
322+
323+
// Resize ColB (index 1) by dragging its handle
324+
const resizeHandle = page.getByTestId("db-table-resize-handle-1");
325+
const handleBox = await resizeHandle.boundingBox();
326+
if (!handleBox) throw new Error("Resize handle not visible");
327+
328+
const startX = handleBox.x + handleBox.width / 2;
329+
const startY = handleBox.y + handleBox.height / 2;
330+
const dragDistance = 80;
331+
332+
await page.mouse.move(startX, startY);
333+
await page.mouse.down();
334+
for (let i = 1; i <= 5; i++) {
335+
await page.mouse.move(startX + (dragDistance * i) / 5, startY);
336+
}
337+
await page.mouse.up();
338+
339+
// Verify ColB width increased
340+
await expect(async () => {
341+
const newWidthB = await getColumnWidth(page, 1);
342+
expect(newWidthB).toBeGreaterThan(180);
343+
}).toPass({ timeout: 5_000 });
344+
345+
// Verify ColA width is unchanged
346+
const finalWidthA = await getColumnWidth(page, 0);
347+
expect(finalWidthA).toBeCloseTo(initialWidthA, -1);
348+
});
349+
});
500 Bytes
Loading
515 Bytes
Loading
2.13 KB
Loading
3.25 KB
Loading
5.75 KB
Loading

src/components/database/views/table-column-header.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ export function TableColumnHeader({
199199
onMouseDown={(e) => onResizeStart(property.id, e)}
200200
role="separator"
201201
aria-orientation="vertical"
202+
data-testid={`db-table-resize-handle-${colIndex}`}
202203
/>
203204

204205
{/* Delete confirmation dialog */}

0 commit comments

Comments
 (0)