|
2 | 2 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors |
3 | 3 | * SPDX-License-Identifier: AGPL-3.0-or-later |
4 | 4 | */ |
5 | | -import { getRowForFile } from './FilesUtils.ts' |
| 5 | +import type { User } from '@nextcloud/e2e-test-server/cypress' |
| 6 | + |
| 7 | +import { getRowForFile, navigateToFolder } from './FilesUtils.ts' |
6 | 8 |
|
7 | 9 | describe('files: Drag and Drop', { testIsolation: true }, () => { |
8 | 10 | beforeEach(() => { |
@@ -138,3 +140,95 @@ describe('files: Drag and Drop', { testIsolation: true }, () => { |
138 | 140 | getRowForFile('Bar').should('not.exist') |
139 | 141 | }) |
140 | 142 | }) |
| 143 | + |
| 144 | +// Regression coverage for https://github.com/nextcloud/server/issues/60139 |
| 145 | +// The per-row drop handler in FileEntryMixin used to pass raw FileSystemEntry |
| 146 | +// objects to @nextcloud/upload's batchUpload; on some Chromium builds the |
| 147 | +// instanceof-based conversion silently failed and the chunk uploader crashed |
| 148 | +// with "e.slice is not a function". The fix routes the per-row drop through |
| 149 | +// the same dataTransferToFileTree pipeline as the main file-list drop. |
| 150 | +// |
| 151 | +// Sibling describe (not nested) so the outer suite's `beforeEach` doesn't |
| 152 | +// spin up an unused user before each test in this block. |
| 153 | +describe('files: Drag and Drop onto a folder row', { testIsolation: true }, () => { |
| 154 | + let user: User |
| 155 | + |
| 156 | + beforeEach(() => { |
| 157 | + cy.createRandomUser().then((u) => { |
| 158 | + user = u |
| 159 | + cy.mkdir(user, '/subfolder') |
| 160 | + cy.login(user) |
| 161 | + }) |
| 162 | + cy.visit('/apps/files') |
| 163 | + getRowForFile('subfolder').should('be.visible') |
| 164 | + }) |
| 165 | + |
| 166 | + it('can drop a single file onto a subfolder row', () => { |
| 167 | + cy.intercept('PUT', /\/remote.php\/dav\/files\//).as('uploadFile') |
| 168 | + |
| 169 | + getRowForFile('subfolder').selectFile({ |
| 170 | + fileName: 'dropped-into-subfolder.txt', |
| 171 | + contents: ['hello '.repeat(1024)], |
| 172 | + }, { action: 'drag-drop' }) |
| 173 | + |
| 174 | + cy.wait('@uploadFile').its('request.url') |
| 175 | + .should('match', /\/subfolder\/dropped-into-subfolder\.txt$/) |
| 176 | + |
| 177 | + cy.get('[data-cy-upload-picker] progress').should('not.be.visible') |
| 178 | + |
| 179 | + navigateToFolder('/subfolder') |
| 180 | + getRowForFile('dropped-into-subfolder.txt').should('be.visible') |
| 181 | + }) |
| 182 | + |
| 183 | + it('can drop multiple files onto a subfolder row', () => { |
| 184 | + cy.intercept('PUT', /\/remote.php\/dav\/files\//).as('uploadFile') |
| 185 | + |
| 186 | + getRowForFile('subfolder').selectFile([ |
| 187 | + { fileName: 'one.txt', contents: ['A'.repeat(1024)] }, |
| 188 | + { fileName: 'two.txt', contents: ['B'.repeat(1024)] }, |
| 189 | + ], { action: 'drag-drop' }) |
| 190 | + |
| 191 | + // Both files must land under the subfolder, not the current dir. |
| 192 | + cy.wait(['@uploadFile', '@uploadFile']).then((intercepts) => { |
| 193 | + const urls = intercepts.map((i) => i.request.url).sort() |
| 194 | + expect(urls).to.have.length(2) |
| 195 | + urls.forEach((url) => { |
| 196 | + expect(url).to.match(/\/subfolder\/(one|two)\.txt$/) |
| 197 | + }) |
| 198 | + }) |
| 199 | + |
| 200 | + cy.get('[data-cy-upload-picker] progress').should('not.be.visible') |
| 201 | + |
| 202 | + navigateToFolder('/subfolder') |
| 203 | + getRowForFile('one.txt').should('be.visible') |
| 204 | + getRowForFile('two.txt').should('be.visible') |
| 205 | + }) |
| 206 | + |
| 207 | + it('opens the conflict picker when dropping a colliding name onto a subfolder row', () => { |
| 208 | + // Pre-populate the subfolder with a file the drop will collide with. |
| 209 | + cy.uploadContent(user, new Blob(['original']), 'text/plain', '/subfolder/collide.txt') |
| 210 | + |
| 211 | + // Reload so the pre-populated file lands in the store before the drop. |
| 212 | + // The drop handler reads filesStore.getNodesByPath first and only |
| 213 | + // fetches fresh contents when the cache is empty, so a stale cache |
| 214 | + // from the beforeEach visit would let the upload proceed without |
| 215 | + // triggering the conflict picker. If this ever flaps on CI, replace |
| 216 | + // the visit with cy.reload() + an explicit wait on store settlement. |
| 217 | + cy.visit('/apps/files') |
| 218 | + getRowForFile('subfolder').should('be.visible') |
| 219 | + |
| 220 | + cy.intercept('PUT', /\/remote.php\/dav\/files\//).as('uploadFile') |
| 221 | + |
| 222 | + getRowForFile('subfolder').selectFile({ |
| 223 | + fileName: 'collide.txt', |
| 224 | + contents: ['replacement '.repeat(1024)], |
| 225 | + }, { action: 'drag-drop' }) |
| 226 | + |
| 227 | + // Wait for the conflict picker to appear, then assert no PUT has |
| 228 | + // fired yet — chained so the upload-count check happens *after* the |
| 229 | + // dialog is visible, enforcing the "dialog blocks upload" invariant. |
| 230 | + cy.findByRole('dialog').should('be.visible').then(() => { |
| 231 | + cy.get('@uploadFile.all').should('have.length', 0) |
| 232 | + }) |
| 233 | + }) |
| 234 | +}) |
0 commit comments