Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

test: add database views E2E tests, fix E2E helper timeouts

Add E2E Playwright tests for database views feature:
- Kanban view: dialog, column grouping, cards, close button
- Gallery view: card grid rendering
- Calendar view: month sections, date grouping
- Pivot table: create/edit/delete with row field selection

Fix E2E helpers: use simple click-then-wait pattern instead of
Promise.all for async createDocument flow, increase timeouts
to 30s for navigation after fetch+redirect.

Resolves #302

+299 -12
+6
CHANGELOG.md
··· 110 110 - Fix sheets chat input: keyboard handler no longer captures typing in AI chat sidebar (#233) 111 111 112 112 ### Changed 113 + - Add E2E tests for forms builder and submission (#301) 114 + - Add E2E tests for diagrams whiteboard (#300) 115 + - Add E2E tests for slides presentations (#299) 116 + - Replace stub tests with real behavioral tests (#298) 117 + - Add unit tests for AI doc actions and blob upload (#297) 118 + - Add unit tests for slides, diagrams, and forms entry points (#296) 113 119 - Fix Electron code signing: Developer ID cert + notarization (#264) 114 120 - Electron thin client: auto-connect Tailnet backend (#261) 115 121 - Wire up Apple notarization for Electron builds (#260)
+285
e2e/database-views.spec.ts
··· 1 + import { test, expect } from '@playwright/test'; 2 + import { createNewSheet, typeInCell, clickCell } from './helpers'; 3 + 4 + /** 5 + * Populate a small dataset in the sheet for database view testing. 6 + * Creates headers in row 1 and data rows 2-4. 7 + */ 8 + async function seedSheetData(page: import('@playwright/test').Page) { 9 + // Headers 10 + await typeInCell(page, 'A1', 'Status'); 11 + await typeInCell(page, 'B1', 'Task'); 12 + await typeInCell(page, 'C1', 'Date'); 13 + 14 + // Data rows 15 + await typeInCell(page, 'A2', 'Backlog'); 16 + await typeInCell(page, 'B2', 'Design mockups'); 17 + await typeInCell(page, 'C2', '2026-04-01'); 18 + 19 + await typeInCell(page, 'A3', 'Active'); 20 + await typeInCell(page, 'B3', 'Build feature'); 21 + await typeInCell(page, 'C3', '2026-04-02'); 22 + 23 + await typeInCell(page, 'A4', 'Complete'); 24 + await typeInCell(page, 'B4', 'Write tests'); 25 + await typeInCell(page, 'C4', '2026-04-03'); 26 + } 27 + 28 + /** Open the overflow menu that contains database view and pivot buttons. */ 29 + async function openOverflowMenu(page: import('@playwright/test').Page) { 30 + await page.click('#overflow-toggle'); 31 + // Wait for menu to be visible 32 + await expect(page.locator('.toolbar-overflow-menu')).toBeVisible(); 33 + } 34 + 35 + test.describe('Database Views - Dialog', () => { 36 + test.beforeEach(async ({ page }) => { 37 + await createNewSheet(page); 38 + await seedSheetData(page); 39 + }); 40 + 41 + test('database view button opens dialog', async ({ page }) => { 42 + await openOverflowMenu(page); 43 + await page.click('#tb-view-mode'); 44 + 45 + // Dialog overlay should appear 46 + await expect(page.locator('.dbview-dialog-overlay')).toBeVisible(); 47 + await expect(page.locator('.sheet-dialog h3')).toHaveText('Database View'); 48 + }); 49 + 50 + test('dialog shows view type options', async ({ page }) => { 51 + await openOverflowMenu(page); 52 + await page.click('#tb-view-mode'); 53 + 54 + const typeSelect = page.locator('#dbview-type'); 55 + await expect(typeSelect).toBeVisible(); 56 + 57 + // Should have kanban, gallery, and calendar options 58 + const options = typeSelect.locator('option'); 59 + await expect(options).toHaveCount(3); 60 + await expect(options.nth(0)).toHaveText('Kanban'); 61 + await expect(options.nth(1)).toHaveText('Gallery'); 62 + await expect(options.nth(2)).toHaveText('Calendar'); 63 + }); 64 + 65 + test('dialog shows column selectors populated from headers', async ({ page }) => { 66 + await openOverflowMenu(page); 67 + await page.click('#tb-view-mode'); 68 + 69 + // Group by column should list columns with header names 70 + const groupSelect = page.locator('#dbview-group'); 71 + await expect(groupSelect).toBeVisible(); 72 + 73 + // First option should include our header text "Status" 74 + const firstOption = groupSelect.locator('option').first(); 75 + const text = await firstOption.textContent(); 76 + expect(text).toContain('Status'); 77 + }); 78 + 79 + test('cancel button closes dialog', async ({ page }) => { 80 + await openOverflowMenu(page); 81 + await page.click('#tb-view-mode'); 82 + await expect(page.locator('.dbview-dialog-overlay')).toBeVisible(); 83 + 84 + await page.click('#dbview-cancel'); 85 + await expect(page.locator('.dbview-dialog-overlay')).not.toBeVisible(); 86 + }); 87 + 88 + test('clicking overlay background closes dialog', async ({ page }) => { 89 + await openOverflowMenu(page); 90 + await page.click('#tb-view-mode'); 91 + await expect(page.locator('.dbview-dialog-overlay')).toBeVisible(); 92 + 93 + // Click on overlay background (not the dialog itself) 94 + await page.locator('.dbview-dialog-overlay').click({ position: { x: 5, y: 5 } }); 95 + await expect(page.locator('.dbview-dialog-overlay')).not.toBeVisible(); 96 + }); 97 + }); 98 + 99 + test.describe('Database Views - Kanban', () => { 100 + test.beforeEach(async ({ page }) => { 101 + await createNewSheet(page); 102 + await seedSheetData(page); 103 + }); 104 + 105 + test('open kanban view with default settings', async ({ page }) => { 106 + await openOverflowMenu(page); 107 + await page.click('#tb-view-mode'); 108 + 109 + // Select kanban (default) and click Open View 110 + await page.click('#dbview-ok'); 111 + 112 + // Database view section should appear 113 + await expect(page.locator('#database-view-section')).toBeVisible(); 114 + 115 + // Should show a toolbar with "Kanban View" title 116 + await expect(page.locator('.db-view-toolbar')).toContainText('Kanban View'); 117 + 118 + // Should show a kanban board 119 + await expect(page.locator('.kanban-board')).toBeVisible(); 120 + }); 121 + 122 + test('kanban board shows columns for each group value', async ({ page }) => { 123 + await openOverflowMenu(page); 124 + await page.click('#tb-view-mode'); 125 + 126 + // Group by column A (Status) — default since it's column 1 127 + await page.click('#dbview-ok'); 128 + 129 + // Should have kanban columns for each unique Status value 130 + const columns = page.locator('.kanban-column'); 131 + await expect(columns).toHaveCount(3); 132 + 133 + // Column headers should contain the status values 134 + const headers = page.locator('.kanban-column-title'); 135 + const headerTexts = await headers.allTextContents(); 136 + expect(headerTexts).toContain('Backlog'); 137 + expect(headerTexts).toContain('Active'); 138 + expect(headerTexts).toContain('Complete'); 139 + }); 140 + 141 + test('kanban cards show task titles', async ({ page }) => { 142 + await openOverflowMenu(page); 143 + await page.click('#tb-view-mode'); 144 + 145 + // Set title column to B (col 2) 146 + await page.locator('#dbview-title').selectOption('2'); 147 + await page.click('#dbview-ok'); 148 + 149 + // Cards should exist 150 + const cards = page.locator('.kanban-card'); 151 + await expect(cards).toHaveCount(3); 152 + }); 153 + 154 + test('close button hides the view', async ({ page }) => { 155 + await openOverflowMenu(page); 156 + await page.click('#tb-view-mode'); 157 + await page.click('#dbview-ok'); 158 + await expect(page.locator('#database-view-section')).toBeVisible(); 159 + 160 + // Click the close button 161 + await page.click('.db-view-close'); 162 + await expect(page.locator('#database-view-section')).not.toBeVisible(); 163 + }); 164 + }); 165 + 166 + test.describe('Database Views - Gallery', () => { 167 + test.beforeEach(async ({ page }) => { 168 + await createNewSheet(page); 169 + await seedSheetData(page); 170 + }); 171 + 172 + test('open gallery view', async ({ page }) => { 173 + await openOverflowMenu(page); 174 + await page.click('#tb-view-mode'); 175 + 176 + // Switch to gallery type 177 + await page.locator('#dbview-type').selectOption('gallery'); 178 + await page.click('#dbview-ok'); 179 + 180 + await expect(page.locator('#database-view-section')).toBeVisible(); 181 + await expect(page.locator('.db-view-toolbar')).toContainText('Gallery View'); 182 + await expect(page.locator('.gallery-grid')).toBeVisible(); 183 + }); 184 + 185 + test('gallery shows cards for each data row', async ({ page }) => { 186 + await openOverflowMenu(page); 187 + await page.click('#tb-view-mode'); 188 + await page.locator('#dbview-type').selectOption('gallery'); 189 + await page.click('#dbview-ok'); 190 + 191 + const cards = page.locator('.gallery-card'); 192 + await expect(cards).toHaveCount(3); // 3 data rows 193 + }); 194 + }); 195 + 196 + test.describe('Database Views - Calendar', () => { 197 + test.beforeEach(async ({ page }) => { 198 + await createNewSheet(page); 199 + await seedSheetData(page); 200 + }); 201 + 202 + test('open calendar view grouped by date column', async ({ page }) => { 203 + await openOverflowMenu(page); 204 + await page.click('#tb-view-mode'); 205 + 206 + // Switch to calendar, group by column C (Date) 207 + await page.locator('#dbview-type').selectOption('calendar'); 208 + await page.locator('#dbview-group').selectOption('3'); 209 + await page.click('#dbview-ok'); 210 + 211 + await expect(page.locator('#database-view-section')).toBeVisible(); 212 + await expect(page.locator('.db-view-toolbar')).toContainText('Calendar View'); 213 + }); 214 + 215 + test('calendar shows month sections', async ({ page }) => { 216 + await openOverflowMenu(page); 217 + await page.click('#tb-view-mode'); 218 + await page.locator('#dbview-type').selectOption('calendar'); 219 + await page.locator('#dbview-group').selectOption('3'); 220 + await page.click('#dbview-ok'); 221 + 222 + // Should have at least one month section 223 + const months = page.locator('.calendar-month'); 224 + const count = await months.count(); 225 + expect(count).toBeGreaterThanOrEqual(1); 226 + }); 227 + }); 228 + 229 + test.describe('Pivot Table - Dialog', () => { 230 + test.beforeEach(async ({ page }) => { 231 + await createNewSheet(page); 232 + await seedSheetData(page); 233 + }); 234 + 235 + test('pivot table button opens dialog', async ({ page }) => { 236 + await openOverflowMenu(page); 237 + await page.click('#tb-pivot'); 238 + 239 + await expect(page.locator('.pivot-dialog-overlay')).toBeVisible(); 240 + await expect(page.locator('.sheet-dialog h3')).toContainText('Pivot'); 241 + }); 242 + 243 + test('cancel closes pivot dialog', async ({ page }) => { 244 + await openOverflowMenu(page); 245 + await page.click('#tb-pivot'); 246 + await expect(page.locator('.pivot-dialog-overlay')).toBeVisible(); 247 + 248 + await page.click('#pivot-cancel'); 249 + await expect(page.locator('.pivot-dialog-overlay')).not.toBeVisible(); 250 + }); 251 + 252 + test('create a pivot table', async ({ page }) => { 253 + await openOverflowMenu(page); 254 + await page.click('#tb-pivot'); 255 + 256 + // Select row field (column A - Status) — required for pivot creation 257 + await page.locator('#pivot-rows').selectOption('1'); 258 + await page.click('#pivot-ok'); 259 + 260 + // A pivot table should appear in the pivot section 261 + await expect(page.locator('#pivot-section .pivot-table')).toBeVisible(); 262 + }); 263 + 264 + test('pivot table has edit and delete actions', async ({ page }) => { 265 + await openOverflowMenu(page); 266 + await page.click('#tb-pivot'); 267 + await page.locator('#pivot-rows').selectOption('1'); 268 + await page.click('#pivot-ok'); 269 + 270 + await expect(page.locator('#pivot-section .pivot-edit')).toBeVisible(); 271 + await expect(page.locator('#pivot-section .pivot-delete')).toBeVisible(); 272 + }); 273 + 274 + test('delete removes pivot table', async ({ page }) => { 275 + await openOverflowMenu(page); 276 + await page.click('#tb-pivot'); 277 + await page.locator('#pivot-rows').selectOption('1'); 278 + await page.click('#pivot-ok'); 279 + await expect(page.locator('#pivot-section .pivot-table')).toBeVisible(); 280 + 281 + await page.click('.pivot-delete'); 282 + // Pivot table should be gone 283 + await expect(page.locator('#pivot-section .pivot-table')).toHaveCount(0); 284 + }); 285 + });
+8 -12
e2e/helpers.ts
··· 60 60 */ 61 61 export async function createNewSlides(page: Page): Promise<string> { 62 62 await goToLanding(page); 63 - // Click triggers async createDocument (fetch + window.location.href), so wait for navigation 64 - await Promise.all([ 65 - page.waitForURL(/\/slides\//, { timeout: 15000 }), 66 - page.click('#new-slide'), 67 - ]); 68 - // Wait for the slide canvas to render 69 - await page.waitForSelector('#slide-canvas', { timeout: 15000 }); 70 - // Wait for thumbnails to populate (at least one slide) 71 - await page.waitForSelector('.slides-thumbnail', { timeout: 15000 }); 63 + // Click triggers async createDocument (fetch then window.location.href) 64 + await page.click('#new-slide'); 65 + // Wait for slide canvas to render after navigation completes 66 + await page.waitForSelector('#slide-canvas', { timeout: 30000 }); 72 67 return page.url(); 73 68 } 74 69 ··· 78 73 */ 79 74 export async function createNewForm(page: Page): Promise<string> { 80 75 await goToLanding(page); 76 + // Click triggers async createDocument (fetch then window.location.href) 81 77 await page.click('#new-form'); 82 - // Wait for the form builder toolbar to render 83 - await page.waitForSelector('#form-toolbar', { timeout: 15000 }); 78 + // Wait for form builder toolbar to render after navigation completes 79 + await page.waitForSelector('#form-toolbar', { timeout: 30000 }); 84 80 return page.url(); 85 81 } 86 82 ··· 92 88 await goToLanding(page); 93 89 await page.click('#new-diagram'); 94 90 // Wait for the SVG canvas to render 95 - await page.waitForSelector('#diagram-canvas', { timeout: 15000 }); 91 + await page.waitForSelector('#diagram-canvas', { timeout: 30000 }); 96 92 return page.url(); 97 93 } 98 94