Full document, spreadsheet, slideshow, and diagram tooling
1import { test, expect } from '@playwright/test';
2import { createNewSheet, clickCell, typeInCell, getCellText } from './helpers';
3
4test.describe('Sheets - Advanced Features', () => {
5 test.beforeEach(async ({ page }) => {
6 await createNewSheet(page);
7 });
8
9 test('multi-sheet tabs: create new sheet and switch between tabs', async ({ page }) => {
10 // Should start with "Sheet 1" tab
11 await expect(page.locator('.sheet-tab').first()).toContainText('Sheet 1');
12
13 // Add a new sheet
14 await page.click('#add-sheet');
15
16 // Should now have two tabs
17 const tabs = page.locator('.sheet-tab');
18 await expect(tabs).toHaveCount(2);
19 await expect(tabs.last()).toContainText('Sheet 2');
20
21 // Type something in Sheet 2
22 await typeInCell(page, 'A1', 'Sheet 2 Data');
23 expect(await getCellText(page, 'A1')).toBe('Sheet 2 Data');
24
25 // Switch back to Sheet 1
26 await tabs.first().click();
27
28 // A1 should be empty on Sheet 1
29 const text = await getCellText(page, 'A1');
30 expect(text).toBe('');
31
32 // Switch back to Sheet 2 and verify data persisted
33 await tabs.last().click();
34 expect(await getCellText(page, 'A1')).toBe('Sheet 2 Data');
35 });
36
37 test('column resize by dragging column border', async ({ page }) => {
38 // Get initial width of column A
39 const colHeader = page.locator('thead th[data-col="1"]');
40 const initialBox = await colHeader.boundingBox();
41 expect(initialBox).toBeTruthy();
42
43 // Find the resize handle
44 const handle = page.locator('.col-resize-handle[data-resize-col="1"]');
45 await expect(handle).toBeVisible();
46
47 // Drag the handle to resize
48 const handleBox = await handle.boundingBox();
49 expect(handleBox).toBeTruthy();
50
51 await page.mouse.move(handleBox!.x + handleBox!.width / 2, handleBox!.y + handleBox!.height / 2);
52 await page.mouse.down();
53 await page.mouse.move(handleBox!.x + 50, handleBox!.y + handleBox!.height / 2, { steps: 5 });
54 await page.mouse.up();
55
56 // Column should be wider now
57 const newBox = await colHeader.boundingBox();
58 expect(newBox!.width).toBeGreaterThan(initialBox!.width);
59 });
60
61 test('right-click context menu appears on cell', async ({ page }) => {
62 await clickCell(page, 'A1');
63 await page.click('td[data-id="A1"]', { button: 'right' });
64
65 // Context menu should appear
66 const contextMenu = page.locator('.context-menu');
67 await expect(contextMenu).toBeVisible({ timeout: 5000 });
68
69 // Should have menu items
70 await expect(contextMenu.locator('.context-menu-item, [role="menuitem"]').first()).toBeVisible();
71
72 // Dismiss by clicking elsewhere
73 await page.keyboard.press('Escape');
74 });
75
76 test('cell merge via toolbar button', async ({ page }) => {
77 // Select a range A1:B2
78 await clickCell(page, 'A1');
79 await page.keyboard.down('Shift');
80 await clickCell(page, 'B2');
81 await page.keyboard.up('Shift');
82
83 // Click merge button
84 await page.click('#tb-merge');
85
86 // The merged cell should have colspan/rowspan attributes
87 const mergedTd = page.locator('td[data-id="A1"]');
88 const colspan = await mergedTd.getAttribute('colspan');
89 const rowspan = await mergedTd.getAttribute('rowspan');
90 expect(colspan).toBe('2');
91 expect(rowspan).toBe('2');
92 });
93
94 test('freeze rows via overflow menu', async ({ page }) => {
95 // Select row 2
96 await clickCell(page, 'A2');
97
98 // Open overflow menu
99 await page.click('#overflow-toggle');
100 await expect(page.locator('.toolbar-overflow-menu')).toBeVisible();
101
102 // Click freeze rows
103 await page.click('#tb-freeze-rows');
104
105 // There should be frozen row styling on row 1
106 await expect(page.locator('td.frozen-row').first()).toBeVisible({ timeout: 5000 });
107 });
108
109 test('freeze columns via overflow menu', async ({ page }) => {
110 // Select column B (col 2)
111 await clickCell(page, 'B1');
112
113 // Open overflow menu
114 await page.click('#overflow-toggle');
115 await expect(page.locator('.toolbar-overflow-menu')).toBeVisible();
116
117 // Click freeze cols
118 await page.click('#tb-freeze-cols');
119
120 // Column A should be frozen
121 await expect(page.locator('td.frozen-col').first()).toBeVisible({ timeout: 5000 });
122 });
123
124 test('conditional formatting dialog opens', async ({ page }) => {
125 await clickCell(page, 'A1');
126
127 // Open overflow menu
128 await page.click('#overflow-toggle');
129 await expect(page.locator('.toolbar-overflow-menu')).toBeVisible();
130
131 // Click conditional formatting
132 await page.click('#tb-cf');
133
134 // A dialog/modal should appear
135 const cfDialog = page.locator('.cf-dialog, .modal, [class*="conditional"]');
136 await expect(cfDialog.first()).toBeVisible({ timeout: 5000 });
137 });
138
139 test('data validation dialog opens', async ({ page }) => {
140 await clickCell(page, 'A1');
141
142 // Open overflow menu
143 await page.click('#overflow-toggle');
144 await expect(page.locator('.toolbar-overflow-menu')).toBeVisible();
145
146 // Click data validation
147 await page.click('#tb-validation');
148
149 // A dialog/modal should appear
150 const validationDialog = page.locator('.validation-dialog, .modal, [class*="validation"]');
151 await expect(validationDialog.first()).toBeVisible({ timeout: 5000 });
152 });
153
154 test('chart button opens chart creation', async ({ page }) => {
155 // Fill some data for chart
156 await typeInCell(page, 'A1', 'Q1');
157 await typeInCell(page, 'A2', 'Q2');
158 await typeInCell(page, 'A3', 'Q3');
159 await typeInCell(page, 'B1', '100');
160 await typeInCell(page, 'B2', '200');
161 await typeInCell(page, 'B3', '150');
162
163 // Select the data range
164 await clickCell(page, 'A1');
165 await page.keyboard.down('Shift');
166 await clickCell(page, 'B3');
167 await page.keyboard.up('Shift');
168
169 // Click chart button
170 await page.click('#tb-chart');
171
172 // Chart dialog or chart element should appear
173 const chartDialog = page.locator('.chart-dialog, .modal, [class*="chart"]');
174 await expect(chartDialog.first()).toBeVisible({ timeout: 5000 });
175 });
176
177 test('sort ascending via toolbar button', async ({ page }) => {
178 await typeInCell(page, 'A1', 'Charlie');
179 await typeInCell(page, 'A2', 'Alice');
180 await typeInCell(page, 'A3', 'Bob');
181
182 // Select column A
183 await clickCell(page, 'A1');
184
185 // Click sort ascending
186 await page.click('#tb-sort-asc');
187
188 // After sort, A1 should be Alice
189 expect(await getCellText(page, 'A1')).toBe('Alice');
190 expect(await getCellText(page, 'A2')).toBe('Bob');
191 expect(await getCellText(page, 'A3')).toBe('Charlie');
192 });
193
194 test('sort descending via toolbar button', async ({ page }) => {
195 await typeInCell(page, 'A1', 'Alice');
196 await typeInCell(page, 'A2', 'Charlie');
197 await typeInCell(page, 'A3', 'Bob');
198
199 await clickCell(page, 'A1');
200 await page.click('#tb-sort-desc');
201
202 expect(await getCellText(page, 'A1')).toBe('Charlie');
203 expect(await getCellText(page, 'A2')).toBe('Bob');
204 expect(await getCellText(page, 'A3')).toBe('Alice');
205 });
206
207 test('striped rows toggle via overflow menu', async ({ page }) => {
208 // Open overflow menu
209 await page.click('#overflow-toggle');
210 await expect(page.locator('.toolbar-overflow-menu')).toBeVisible();
211
212 // Click striped rows
213 await page.click('#tb-striped');
214
215 // Even rows should have striped class
216 await expect(page.locator('td.striped-row').first()).toBeVisible({ timeout: 5000 });
217 });
218
219 test('status bar shows selection stats', async ({ page }) => {
220 await typeInCell(page, 'A1', '10');
221 await typeInCell(page, 'A2', '20');
222 await typeInCell(page, 'A3', '30');
223
224 // Select range A1:A3
225 await clickCell(page, 'A1');
226 await page.keyboard.down('Shift');
227 await clickCell(page, 'A3');
228 await page.keyboard.up('Shift');
229
230 // Status bar should show stats (sum, average, count)
231 const statusBar = page.locator('#status-bar');
232 await expect(statusBar).toBeVisible({ timeout: 5000 });
233 const statsText = await page.locator('#status-bar-stats').textContent();
234 expect(statsText).toContain('60'); // sum
235 });
236});