test(files): Add e2e tests for live photo sync

Signed-off-by: Louis Chemineau <louis@chmn.me>
pull/44069/head
Louis Chemineau 2 months ago
parent 2de9880d79
commit 01fe326df1
No known key found for this signature in database

@ -26,23 +26,28 @@
@update:open="onClose">
<!-- Settings API-->
<NcAppSettingsSection id="settings" :name="t('files', 'Files settings')">
<NcCheckboxRadioSwitch :checked="userConfig.sort_favorites_first"
<NcCheckboxRadioSwitch data-cy-files-settings-setting="sort_favorites_first"
:checked="userConfig.sort_favorites_first"
@update:checked="setConfig('sort_favorites_first', $event)">
{{ t('files', 'Sort favorites first') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked="userConfig.sort_folders_first"
<NcCheckboxRadioSwitch data-cy-files-settings-setting="sort_folders_first"
:checked="userConfig.sort_folders_first"
@update:checked="setConfig('sort_folders_first', $event)">
{{ t('files', 'Sort folders before files') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked="userConfig.show_hidden"
<NcCheckboxRadioSwitch data-cy-files-settings-setting="show_hidden"
:checked="userConfig.show_hidden"
@update:checked="setConfig('show_hidden', $event)">
{{ t('files', 'Show hidden files') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked="userConfig.crop_image_previews"
<NcCheckboxRadioSwitch data-cy-files-settings-setting="crop_image_previews"
:checked="userConfig.crop_image_previews"
@update:checked="setConfig('crop_image_previews', $event)">
{{ t('files', 'Crop image previews') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-if="enableGridView"
data-cy-files-settings-setting="grid_view"
:checked="userConfig.grid_view"
@update:checked="setConfig('grid_view', $event)">
{{ t('files', 'Enable the grid view') }}

@ -23,6 +23,7 @@
<template>
<NcAppSidebar v-if="file"
ref="sidebar"
cy-data-sidebar
v-bind="appSidebar"
:force-menu="true"
@close="close"

@ -20,17 +20,31 @@
*
*/
export const getRowForFileId = (fileid: number) => cy.get(`[data-cy-files-list-row-fileid="${fileid}"]`)
export const getRowForFile = (filename: string) => cy.get(`[data-cy-files-list-row-name="${CSS.escape(filename)}"]`)
export const getActionsForFileId = (fileid: number) => getRowForFileId(fileid).find('[data-cy-files-list-row-actions]')
export const getActionsForFile = (filename: string) => getRowForFile(filename).find('[data-cy-files-list-row-actions]')
export const getActionButtonForFileId = (fileid: number) => getActionsForFileId(fileid).find('button[aria-label="Actions"]')
export const getActionButtonForFile = (filename: string) => getActionsForFile(filename).find('button[aria-label="Actions"]')
export const triggerActionForFileId = (fileid: number, actionId: string) => {
getActionButtonForFileId(fileid).click()
cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).should('exist').click()
}
export const triggerActionForFile = (filename: string, actionId: string) => {
getActionButtonForFile(filename).click()
cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).should('exist').click()
}
export const triggerInlineActionForFileId = (fileid: number, actionId: string) => {
getActionsForFileId(fileid).find(`button[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`).should('exist').click()
}
export const triggerInlineActionForFile = (filename: string, actionId: string) => {
getActionsForFile(filename).get(`button[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`).should('exist').click()
}
export const moveFile = (fileName: string, dirName: string) => {
getRowForFile(fileName).should('be.visible')
triggerActionForFile(fileName, 'move-copy')
@ -85,6 +99,23 @@ export const copyFile = (fileName: string, dirName: string) => {
})
}
export const renameFile = (fileName: string, newFileName: string) => {
getRowForFile(fileName)
triggerActionForFile(fileName, 'rename')
// intercept the move so we can wait for it
cy.intercept('MOVE', /\/remote.php\/dav\/files\//).as('moveFile')
getRowForFile(fileName).find('[data-cy-files-list-row-name] input').clear()
getRowForFile(fileName).find('[data-cy-files-list-row-name] input').type(`${newFileName}{enter}`)
cy.wait('@moveFile')
}
export const navigateToFolder = (folderName: string) => {
getRowForFile(folderName).should('be.visible').find('[data-cy-files-list-row-name-link]').click()
}
export const closeSidebar = () => {
cy.get('[cy-data-sidebar] .app-sidebar__close').click()
}

@ -0,0 +1,215 @@
/**
* @copyright Copyright (c) 2024 Louis Chmn <louis@chmn.me>
*
* @author Louis Chmn <louis@chmn.me>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type { User } from '@nextcloud/cypress'
import { closeSidebar, copyFile, getRowForFile, getRowForFileId, renameFile, triggerActionForFile, triggerInlineActionForFileId } from './FilesUtils'
/**
*
* @param label
*/
function refreshView(label: string) {
cy.intercept('PROPFIND', /\/remote.php\/dav\//).as('propfind')
cy.get('[data-cy-files-content-breadcrumbs]').contains(label).click()
cy.wait('@propfind')
}
/**
*
* @param user
* @param fileName
* @param domain
* @param requesttoken
* @param metadata
*/
function setMetadata(user: User, fileName: string, domain: string, requesttoken: string, metadata: object) {
cy.request({
method: 'PROPPATCH',
url: `http://${domain}/remote.php/dav/files/${user.userId}/${fileName}`,
auth: { user: user.userId, pass: user.password },
headers: {
requesttoken,
},
body: `<?xml version="1.0"?>
<d:propertyupdate xmlns:d="DAV:" xmlns:nc="http://nextcloud.org/ns">
<d:set>
<d:prop>
${Object.entries(metadata).map(([key, value]) => `<${key}>${value}</${key}>`).join('\n')}
</d:prop>
</d:set>
</d:propertyupdate>`,
})
}
describe('Files: Live photos', { testIsolation: true }, () => {
let currentUser: User
let randomFileName: string
let jpgFileId: number
let movFileId: number
let hostname: string
let requesttoken: string
before(() => {
cy.createRandomUser().then((user) => {
currentUser = user
cy.login(currentUser)
cy.visit('/apps/files')
})
cy.url().then(url => { hostname = new URL(url).hostname })
})
beforeEach(() => {
randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
cy.uploadContent(currentUser, new Blob(['jpg file'], { type: 'image/jpg' }), 'image/jpg', `/${randomFileName}.jpg`)
.then(response => { jpgFileId = parseInt(response.headers['oc-fileid']) })
cy.uploadContent(currentUser, new Blob(['mov file'], { type: 'video/mov' }), 'video/mov', `/${randomFileName}.mov`)
.then(response => { movFileId = parseInt(response.headers['oc-fileid']) })
cy.login(currentUser)
cy.visit('/apps/files')
cy.get('head').invoke('attr', 'data-requesttoken').then(_requesttoken => { requesttoken = _requesttoken as string })
cy.then(() => {
setMetadata(currentUser, `${randomFileName}.jpg`, hostname, requesttoken, { 'nc:metadata-files-live-photo': movFileId })
setMetadata(currentUser, `${randomFileName}.mov`, hostname, requesttoken, { 'nc:metadata-files-live-photo': jpgFileId })
})
cy.then(() => {
cy.visit(`/apps/files/files/${jpgFileId}`) // Refresh and scroll to the .jpg file.
closeSidebar()
})
})
it('Only renders the .jpg file', () => {
getRowForFileId(jpgFileId).should('have.length', 1)
getRowForFileId(movFileId).should('have.length', 0)
})
context("'Show hidden files' is enabled", () => {
before(() => {
cy.login(currentUser)
cy.visit('/apps/files')
cy.get('[data-cy-files-navigation-settings-button]').click()
// Force:true because the checkbox is hidden by the pretty UI.
cy.get('[data-cy-files-settings-setting="show_hidden"] input').check({ force: true })
})
it("Shows both files when 'Show hidden files' is enabled", () => {
getRowForFileId(jpgFileId).should('have.length', 1).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}.jpg`)
getRowForFileId(movFileId).should('have.length', 1).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}.mov`)
})
it('Copies both files when copying the .jpg', () => {
copyFile(`${randomFileName}.jpg`, '.')
refreshView('All files')
getRowForFile(`${randomFileName}.jpg`).should('have.length', 1)
getRowForFile(`${randomFileName}.mov`).should('have.length', 1)
getRowForFile(`${randomFileName} (copy).jpg`).should('have.length', 1)
getRowForFile(`${randomFileName} (copy).mov`).should('have.length', 1)
})
it('Copies both files when copying the .mov', () => {
copyFile(`${randomFileName}.mov`, '.')
refreshView('All files')
getRowForFile(`${randomFileName}.mov`).should('have.length', 1)
getRowForFile(`${randomFileName} (copy).jpg`).should('have.length', 1)
getRowForFile(`${randomFileName} (copy).mov`).should('have.length', 1)
})
it('Moves files when moving the .jpg', () => {
renameFile(`${randomFileName}.jpg`, `${randomFileName}_moved.jpg`)
refreshView('All files')
getRowForFileId(jpgFileId).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}_moved.jpg`)
getRowForFileId(movFileId).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}_moved.mov`)
})
it('Moves files when moving the .mov', () => {
renameFile(`${randomFileName}.mov`, `${randomFileName}_moved.mov`)
refreshView('All files')
getRowForFileId(jpgFileId).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}_moved.jpg`)
getRowForFileId(movFileId).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}_moved.mov`)
})
it('Deletes files when deleting the .jpg', () => {
triggerActionForFile(`${randomFileName}.jpg`, 'delete')
refreshView('All files')
getRowForFile(`${randomFileName}.jpg`).should('have.length', 0)
getRowForFile(`${randomFileName}.mov`).should('have.length', 0)
cy.visit('/apps/files/trashbin')
getRowForFileId(jpgFileId).invoke('attr', 'data-cy-files-list-row-name').should('to.match', new RegExp(`^${randomFileName}.jpg\\.d[0-9]+$`))
getRowForFileId(movFileId).invoke('attr', 'data-cy-files-list-row-name').should('to.match', new RegExp(`^${randomFileName}.mov\\.d[0-9]+$`))
})
it('Block deletion when deleting the .mov', () => {
triggerActionForFile(`${randomFileName}.mov`, 'delete')
refreshView('All files')
getRowForFile(`${randomFileName}.jpg`).should('have.length', 1)
getRowForFile(`${randomFileName}.mov`).should('have.length', 1)
cy.visit('/apps/files/trashbin')
getRowForFileId(jpgFileId).should('have.length', 0)
getRowForFileId(movFileId).should('have.length', 0)
})
it('Restores files when restoring the .jpg', () => {
triggerActionForFile(`${randomFileName}.jpg`, 'delete')
cy.visit('/apps/files/trashbin')
triggerInlineActionForFileId(jpgFileId, 'restore')
refreshView('Deleted files')
getRowForFile(`${randomFileName}.jpg`).should('have.length', 0)
getRowForFile(`${randomFileName}.mov`).should('have.length', 0)
cy.visit('/apps/files')
getRowForFile(`${randomFileName}.jpg`).should('have.length', 1)
getRowForFile(`${randomFileName}.mov`).should('have.length', 1)
})
it('Blocks restoration when restoring the .mov', () => {
triggerActionForFile(`${randomFileName}.jpg`, 'delete')
cy.visit('/apps/files/trashbin')
triggerInlineActionForFileId(movFileId, 'restore')
refreshView('Deleted files')
getRowForFileId(jpgFileId).should('have.length', 1)
getRowForFileId(movFileId).should('have.length', 1)
cy.visit('/apps/files')
getRowForFile(`${randomFileName}.jpg`).should('have.length', 0)
getRowForFile(`${randomFileName}.mov`).should('have.length', 0)
})
})
})

@ -58,8 +58,10 @@ export function updateShare(fileName: string, index: number, shareSettings: Part
if (shareSettings.download !== undefined) {
cy.get('[data-cy-files-sharing-share-permissions-checkbox="download"]').find('input').as('downloadCheckbox')
if (shareSettings.download) {
// Force:true because the checkbox is hidden by the pretty UI.
cy.get('@downloadCheckbox').check({ force: true, scrollBehavior: 'nearest' })
} else {
// Force:true because the checkbox is hidden by the pretty UI.
cy.get('@downloadCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
}
}
@ -67,8 +69,10 @@ export function updateShare(fileName: string, index: number, shareSettings: Part
if (shareSettings.read !== undefined) {
cy.get('[data-cy-files-sharing-share-permissions-checkbox="read"]').find('input').as('readCheckbox')
if (shareSettings.read) {
// Force:true because the checkbox is hidden by the pretty UI.
cy.get('@readCheckbox').check({ force: true, scrollBehavior: 'nearest' })
} else {
// Force:true because the checkbox is hidden by the pretty UI.
cy.get('@readCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
}
}
@ -76,8 +80,10 @@ export function updateShare(fileName: string, index: number, shareSettings: Part
if (shareSettings.update !== undefined) {
cy.get('[data-cy-files-sharing-share-permissions-checkbox="update"]').find('input').as('updateCheckbox')
if (shareSettings.update) {
// Force:true because the checkbox is hidden by the pretty UI.
cy.get('@updateCheckbox').check({ force: true, scrollBehavior: 'nearest' })
} else {
// Force:true because the checkbox is hidden by the pretty UI.
cy.get('@updateCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
}
}
@ -85,8 +91,10 @@ export function updateShare(fileName: string, index: number, shareSettings: Part
if (shareSettings.delete !== undefined) {
cy.get('[data-cy-files-sharing-share-permissions-checkbox="delete"]').find('input').as('deleteCheckbox')
if (shareSettings.delete) {
// Force:true because the checkbox is hidden by the pretty UI.
cy.get('@deleteCheckbox').check({ force: true, scrollBehavior: 'nearest' })
} else {
// Force:true because the checkbox is hidden by the pretty UI.
cy.get('@deleteCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
}
}

@ -113,7 +113,7 @@ describe('Versions restoration', () => {
auth: { user: recipient.userId, pass: recipient.password },
headers: {
cookie: '',
Destination: 'https://nextcloud_server1.test/remote.php/dav/versions/admin/restore/target',
Destination: `http://${hostname}/remote.php/dav/versions/${recipient.userId}/restore/target`,
},
url: `http://${hostname}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`,
failOnStatusCode: false,

@ -49,7 +49,7 @@ declare global {
* Upload a raw content to a given user storage.
* **Warning**: Using this function will reset the previous session
*/
uploadContent(user: User, content: Blob, mimeType: string, target: string, mtime?: number): Cypress.Chainable<void>,
uploadContent(user: User, content: Blob, mimeType: string, target: string, mtime?: number): Cypress.Chainable<AxiosResponse>,
/**
* Create a new directory
@ -156,7 +156,7 @@ Cypress.Commands.add('setFileAsFavorite', (user: User, target: string, favorite
<oc:favorite>${favorite ? 1 : 0}</oc:favorite>
</d:prop>
</d:set>
</d:propertyupdate>`
</d:propertyupdate>`,
})
cy.log(`Created directory ${target}`, response)
} catch (error) {
@ -198,36 +198,36 @@ Cypress.Commands.add('mkdir', (user: User, target: string) => {
* @param {string} mimeType e.g. image/png
* @param {string} target the target of the file relative to the user root
*/
Cypress.Commands.add('uploadContent', (user, blob, mimeType, target, mtime = undefined) => {
// eslint-disable-next-line cypress/unsafe-to-chain-command
Cypress.Commands.add('uploadContent', (user: User, blob: Blob, mimeType: string, target: string, mtime?: number) => {
cy.clearCookies()
.then(async () => {
const fileName = basename(target)
return cy.then(async () => {
const fileName = basename(target)
// Process paths
const rootPath = `${Cypress.env('baseUrl')}/remote.php/dav/files/${encodeURIComponent(user.userId)}`
const filePath = target.split('/').map(encodeURIComponent).join('/')
try {
const file = new File([blob], fileName, { type: mimeType })
const response = await axios({
url: `${rootPath}${filePath}`,
method: 'PUT',
data: file,
headers: {
'Content-Type': mimeType,
'X-OC-MTime': mtime ? `${mtime}` : undefined,
},
auth: {
username: user.userId,
password: user.password,
},
})
cy.log(`Uploaded content as ${fileName}`, response)
} catch (error) {
cy.log('error', error)
throw new Error('Unable to process fixture')
}
})
// Process paths
const rootPath = `${Cypress.env('baseUrl')}/remote.php/dav/files/${encodeURIComponent(user.userId)}`
const filePath = target.split('/').map(encodeURIComponent).join('/')
try {
const file = new File([blob], fileName, { type: mimeType })
const response = await axios({
url: `${rootPath}${filePath}`,
method: 'PUT',
data: file,
headers: {
'Content-Type': mimeType,
'X-OC-MTime': mtime ? `${mtime}` : undefined,
},
auth: {
username: user.userId,
password: user.password,
},
})
cy.log(`Uploaded content as ${fileName}`, response)
return response
} catch (error) {
cy.log('error', error)
throw new Error('Unable to process fixture')
}
})
})
/**

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save