main
ts 344 lines 13.9 KB
Raw
1 import { expect, Page, test } from '@playwright/test'
2 import { ADMIN_URL, clickAdminMenu, username, password, clickIconBtn } from './common'
3
4 async function selectVfsNode(page: Page, name: string, expectedId: string) {
5 await page.getByRole('treeitem', { name, exact: true }).click()
6 await expect.poll(() => page.evaluate(() => (window as any).state?.selectedFiles?.[0]?.id || ''))
7 .toBe(expectedId)
8 }
9
10 async function pasteMovingNode(page: Page, movingName: string) {
11 await clickIconBtn(new RegExp(movingName), page)
12 }
13
14 async function expandVfsNode(page: Page, nodeId: string) {
15 // Mobile uses a details dialog for selection, so forcing expansion via app state keeps this test focused on move behavior.
16 await expect.poll(() => page.evaluate(id => {
17 const state = (window as any).state
18 if (!state) return false
19 if (!state.expanded.includes(id))
20 state.expanded = [...state.expanded, id]
21 return state.expanded.includes(id)
22 }, nodeId)).toBe(true)
23 }
24
25 test('move via cut/paste keeps node visible', async ({ page }) => {
26 await page.goto(ADMIN_URL)
27 await page.getByRole('textbox', { name: 'Username' }).fill(username)
28 await page.getByRole('textbox', { name: 'Password' }).fill(password)
29 await page.getByRole('textbox', { name: 'Password' }).press('Enter')
30 await clickAdminMenu(page, /Shared files/)
31 await page.getByText('zipNoList', { exact: true }).waitFor({ timeout: 10_000 })
32
33 await selectVfsNode(page, 'zipNoList', '/zipNoList/')
34 await clickIconBtn('Cut', page)
35 await clickIconBtn('Close', page)
36 await selectVfsNode(page, 'f1', '/f1/')
37 await pasteMovingNode(page, 'zipNoList')
38
39 await expect.poll(() => page.evaluate(() => {
40 function find(node: any, name: string): any {
41 if (!node) return
42 if (node.name === name) return node
43 for (const child of node.children || []) {
44 const found = find(child, name)
45 if (found) return found
46 }
47 }
48 const root = (window as any).state?.vfs
49 const moved = find(root, 'zipNoList')
50 const destination = find(root, 'f1')
51 return {
52 rootHasMoved: (root?.children || []).some((x: any) => x.name === 'zipNoList'),
53 destinationHasMoved: (destination?.children || []).some((x: any) => x.name === 'zipNoList'),
54 movedParent: moved?.parent?.id,
55 }
56 })).toEqual({
57 rootHasMoved: false,
58 destinationHasMoved: true,
59 movedParent: '/f1/',
60 })
61 })
62
63 test('move to nested destination expands ancestors', async ({ page }) => {
64 await page.goto(ADMIN_URL)
65 await page.getByRole('textbox', { name: 'Username' }).fill(username)
66 await page.getByRole('textbox', { name: 'Password' }).fill(password)
67 await page.getByRole('textbox', { name: 'Password' }).press('Enter')
68 await clickAdminMenu(page, /Shared files/)
69 await page.getByText('zipNoList', { exact: true }).waitFor({ timeout: 10_000 })
70
71 await selectVfsNode(page, 'zipNoList', '/zipNoList/')
72 await clickIconBtn('Cut', page)
73 await clickIconBtn('Close', page)
74 await expandVfsNode(page, '/protectFromAbove/')
75 await selectVfsNode(page, 'child', '/protectFromAbove/child/')
76 await pasteMovingNode(page, 'zipNoList')
77
78 await expect.poll(() => page.evaluate(() => {
79 function find(node: any, name: string): any {
80 if (!node) return
81 if (node.name === name) return node
82 for (const child of node.children || []) {
83 const found = find(child, name)
84 if (found) return found
85 }
86 }
87 const root = (window as any).state?.vfs
88 const moved = find(root, 'zipNoList')
89 const destination = find(root, 'child')
90 const expanded = (window as any).state?.expanded || []
91 return {
92 destinationHasMoved: (destination?.children || []).some((x: any) => x.name === 'zipNoList'),
93 movedParent: moved?.parent?.id,
94 hasDestinationExpanded: expanded.includes('/protectFromAbove/child/'),
95 hasAncestorExpanded: expanded.includes('/protectFromAbove/'),
96 }
97 })).toEqual({
98 destinationHasMoved: true,
99 movedParent: '/protectFromAbove/child/',
100 hasDestinationExpanded: true,
101 hasAncestorExpanded: true,
102 })
103 })
104
105 test('move into empty folder keeps node visible', async ({ page }) => {
106 await page.goto(ADMIN_URL)
107 await page.getByRole('textbox', { name: 'Username' }).fill(username)
108 await page.getByRole('textbox', { name: 'Password' }).fill(password)
109 await page.getByRole('textbox', { name: 'Password' }).press('Enter')
110 await clickAdminMenu(page, /Shared files/)
111 await page.getByText('zipNoList', { exact: true }).waitFor({ timeout: 10_000 })
112
113 await selectVfsNode(page, 'zipNoList', '/zipNoList/')
114 await clickIconBtn('Cut', page)
115 await clickIconBtn('Close', page)
116 await selectVfsNode(page, 'for-disabled', '/for-disabled/')
117 await pasteMovingNode(page, 'zipNoList')
118
119 await expect.poll(() => page.evaluate(() => {
120 function find(node: any, name: string): any {
121 if (!node) return
122 if (node.name === name) return node
123 for (const child of node.children || []) {
124 const found = find(child, name)
125 if (found) return found
126 }
127 }
128 const root = (window as any).state?.vfs
129 const moved = find(root, 'zipNoList')
130 const destination = find(root, 'for-disabled')
131 return {
132 rootHasMoved: (root?.children || []).some((x: any) => x.name === 'zipNoList'),
133 destinationHasMoved: (destination?.children || []).some((x: any) => x.name === 'zipNoList'),
134 movedParent: moved?.parent?.id,
135 }
136 })).toEqual({
137 rootHasMoved: false,
138 destinationHasMoved: true,
139 movedParent: '/for-disabled/',
140 })
141 })
142
143 test('delete virtual folder updates tree and marks modified', async ({ page }) => {
144 await page.goto(ADMIN_URL)
145 await page.getByRole('textbox', { name: 'Username' }).fill(username)
146 await page.getByRole('textbox', { name: 'Password' }).fill(password)
147 await page.getByRole('textbox', { name: 'Password' }).press('Enter')
148 await clickAdminMenu(page, /Shared files/)
149 await page.getByText('zipNoList', { exact: true }).waitFor({ timeout: 10_000 })
150
151 const folderName = 'for-disabled'
152 await selectVfsNode(page, folderName, '/for-disabled/')
153 await clickIconBtn('Delete', page)
154
155 await expect.poll(() => page.evaluate(name => {
156 const root = (window as any).state?.vfs
157 return {
158 hasFolder: (root?.children || []).some((x: any) => x.name === name),
159 modified: (window as any).state?.vfsModified,
160 }
161 }, folderName)).toEqual({
162 hasFolder: false,
163 modified: true,
164 })
165 })
166
167 test('undo toggles with single-level redo behavior', async ({ page }) => {
168 await page.goto(ADMIN_URL)
169 await page.getByRole('textbox', { name: 'Username' }).fill(username)
170 await page.getByRole('textbox', { name: 'Password' }).fill(password)
171 await page.getByRole('textbox', { name: 'Password' }).press('Enter')
172 await clickAdminMenu(page, /Shared files/)
173 await page.getByText('zipNoList', { exact: true }).waitFor({ timeout: 10_000 })
174
175 const folderName = 'for-disabled'
176 const undoButton = page.getByRole('button', { name: 'Undo' }).first()
177 await expect(undoButton).toBeDisabled()
178
179 await selectVfsNode(page, folderName, '/for-disabled/')
180 await clickIconBtn('Delete', page)
181 await expect(undoButton).toBeEnabled()
182
183 await expect.poll(() => page.evaluate(name => {
184 const root = (window as any).state?.vfs
185 return {
186 hasFolder: (root?.children || []).some((x: any) => x.name === name),
187 modified: (window as any).state?.vfsModified,
188 }
189 }, folderName)).toEqual({
190 hasFolder: false,
191 modified: true,
192 })
193
194 await undoButton.click()
195 await expect.poll(() => page.evaluate(name => {
196 const root = (window as any).state?.vfs
197 return {
198 hasFolder: (root?.children || []).some((x: any) => x.name === name),
199 modified: (window as any).state?.vfsModified,
200 }
201 }, folderName)).toEqual({
202 hasFolder: true,
203 modified: true,
204 })
205
206 await undoButton.click()
207 await expect.poll(() => page.evaluate(name => {
208 const root = (window as any).state?.vfs
209 return {
210 hasFolder: (root?.children || []).some((x: any) => x.name === name),
211 modified: (window as any).state?.vfsModified,
212 }
213 }, folderName)).toEqual({
214 hasFolder: false,
215 modified: true,
216 })
217 })
218
219 test('apply keeps unset permissions nullish in-memory', async ({ page }) => {
220 await page.goto(ADMIN_URL)
221 await page.getByRole('textbox', { name: 'Username' }).fill(username)
222 await page.getByRole('textbox', { name: 'Password' }).fill(password)
223 await page.getByRole('textbox', { name: 'Password' }).press('Enter')
224 await clickAdminMenu(page, /Shared files/)
225 await page.getByText('zipNoList', { exact: true }).waitFor({ timeout: 10_000 })
226
227 await selectVfsNode(page, 'f1', '/f1/')
228 await page.locator('button:has-text("Apply")').click()
229
230 await expect.poll(() => page.evaluate(() => {
231 function findById(node: any, id: string): any {
232 if (!node) return
233 if (node.id === id) return node
234 for (const child of node.children || []) {
235 const found = findById(child, id)
236 if (found) return found
237 }
238 }
239 // On mobile, applying from the details dialog can clear selectedFiles on dialog close; assert against canonical VFS node instead.
240 const node = findById((window as any).state?.vfs, '/f1/')
241 const unsetPerms = ['can_see', 'can_read', 'can_list', 'can_upload', 'can_delete', 'can_archive']
242 .filter(k => node?.[k] == null)
243 return { id: node?.id, unsetPerms }
244 })).toEqual({
245 id: '/f1/',
246 unsetPerms: ['can_see', 'can_read', 'can_list', 'can_upload', 'can_delete', 'can_archive'],
247 })
248 })
249
250 test('apply refreshes inherited permissions for descendants in-memory', async ({ page }) => {
251 await page.goto(ADMIN_URL)
252 await page.getByRole('textbox', { name: 'Username' }).fill(username)
253 await page.getByRole('textbox', { name: 'Password' }).fill(password)
254 await page.getByRole('textbox', { name: 'Password' }).press('Enter')
255 await clickAdminMenu(page, /Shared files/)
256 await page.getByText('zipNoList', { exact: true }).waitFor({ timeout: 10_000 })
257
258 await expect.poll(() => page.evaluate(() => {
259 const state = (window as any).state
260 if (!state?.vfs) return ''
261 state.selectedFiles = [state.vfs]
262 return state.selectedFiles[0]?.id || ''
263 })).toBe('/')
264 await page.getByRole('combobox', { name: 'Who can download' }).click()
265 await page.getByRole('option', { name: 'No one' }).click()
266 await page.locator('button:has-text("Apply")').click()
267
268 await expect.poll(() => page.evaluate(() => {
269 function findById(node: any, id: string): any {
270 if (!node) return
271 if (node.id === id) return node
272 for (const child of node.children || []) {
273 const found = findById(child, id)
274 if (found) return found
275 }
276 }
277 const node = findById((window as any).state?.vfs, '/f1/')
278 return {
279 rootCanRead: (window as any).state?.vfs?.can_read,
280 inheritedCanRead: node?.inherited?.can_read,
281 }
282 })).toEqual({
283 rootCanRead: false,
284 inheritedCanRead: false,
285 })
286
287 await selectVfsNode(page, 'f1', '/f1/')
288 await page.locator('button:has-text("Apply")').click()
289
290 await expect.poll(() => page.evaluate(() => {
291 function findById(node: any, id: string): any {
292 if (!node) return
293 if (node.id === id) return node
294 for (const child of node.children || []) {
295 const found = findById(child, id)
296 if (found) return found
297 }
298 }
299 const node = findById((window as any).state?.vfs, '/f1/f2/')
300 return {
301 middleCanRead: findById((window as any).state?.vfs, '/f1/')?.can_read,
302 inheritedCanRead: node?.inherited?.can_read,
303 }
304 })).toEqual({
305 middleCanRead: null,
306 inheritedCanRead: false,
307 })
308 })
309
310 test('tree expands from icon click and ArrowRight', async ({ page }) => {
311 await page.goto(ADMIN_URL)
312 await page.getByRole('textbox', { name: 'Username' }).fill(username)
313 await page.getByRole('textbox', { name: 'Password' }).fill(password)
314 await page.getByRole('textbox', { name: 'Password' }).press('Enter')
315 await clickAdminMenu(page, /Shared files/)
316 await page.getByText('zipNoList', { exact: true }).waitFor({ timeout: 10_000 })
317
318 const nodeId = '/'
319 const nodeName = 'Home folder'
320 const item = page.getByRole('treeitem', { name: nodeName, exact: true })
321
322 // start from collapsed to verify the actual interaction path
323 await page.evaluate(id => {
324 const state = (window as any).state
325 if (!state) return
326 state.expanded = state.expanded.filter((x: string) => x !== id)
327 }, nodeId)
328
329 await item.locator(':scope > .MuiTreeItem-content > .MuiTreeItem-iconContainer').click()
330 await expect.poll(() => page.evaluate(id => (window as any).state?.expanded?.includes(id) || false, nodeId))
331 .toBe(true)
332
333 if (!['Android', 'iPhone 6'].includes(test.info().project.name)) {
334 await page.evaluate(id => {
335 const state = (window as any).state
336 if (!state) return
337 state.expanded = state.expanded.filter((x: string) => x !== id)
338 }, nodeId)
339 await item.click()
340 await item.press('ArrowRight')
341 await expect.poll(() => page.evaluate(id => (window as any).state?.expanded?.includes(id) || false, nodeId))
342 .toBe(true)
343 }
344 })