Merge pull request #55885 from nextcloud/chore/e2e-test-server

chore: migrate Cypress to `@nextcloud/e2e-test-server`
pull/55910/head
Ferdinand Thiessen 2025-10-22 14:34:15 +07:00 committed by GitHub
commit 7978ee82a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
82 changed files with 740 additions and 956 deletions

@ -205,20 +205,19 @@ jobs:
cypress/snapshots
cypress/videos
- name: Extract NC logs
- name: Show logs
if: failure() && matrix.containers != 'component'
run: docker logs nextcloud-cypress-tests_${{ env.APP_NAME }} > nextcloud.log
- name: Upload NC logs
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: failure() && matrix.containers != 'component'
with:
name: nc_logs_${{ matrix.containers }}
path: nextcloud.log
run: |
for id in $(docker ps -aq); do
docker container inspect "$id" --format '=== Logs for container {{.Name}} ==='
docker logs "$id" >> nextcloud.log
done
echo '=== Nextcloud server logs ==='
docker exec nextcloud-e2e-test-server_${{ env.APP_NAME }} cat data/nextcloud.log
- name: Create data dir archive
if: failure() && matrix.containers != 'component'
run: docker exec nextcloud-cypress-tests_${{ env.APP_NAME }} tar -cvjf - data > data.tar
run: docker exec nextcloud-e2e-test-server_${{ env.APP_NAME }} tar -cvjf - data > data.tar
- name: Upload data dir archive
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2

@ -57,7 +57,6 @@ $expectedFiles = [
'cron.php',
'custom.d.ts',
'cypress.config.ts',
'cypress.d.ts',
'cypress',
'dist',
'eslint.config.mjs',

@ -4,21 +4,19 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Configuration } from 'webpack'
import webpackPreprocessor from '@cypress/webpack-preprocessor'
import { configureNextcloud, docker, getContainer, getContainerName, runExec, runOcc, startNextcloud, stopNextcloud, waitOnNextcloud } from '@nextcloud/e2e-test-server'
import { defineConfig } from 'cypress'
import { removeDirectory } from 'cypress-delete-downloads-folder'
import cypressSplit from 'cypress-split'
import { join } from 'path'
import {
applyChangesToNextcloud,
configureNextcloud,
startNextcloud,
stopNextcloud,
waitOnNextcloud,
} from './cypress/dockerNode.ts'
import webpackConfig from './webpack.config.js'
import vitePreprocessor from 'cypress-vite'
import { existsSync, rmdirSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
if (!globalThis.__dirname) {
// Cypress has their own weird parser
globalThis.__dirname = dirname(fileURLToPath(new URL(import.meta.url)))
}
export default defineConfig({
projectId: '37xpdh',
@ -65,13 +63,13 @@ export default defineConfig({
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
async setupNodeEvents(on, config) {
on('file:preprocessor', webpackPreprocessor({ webpackOptions: webpackConfig as Configuration }))
on('task', { removeDirectory })
on('file:preprocessor', vitePreprocessor({
plugins: [nodePolyfills()],
}))
// This allows to store global data (e.g. the name of a snapshot)
// because Cypress.env() and other options are local to the current spec file.
const data = {}
const data: Record<string, unknown> = {}
on('task', {
setVariable({ key, value }) {
data[key] = value
@ -80,6 +78,17 @@ export default defineConfig({
getVariable({ key }) {
return data[key] ?? null
},
// allow to clear the downloads folder
deleteFolder(path: string) {
try {
if (existsSync(path)) {
rmdirSync(path, { maxRetries: 10, recursive: true })
}
return null
} catch (error) {
throw Error(`Error while deleting ${path}. Original error: ${error}`)
}
},
})
// Disable spell checking to prevent rendering differences
@ -117,21 +126,82 @@ export default defineConfig({
cypressSplit(on, config)
}
const mounts = {
'3rdparty': resolve(__dirname, './3rdparty'),
apps: resolve(__dirname, './apps'),
core: resolve(__dirname, './core'),
cypress: resolve(__dirname, './cypress'),
dist: resolve(__dirname, './dist'),
lib: resolve(__dirname, './lib'),
ocs: resolve(__dirname, './ocs'),
'ocs-provider': resolve(__dirname, './ocs-provider'),
resources: resolve(__dirname, './resources'),
tests: resolve(__dirname, './tests'),
'console.php': resolve(__dirname, './console.php'),
'cron.php': resolve(__dirname, './cron.php'),
'index.php': resolve(__dirname, './index.php'),
occ: resolve(__dirname, './occ'),
'public.php': resolve(__dirname, './public.php'),
'remote.php': resolve(__dirname, './remote.php'),
'status.php': resolve(__dirname, './status.php'),
'version.php': resolve(__dirname, './version.php'),
} as Record<string, string>
for (const [key, path] of Object.entries(mounts)) {
if (!existsSync(path)) {
delete mounts[key]
}
}
// Before the browser launches
// starting Nextcloud testing container
const ip = await startNextcloud(process.env.BRANCH)
const port = 8042
const ip = await startNextcloud(process.env.BRANCH, false, {
mounts,
exposePort: port,
})
// Setting container's IP as base Url
config.baseUrl = `http://${ip}/index.php`
config.baseUrl = `http://localhost:${port}/index.php`
// if needed for the setup tests, connect to the actions network
await connectToActionsNetwork()
// make sure not to write into apps but use a local apps folder
runExec(['mkdir', 'apps-cypress'])
runExec(['cp', 'cypress/fixtures/app.config.php', 'config'])
// now wait until Nextcloud is ready and configure it
await waitOnNextcloud(ip)
await configureNextcloud()
// additionally we do not want to DoS the app store
runOcc(['config:system:set', 'appstoreenabled', '--value', 'false', '--type', 'boolean'])
if (!process.env.CI) {
await applyChangesToNextcloud()
}
// for later use in tests save the container name
// @ts-expect-error we are adding a custom property
config.dockerContainerName = getContainerName()
// IMPORTANT: return the config otherwise cypress-split will not work
return config
},
},
})
/**
* Connect the running test container to the GitHub Actions network
*/
async function connectToActionsNetwork() {
if (process.env.SETUP_TESTING !== 'true') {
console.log('├─ Not running setup tests, skipping actions network connection 🌐')
return
}
console.log('├─ Looking for github actions network... 🔍')
const networks = await docker.listNetworks()
const network = networks.find((network) => network.Name.startsWith('github_network'))
if (!network) {
console.log('│ └─ No actions network found ⚠️')
return
}
console.log('│ |─ Found actions network: ' + network.Name)
await docker.getNetwork(network.Id)
.connect({ Container: getContainer().id })
console.log('│ └─ Connected to actions network 🌐')
}

32
cypress.d.ts vendored

@ -1,32 +0,0 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
/* eslint-disable */
import { mount } from '@cypress/vue2'
type MountParams = Parameters<typeof mount>;
type OptionsParam = MountParams[1];
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount;
/**
* Mock an initial state for component testing
*
* @param app App name of the initial state
* @param key Key of the initial state
* @param value The mocked value of the initial state
*/
mockInitialState: (app: string, key: string, value: any) => void
/**
* Unmock all initial states or one defined by app and key
*
* @param app app name of the initial state
* @param key the key of the initial state
*/
unmockInitialState: (app?: string, key?: string) => void
}
}
}

@ -1,375 +0,0 @@
/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { execSync } from 'child_process'
import Docker from 'dockerode'
import { existsSync } from 'fs'
import path, { basename } from 'path'
import { c as createTar } from 'tar'
import waitOn from 'wait-on'
export const docker = new Docker()
const CONTAINER_NAME = `nextcloud-cypress-tests_${basename(process.cwd()).replace(' ', '')}`
const SERVER_IMAGE = 'ghcr.io/nextcloud/continuous-integration-shallow-server'
/**
* Start the testing container
*
* @param branch the branch of your current work
*/
export async function startNextcloud(branch: string = getCurrentGitBranch()): Promise<any> {
try {
try {
// Pulling images
console.log('\nPulling images... ⏳')
await new Promise((resolve, reject): any => docker.pull(SERVER_IMAGE, (err, stream) => {
if (err) {
reject(err)
}
if (stream === null) {
reject(new Error('Could not connect to docker, ensure docker is running.'))
return
}
// https://github.com/apocas/dockerode/issues/357
docker.modem.followProgress(stream, onFinished)
/**
*
* @param err
*/
function onFinished(err) {
if (!err) {
resolve(true)
return
}
reject(err)
}
}))
const digest = await (await docker.getImage(SERVER_IMAGE).inspect()).RepoDigests.at(0)
const sha = digest?.split('@').at(1)
console.log('├─ Using image ' + sha)
console.log('└─ Done')
} catch (e) {
console.log('└─ Failed to pull images')
throw e
}
// Remove old container if exists
console.log('\nChecking running containers... 🔍')
try {
const oldContainer = docker.getContainer(CONTAINER_NAME)
const oldContainerData = await oldContainer.inspect()
if (oldContainerData) {
console.log('├─ Existing running container found')
console.log('├─ Removing... ⏳')
// Forcing any remnants to be removed just in case
await oldContainer.remove({ force: true })
console.log('└─ Done')
}
} catch {
console.log('└─ None found!')
}
// Starting container
console.log('\nStarting Nextcloud container... 🚀')
console.log(`├─ Using branch '${branch}'`)
const container = await docker.createContainer({
Image: SERVER_IMAGE,
name: CONTAINER_NAME,
HostConfig: {
Mounts: [{
Target: '/var/www/html/data',
Source: '',
Type: 'tmpfs',
ReadOnly: false,
}],
PortBindings: {
'80/tcp': [{
HostIP: '0.0.0.0',
HostPort: '8083',
}],
},
// If running the setup tests, let's bind to host
// to communicate with the github actions DB services
NetworkMode: process.env.SETUP_TESTING === 'true' ? await getGithubNetwork() : undefined,
},
Env: [
`BRANCH=${branch}`,
'APCU=1',
],
})
await container.start()
// Set proper permissions for the data folder
await runExec(container, ['chown', '-R', 'www-data:www-data', '/var/www/html/data'], false, 'root')
await runExec(container, ['chmod', '0770', '/var/www/html/data'], false, 'root')
// Get container's IP
const ip = await getContainerIP(container)
console.log(`├─ Nextcloud container's IP is ${ip} 🌏`)
return ip
} catch (err) {
console.log('└─ Unable to start the container 🛑')
console.log('\n', err, '\n')
stopNextcloud()
throw new Error('Unable to start the container')
}
}
/**
* Configure Nextcloud
*/
export async function configureNextcloud() {
console.log('\nConfiguring nextcloud...')
const container = docker.getContainer(CONTAINER_NAME)
await runExec(container, ['php', 'occ', '--version'], true)
// Be consistent for screenshots
await runExec(container, ['php', 'occ', 'config:system:set', 'default_language', '--value', 'en'], true)
await runExec(container, ['php', 'occ', 'config:system:set', 'force_language', '--value', 'en'], true)
await runExec(container, ['php', 'occ', 'config:system:set', 'default_locale', '--value', 'en_US'], true)
await runExec(container, ['php', 'occ', 'config:system:set', 'force_locale', '--value', 'en_US'], true)
await runExec(container, ['php', 'occ', 'config:system:set', 'enforce_theme', '--value', 'light'], true)
// Speed up test and make them less flaky. If a cron execution is needed, it can be triggered manually.
await runExec(container, ['php', 'occ', 'background:cron'], true)
// Checking apcu
const distributed = await runExec(container, ['php', 'occ', 'config:system:get', 'memcache.distributed'])
const local = await runExec(container, ['php', 'occ', 'config:system:get', 'memcache.local'])
const hashing = await runExec(container, ['php', 'occ', 'config:system:get', 'hashing_default_password'])
console.log('├─ Checking APCu configuration... 👀')
if (!distributed.trim().includes('Memcache\\APCu')
|| !local.trim().includes('Memcache\\APCu')
|| !hashing.trim().includes('true')) {
console.log('└─ APCu is not properly configured 🛑')
throw new Error('APCu is not properly configured')
}
console.log('│ └─ OK !')
// Saving DB state
console.log('├─ Creating init DB snapshot...')
await runExec(container, ['cp', '/var/www/html/data/owncloud.db', '/var/www/html/data/owncloud.db-init'], true)
console.log('├─ Creating init data backup...')
await runExec(container, ['tar', 'cf', 'data-init.tar', 'admin'], true, undefined, '/var/www/html/data')
console.log('└─ Nextcloud is now ready to use 🎉')
}
/**
* Applying local changes to the container
* Only triggered if we're not in CI. Otherwise the
* continuous-integration-shallow-server image will
* already fetch the proper branch.
*/
export async function applyChangesToNextcloud() {
console.log('\nApply local changes to nextcloud...')
const htmlPath = '/var/www/html'
const folderPaths = [
'./3rdparty',
'./apps',
'./core',
'./dist',
'./lib',
'./ocs',
'./ocs-provider',
'./resources',
'./tests',
'./console.php',
'./cron.php',
'./index.php',
'./occ',
'./public.php',
'./remote.php',
'./status.php',
'./version.php',
].filter((folderPath) => {
const fullPath = path.resolve(__dirname, '..', folderPath)
if (existsSync(fullPath)) {
console.log(`├─ Copying ${folderPath}`)
return true
}
return false
})
// Don't try to apply changes, when there are none. Otherwise we
// still execute the 'chown' command, which is not needed.
if (folderPaths.length === 0) {
console.log('└─ No local changes found to apply')
return
}
const container = docker.getContainer(CONTAINER_NAME)
// Tar-streaming the above folders into the container
const serverTar = createTar({ gzip: false }, folderPaths)
await container.putArchive(serverTar, {
path: htmlPath,
})
// Making sure we have the proper permissions
await runExec(container, ['chown', '-R', 'www-data:www-data', htmlPath], false, 'root')
console.log('└─ Changes applied successfully 🎉')
}
/**
* Force stop the testing container
*/
export async function stopNextcloud() {
try {
const container = docker.getContainer(CONTAINER_NAME)
console.log('Stopping Nextcloud container...')
container.remove({ force: true })
console.log('└─ Nextcloud container removed 🥀')
} catch (err) {
console.log(err)
}
}
/**
* Get the testing container's IP
*
* @param container the container to get the IP from
*/
export async function getContainerIP(container = docker.getContainer(CONTAINER_NAME)): Promise<string> {
let ip = ''
let tries = 0
while (ip === '' && tries < 10) {
tries++
container.inspect(function(err, data) {
if (err) {
throw err
}
if (data?.HostConfig.PortBindings?.['80/tcp']?.[0]?.HostPort) {
ip = `localhost:${data.HostConfig.PortBindings['80/tcp'][0].HostPort}`
} else {
ip = data?.NetworkSettings?.IPAddress || ''
}
})
if (ip !== '') {
break
}
await sleep(1000 * tries)
}
return ip
}
// Would be simpler to start the container from cypress.config.ts,
// but when checking out different branches, it can take a few seconds
// Until we can properly configure the baseUrl retry intervals,
// We need to make sure the server is already running before cypress
// https://github.com/cypress-io/cypress/issues/22676
/**
*
* @param ip
*/
export async function waitOnNextcloud(ip: string) {
console.log('├─ Waiting for Nextcloud to be ready... ⏳')
await waitOn({
resources: [`http://${ip}/index.php`],
// wait for nextcloud to be up and return any non error status
validateStatus: (status) => status >= 200 && status < 400,
// timout in ms
timeout: 5 * 60 * 1000,
// timeout for a single HTTP request
httpTimeout: 60 * 1000,
})
console.log('└─ Done')
}
/**
*
* @param container
* @param command
* @param verbose
* @param user
* @param workdir
*/
async function runExec(
container: Docker.Container,
command: string[],
verbose = false,
user = 'www-data',
workdir?: string,
): Promise<string> {
const exec = await container.exec({
Cmd: command,
WorkingDir: workdir,
AttachStdout: true,
AttachStderr: true,
User: user,
})
return new Promise((resolve, reject) => {
let output = ''
exec.start({}, (err, stream) => {
if (err) {
reject(err)
}
if (stream) {
stream.setEncoding('utf-8')
stream.on('data', (str) => {
str = str.trim()
// Remove non printable characters
// eslint-disable-next-line no-control-regex
.replace(/[^\x0A\x0D\x20-\x7E]+/g, '')
// Remove non alphanumeric leading characters
.replace(/^[^a-z]/gi, '')
output += str
if (verbose && str !== '') {
console.log(`├─ ${str.replace(/\n/gi, '\n├─ ')}`)
}
})
stream.on('end', () => resolve(output))
}
})
})
}
/**
*
* @param milliseconds
*/
function sleep(milliseconds: number) {
return new Promise((resolve) => setTimeout(resolve, milliseconds))
}
/**
*
*/
function getCurrentGitBranch() {
return execSync('git rev-parse --abbrev-ref HEAD').toString().trim() || 'master'
}
/**
* Get the network name of the github actions network
* This is used to connect to the database services
* started by github actions
*/
async function getGithubNetwork(): Promise<string | undefined> {
console.log('├─ Looking for github actions network... 🔍')
const networks = await docker.listNetworks()
const network = networks.find((network) => network.Name.startsWith('github_network'))
if (network) {
console.log('│ └─ Found github actions network: ' + network.Name)
return network.Name
}
console.log('│ └─ No github actions network found')
return undefined
}

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { User } from '@nextcloud/cypress'
import { User } from '@nextcloud/e2e-test-server/cypress'
import { clearState, getNextcloudUserMenu, getNextcloudUserMenuToggle } from '../../support/commonUtils.ts'
const admin = new User('admin', 'admin')

@ -3,9 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { User } from '@nextcloud/cypress'
import randomString from 'crypto-random-string'
import { User } from '@nextcloud/e2e-test-server/cypress'
import { clearState, getNextcloudHeader } from '../../support/commonUtils.ts'
import { randomString } from '../../support/utils/randomString.ts'
const admin = new User('admin', 'admin')

@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { randomString } from '../../support/utils/randomString.ts'
/**
* DO NOT RENAME THIS FILE to .cy.ts
* This is not following the pattern of the other files in this folder
@ -110,7 +112,7 @@ describe('Can install Nextcloud', { testIsolation: true, retries: 0 }, () => {
* Shared admin setup function for the Nextcloud setup
*/
function sharedSetup() {
const randAdmin = 'admin-' + Math.random().toString(36).substring(2, 15)
const randAdmin = 'admin-' + randomString(10)
// mock appstore
cy.intercept('**/settings/apps/list', { fixture: 'appstore/apps.json' })

@ -3,9 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { ACTION_COPY_MOVE } from '../../../apps/files/src/actions/moveOrCopyAction.ts'
const ACTION_COPY_MOVE = 'move-copy'
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)}"]`)

@ -3,7 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { randomString } from '../../support/utils/randomString.ts'
type SetupInfo = {
snapshot: string
@ -72,7 +74,7 @@ export function setupLivePhotos(): Cypress.Chainable<SetupInfo> {
} else {
let requesttoken: string
setupInfo.fileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
setupInfo.fileName = randomString(10)
cy.createRandomUser().then((_user) => {
setupInfo.user = _user

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { getActionButtonForFile, getRowForFile, triggerActionForFile } from './FilesUtils.ts'

@ -3,14 +3,15 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { FileAction } from '@nextcloud/files'
import { ACTION_DELETE } from '../../../apps/files/src/actions/deleteAction.ts'
import { ACTION_COPY_MOVE } from '../../../apps/files/src/actions/moveOrCopyAction.ts'
import { ACTION_DETAILS } from '../../../apps/files/src/actions/sidebarAction.ts'
import { getActionButtonForFileId, getActionEntryForFileId, getRowForFile, getSelectionActionButton, getSelectionActionEntry, selectRowForFile } from './FilesUtils.ts'
const ACTION_DELETE = 'delete'
const ACTION_COPY_MOVE = 'move-copy'
const ACTION_DETAILS = 'details'
declare global {
interface Window {
_nc_fileactions: FileAction[]

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { getRowForFile, navigateToFolder, selectAllFiles, triggerActionForFile } from './FilesUtils.ts'

@ -3,11 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import randomString from 'crypto-random-string'
import { deleteDownloadsFolderBeforeEach } from 'cypress-delete-downloads-folder'
import { zipFileContains } from '../../support/utils/assertions.ts'
import { deleteDownloadsFolderBeforeEach } from '../../support/utils/deleteDownloadsFolder.ts'
import { randomString } from '../../support/utils/randomString.ts'
import { getRowForFile, navigateToFolder, triggerActionForFile } from './FilesUtils.ts'
describe('files: Download files using file actions', { testIsolation: true }, () => {

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { FilesFilterPage } from '../../pages/FilesFilters.ts'
import { FilesNavigationPage } from '../../pages/FilesNavigation.ts'

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { getRowForFile, navigateToFolder } from './FilesUtils.ts'

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { calculateViewportHeight, createFolder, getRowForFile, haveValidity, renameFile, triggerActionForFile } from './FilesUtils.ts'

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { deselectAllFiles, selectAllFiles, selectRowForFile } from './FilesUtils.ts'

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { getRowForFile } from './FilesUtils.ts'

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { assertNotExistOrNotVisible } from '../settings/usersUtils.ts'
import { getRowForFile, navigateToFolder, triggerActionForFile } from './FilesUtils.ts'

@ -1,4 +1,4 @@
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import {
clickOnBreadcrumbs,

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { getRowForFile, triggerActionForFile } from './FilesUtils.ts'

@ -3,9 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { join } from 'path'
import { joinPaths as join } from '@nextcloud/paths'
import { getRowForFileId } from './FilesUtils.ts'
/**

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { FilesNavigationPage } from '../../pages/FilesNavigation.ts'
import { getRowForFile, navigateToFolder } from './FilesUtils.ts'

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
export type StorageConfig = {
[key: string]: string

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { getRowForFile } from '../files/FilesUtils.ts'
import { AuthBackend, createStorageWithConfig, StorageBackend } from './StorageUtils.ts'

@ -3,13 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { ACTION_CREDENTIALS_EXTERNAL_STORAGE } from '../../../apps/files_external/src/actions/enterCredentialsAction.ts'
import { getInlineActionEntryForFile, getRowForFile, navigateToFolder, triggerInlineActionForFile } from '../files/FilesUtils.ts'
import { handlePasswordConfirmation } from '../settings/usersUtils.ts'
import { AuthBackend, createStorageWithConfig, StorageBackend } from './StorageUtils.ts'
const ACTION_CREDENTIALS_EXTERNAL_STORAGE = 'credentials-external-storage'
describe('Files user credentials', { testIsolation: true }, () => {
let user1: User
let user2: User

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { AuthBackend, createStorageWithConfig, deleteAllExternalStorages, setStorageMountOptions, StorageBackend } from './StorageUtils.ts'

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { closeSidebar } from '../files/FilesUtils.ts'
import { createShare, openSharingDetails, openSharingPanel, updateShare } from './FilesSharingUtils.ts'

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { createFolder, getRowForFile, navigateToFolder } from '../files/FilesUtils.ts'
import { createFileRequest } from './FilesSharingUtils.ts'

@ -2,9 +2,9 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import { ACTION_COPY_MOVE } from '../../../apps/files/src/actions/moveOrCopyAction.ts'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import {
copyFile,
getRowForFile,
@ -13,6 +13,8 @@ import {
} from '../files/FilesUtils.ts'
import { createShare } from './FilesSharingUtils.ts'
const ACTION_COPY_MOVE = 'move-copy'
export function copyFileForbidden(fileName: string, dirPath: string) {
getRowForFile(fileName).should('be.visible')
triggerActionForFile(fileName, ACTION_COPY_MOVE)

@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import {
getActionButtonForFile,

@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { getRowForFile } from '../files/FilesUtils.ts'
import { createShare } from './FilesSharingUtils.ts'

@ -3,8 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { randomString } from '../../support/utils/randomString.ts'
import { createShare } from './FilesSharingUtils.ts'
describe('Limit to sharing to people in the same group', () => {
@ -17,11 +18,11 @@ describe('Limit to sharing to people in the same group', () => {
let randomGroupName3 = ''
before(() => {
randomFileName1 = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt'
randomFileName2 = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt'
randomGroupName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
randomGroupName2 = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
randomGroupName3 = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
randomFileName1 = randomString(10) + '.txt'
randomFileName2 = randomString(10) + '.txt'
randomGroupName = randomString(10)
randomGroupName2 = randomString(10)
randomGroupName3 = randomString(10)
cy.runOccCommand('config:app:set core shareapi_only_share_with_group_members --value yes')

@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { navigateToFolder } from '../files/FilesUtils.ts'
import { createShare, openSharingPanel } from './FilesSharingUtils.ts'

@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import type { ShareOptions } from '../ShareOptionsType.ts'
import { openSharingPanel } from '../FilesSharingUtils.ts'

@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { getRowForFile } from '../../files/FilesUtils.ts'
import { createLinkShare, setupData } from './PublicShareUtils.ts'

@ -1,18 +1,15 @@
import type { User } from '@nextcloud/cypress'
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
// @ts-expect-error The package is currently broken - but works...
import { deleteDownloadsFolderBeforeEach } from 'cypress-delete-downloads-folder'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import type { ShareContext } from './PublicShareUtils.ts'
import { zipFileContains } from '../../../support/utils/assertions.ts'
import { deleteDownloadsFolderBeforeEach } from '../../../support/utils/deleteDownloadsFolder.ts'
import { getRowForFile, getRowForFileId, triggerActionForFile, triggerActionForFileId } from '../../files/FilesUtils.ts'
import {
type ShareContext,
createLinkShare, getShareUrl, openLinkShareDetails, setupPublicShare,
} from './PublicShareUtils.ts'
import { createLinkShare, getShareUrl, openLinkShareDetails, setupPublicShare } from './PublicShareUtils.ts'
describe('files_sharing: Public share - downloading files', { testIsolation: true }, () => {
// in general there is no difference except downloading

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { createLinkShare, openLinkShareDetails } from './PublicShareUtils.ts'

@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { closeSidebar, enableGridMode, getActionButtonForFile, getInlineActionEntryForFile, getRowForFile } from '../files/FilesUtils.ts'
import { createShare } from './FilesSharingUtils.ts'

@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { deleteFileWithRequest, triggerFileListAction } from '../files/FilesUtils.ts'

@ -3,15 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
// @ts-expect-error package has wrong typings
import { deleteDownloadsFolderBeforeEach } from 'cypress-delete-downloads-folder'
import { deleteDownloadsFolderBeforeEach } from '../../support/utils/deleteDownloadsFolder.ts'
import { deleteFileWithRequest, getRowForFileId, selectAllFiles, triggerActionForFileId } from '../files/FilesUtils.ts'
describe('files_trashbin: download files', { testIsolation: true }, () => {
let user: User
const fileids: number[] = []
const fileids: [number, number] = [0, 0]
deleteDownloadsFolderBeforeEach()
@ -20,10 +19,10 @@ describe('files_trashbin: download files', { testIsolation: true }, () => {
user = $user
cy.uploadContent(user, new Blob(['<content>']), 'text/plain', '/file.txt')
.then(({ headers }) => fileids.push(Number.parseInt(headers['oc-fileid'])))
.then(({ headers }) => fileids[0] = Number.parseInt(headers['oc-fileid']))
.then(() => deleteFileWithRequest(user, '/file.txt'))
cy.uploadContent(user, new Blob(['<content>']), 'text/plain', '/other-file.txt')
.then(({ headers }) => fileids.push(Number.parseInt(headers['oc-fileid'])))
.then(({ headers }) => fileids[1] = Number.parseInt(headers['oc-fileid']))
.then(() => deleteFileWithRequest(user, '/other-file.txt'))
})
})

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import type { ShareSetting } from '../files_sharing/FilesSharingUtils.ts'
import { createShare } from '../files_sharing/FilesSharingUtils.ts'

@ -3,13 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { randomString } from '../../support/utils/randomString.ts'
import { openVersionsPanel, uploadThreeVersions } from './filesVersionsUtils.ts'
describe('Versions creation', () => {
let randomFileName = ''
before(() => {
randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt'
randomFileName = randomString(10) + '.txt'
cy.createRandomUser()
.then((user) => {

@ -3,32 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { randomString } from '../../support/utils/randomString.ts'
import { clickOnBreadcrumbs, closeSidebar, copyFile, moveFile, navigateToFolder } from '../files/FilesUtils.ts'
import { assertVersionContent, nameVersion, openVersionsPanel, setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils.ts'
/**
*
* @param filePath
*/
function assertVersionsContent(filePath: string) {
const path = filePath.split('/').slice(0, -1).join('/')
clickOnBreadcrumbs('All files')
if (path !== '') {
navigateToFolder(path)
}
openVersionsPanel(filePath)
cy.get('[data-files-versions-version]').should('have.length', 3)
assertVersionContent(0, 'v3')
assertVersionContent(1, 'v2')
assertVersionContent(2, 'v1')
}
describe('Versions cross share move and copy', () => {
let randomSharedFolderName = ''
let randomFileName = ''
@ -37,7 +17,7 @@ describe('Versions cross share move and copy', () => {
let bob: User
before(() => {
randomSharedFolderName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
randomSharedFolderName = randomString(10)
cy.createRandomUser()
.then((user) => {
@ -49,7 +29,7 @@ describe('Versions cross share move and copy', () => {
})
beforeEach(() => {
randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt'
randomFileName = randomString(10) + '.txt'
randomFilePath = `${randomSharedFolderName}/${randomFileName}`
uploadThreeVersions(alice, randomFilePath)
@ -78,8 +58,8 @@ describe('Versions cross share move and copy', () => {
let randomSubSubFolderName
beforeEach(() => {
randomSubFolderName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
randomSubSubFolderName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
randomSubFolderName = randomString(10)
randomSubSubFolderName = randomString(10)
clickOnBreadcrumbs('All files')
cy.mkdir(bob, `/${randomSharedFolderName}/${randomSubFolderName}`)
cy.mkdir(bob, `/${randomSharedFolderName}/${randomSubFolderName}/${randomSubSubFolderName}`)
@ -101,3 +81,23 @@ describe('Versions cross share move and copy', () => {
})
})
})
/**
* @param filePath
*/
function assertVersionsContent(filePath: string) {
const path = filePath.split('/').slice(0, -1).join('/')
clickOnBreadcrumbs('All files')
if (path !== '') {
navigateToFolder(path)
}
openVersionsPanel(filePath)
cy.get('[data-files-versions-version]').should('have.length', 3)
assertVersionContent(0, 'v3')
assertVersionContent(1, 'v2')
assertVersionContent(2, 'v1')
}

@ -3,14 +3,15 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { randomString } from '../../support/utils/randomString.ts'
import { getRowForFile, navigateToFolder } from '../files/FilesUtils.ts'
import { deleteVersion, doesNotHaveAction, openVersionsPanel, setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils.ts'
describe('Versions restoration', () => {
const folderName = 'shared_folder'
const randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt'
const randomFileName = randomString(10) + '.txt'
const randomFilePath = `/${folderName}/${randomFileName}`
let user: User
let versionCount = 0

@ -3,8 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { randomString } from '../../support/utils/randomString.ts'
import { getRowForFile } from '../files/FilesUtils.ts'
import { assertVersionContent, doesNotHaveAction, openVersionsPanel, setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils.ts'
@ -13,7 +14,7 @@ describe('Versions download', () => {
let user: User
before(() => {
randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt'
randomFileName = randomString(10) + '.txt'
cy.runOccCommand('config:app:set --value no core shareapi_allow_view_without_download')
cy.createRandomUser()

@ -3,13 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { randomString } from '../../support/utils/randomString.ts'
import { assertVersionContent, nameVersion, openVersionsPanel, uploadThreeVersions } from './filesVersionsUtils.ts'
describe('Versions expiration', () => {
let randomFileName = ''
beforeEach(() => {
randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt'
randomFileName = randomString(10) + '.txt'
cy.createRandomUser()
.then((user) => {

@ -3,8 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { randomString } from '../../support/utils/randomString.ts'
import { getRowForFile } from '../files/FilesUtils.ts'
import { doesNotHaveAction, nameVersion, openVersionsPanel, setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils.ts'
@ -13,7 +14,7 @@ describe('Versions naming', () => {
let user: User
before(() => {
randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt'
randomFileName = randomString(10) + '.txt'
cy.createRandomUser()
.then((_user) => {

@ -3,8 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { randomString } from '../../support/utils/randomString.ts'
import { getRowForFile } from '../files/FilesUtils.ts'
import { assertVersionContent, doesNotHaveAction, openVersionsPanel, restoreVersion, setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils.ts'
@ -13,7 +14,7 @@ describe('Versions restoration', () => {
let user: User
before(() => {
randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt'
randomFileName = randomString(10) + '.txt'
cy.createRandomUser()
.then((_user) => {

@ -2,14 +2,15 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { randomString } from '../../support/utils/randomString.ts'
import { navigateToFolder, triggerActionForFile } from '../files/FilesUtils.ts'
import { setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils.ts'
describe('Versions on shares', () => {
const randomSharedFolderName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
const randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt'
const randomSharedFolderName = randomString(10)
const randomFileName = randomString(10) + '.txt'
const randomFilePath = `${randomSharedFolderName}/${randomFileName}`
let alice: User
let bob: User

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { getNextcloudUserMenu, getNextcloudUserMenuToggle } from '../../support/commonUtils.ts'

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
interface IChromeVirtualAuthenticator {
authenticatorId: string

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { User } from '@nextcloud/cypress'
import { User } from '@nextcloud/e2e-test-server/cypress'
import { clearState, getNextcloudUserMenu, getNextcloudUserMenuToggle } from '../../support/commonUtils.ts'
const admin = new User('admin', 'admin')

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { User } from '@nextcloud/cypress'
import { User } from '@nextcloud/e2e-test-server/cypress'
import { handlePasswordConfirmation } from './usersUtils.ts'
const admin = new User('admin', 'admin')

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { handlePasswordConfirmation } from './usersUtils.ts'

@ -2,10 +2,9 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
/// <reference types="cypress-if" />
import { User } from '@nextcloud/cypress'
import randomString from 'crypto-random-string'
import { User } from '@nextcloud/e2e-test-server/cypress'
import { randomString } from '../../support/utils/randomString.ts'
import { getUserListRow, handlePasswordConfirmation } from './usersUtils.ts'
const admin = new User('admin', 'admin')

@ -4,7 +4,7 @@
*/
/// <reference types="cypress-if" />
import { User } from '@nextcloud/cypress'
import { User } from '@nextcloud/e2e-test-server/cypress'
import { getUserListRow, handlePasswordConfirmation } from './usersUtils.ts'
const admin = new User('admin', 'admin')

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
/**
* Assert that `element` does not exist or is not visible

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { User } from '@nextcloud/cypress'
import { User } from '@nextcloud/e2e-test-server/cypress'
import { assertNotExistOrNotVisible, getUserList } from './usersUtils.js'
const admin = new User('admin', 'admin')

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { User } from '@nextcloud/cypress'
import { User } from '@nextcloud/e2e-test-server/cypress'
import { clearState } from '../../support/commonUtils.ts'
import { getUserListRow } from './usersUtils.ts'

@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { User } from '@nextcloud/cypress'
import randomString from 'crypto-random-string'
import { User } from '@nextcloud/e2e-test-server/cypress'
import { randomString } from '../../support/utils/randomString.ts'
import { assertNotExistOrNotVisible, getUserListRow, handlePasswordConfirmation, toggleEditButton } from './usersUtils.ts'
const admin = new User('admin', 'admin')
@ -16,6 +16,8 @@ describe('Settings: Create groups', () => {
})
it('Can create a group', () => {
cy.intercept('POST', '**/ocs/v2.php/cloud/groups').as('createGroups')
const groupName = randomString(7)
// open the Create group menu
cy.get('button[aria-label="Create group"]').click()
@ -33,6 +35,7 @@ describe('Settings: Create groups', () => {
// Make sure no confirmation modal is shown
handlePasswordConfirmation(admin.password)
cy.wait('@createGroups').its('response.statusCode').should('eq', 200)
// see that the created group is in the list
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
@ -108,7 +111,7 @@ describe('Settings: Assign user to a group', { testIsolation: false }, () => {
it('validate the user was added on backend', () => {
cy.runOccCommand(`user:info --output=json '${testUser.userId}'`).then((output) => {
cy.wrap(output.code).should('eq', 0)
cy.wrap(output.exitCode).should('eq', 0)
cy.wrap(JSON.parse(output.stdout)?.groups).should('include', groupName)
})
})

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { User } from '@nextcloud/cypress'
import { User } from '@nextcloud/e2e-test-server/cypress'
import { clearState } from '../../support/commonUtils.ts'
import { getUserListRow, handlePasswordConfirmation, toggleEditButton, waitLoading } from './usersUtils.ts'

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { User } from '@nextcloud/cypress'
import { User } from '@nextcloud/e2e-test-server/cypress'
import { clearState } from '../../support/commonUtils.ts'
import { getUserListRow, handlePasswordConfirmation, toggleEditButton, waitLoading } from './usersUtils.ts'
@ -140,7 +140,7 @@ describe('Settings: Change user properties', function() {
// I see that the quota was set on the backend
cy.runOccCommand(`user:info --output=json '${user.userId}'`).then(($result) => {
expect($result.code).to.equal(0)
expect($result.exitCode).to.equal(0)
const info = JSON.parse($result.stdout)
expect(info?.quota).to.equal('5 GB')
})
@ -176,7 +176,7 @@ describe('Settings: Change user properties', function() {
// I see that the quota was set on the backend
cy.runOccCommand(`user:info --output=json '${user.userId}'`).then(($result) => {
expect($result.code).to.equal(0)
expect($result.exitCode).to.equal(0)
// TODO: Enable this after the file size handling is fixed!!!!!!
// const info = JSON.parse($result.stdout)
// expect(info?.quota).to.equal('4 MB')

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { User } from '@nextcloud/cypress'
import { User } from '@nextcloud/e2e-test-server/cypress'
const admin = new User('admin', 'admin')

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { randomBytes } from 'crypto'
import { getRowForFile, selectAllFiles, selectRowForFile, triggerSelectionAction } from '../files/FilesUtils.ts'

@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { randomBytes } from 'crypto'
import { closeSidebar, getRowForFile, triggerActionForFile } from '../files/FilesUtils.ts'

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { randomBytes } from 'crypto'
import { getRowForFile, triggerActionForFile } from '../files/FilesUtils.ts'

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { randomBytes } from 'crypto'
import { closeSidebar, getRowForFile, getRowForFileId, triggerActionForFile } from '../files/FilesUtils.ts'

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { User } from '@nextcloud/cypress'
import { User } from '@nextcloud/e2e-test-server/cypress'
import { NavigationHeader } from '../../pages/NavigationHeader.ts'
import {
defaultBackground,

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { User } from '@nextcloud/cypress'
import { User } from '@nextcloud/e2e-test-server/cypress'
import { NavigationHeader } from '../../pages/NavigationHeader.ts'
const admin = new User('admin', 'admin')

@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { User } from '@nextcloud/cypress'
import { User } from '@nextcloud/e2e-test-server/cypress'
const admin = new User('admin', 'admin')

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { NavigationHeader } from '../../pages/NavigationHeader.ts'
import { installTestApp, uninstallTestApp } from '../../support/commonUtils.ts'

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { User } from '@nextcloud/cypress'
import { User } from '@nextcloud/e2e-test-server/cypress'
import { NavigationHeader } from '../../pages/NavigationHeader.ts'
import { defaultBackground, defaultPrimary, validateBodyThemingCss } from './themingUtils.ts'

@ -0,0 +1,20 @@
<?php
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
$CONFIG = [
'apps_paths' => [
[
'path' => '/var/www/html/apps',
'url' => '/apps',
'writable' => false,
],
[
'path' => '/var/www/html/apps-cypress',
'url' => '/apps-cypress',
'writable' => true,
],
],
];

@ -3,9 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { addCommands, User } from '@nextcloud/cypress'
import { addCommands, User } from '@nextcloud/e2e-test-server/cypress'
import { basename } from '@nextcloud/paths'
import axios from 'axios'
import { basename } from 'path'
// Add custom commands
import '@testing-library/cypress/add-commands'
@ -18,7 +18,7 @@ Cypress.env('baseUrl', url)
/**
* Enable or disable a user
* TODO: standardize in @nextcloud/cypress
* TODO: standardize in @nextcloud/e2e-test-server
*
* @param {User} user the user to dis- / enable
* @param {boolean} enable True if the user should be enable, false to disable
@ -45,7 +45,7 @@ Cypress.Commands.add('enableUser', (user: User, enable = true) => {
/**
* cy.uploadedFile - uploads a file from the fixtures folder
* TODO: standardize in @nextcloud/cypress
* TODO: standardize in @nextcloud/e2e-test-server
*
* @param {User} user the owner of the file, e.g. admin
* @param {string} fixture the fixture file name, e.g. image1.jpg
@ -142,7 +142,7 @@ Cypress.Commands.add('rm', (user: User, target: string) => {
/**
* cy.uploadedContent - uploads a raw content
* TODO: standardize in @nextcloud/cypress
* TODO: standardize in @nextcloud/e2e-test-server
*
* @param {User} user the owner of the file, e.g. admin
* @param {Blob} blob the content to upload

@ -3,8 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { basename } from 'path'
/**
* Get the header navigation bar
*/
@ -49,14 +47,14 @@ export function clearState() {
export function installTestApp() {
const testAppPath = 'cypress/fixtures/testapp'
cy.runOccCommand('-V').then((output) => {
// @ts-expect-error we added this property in cypress.config.ts
const containerName = Cypress.config('dockerContainerName')
const version = output.stdout.match(/(\d\d+)\.\d+\.\d+/)?.[1]
cy.wrap(version).should('not.be.undefined')
getContainerName()
.then((containerName) => {
cy.exec(`docker cp '${testAppPath}' ${containerName}:/var/www/html/apps`, { log: true })
cy.exec(`docker exec --workdir /var/www/html ${containerName} chown -R www-data:www-data /var/www/html/apps/testapp`)
})
cy.runCommand(`sed -i -e 's|-version=\\"[0-9]\\+|-version=\\"${version}|g' apps/testapp/appinfo/info.xml`)
cy.exec(`docker cp '${testAppPath}' ${containerName}:/var/www/html/apps-cypress`, { log: true })
cy.exec(`docker exec --workdir /var/www/html ${containerName} chown -R www-data:www-data /var/www/html/apps-cypress/testapp`)
cy.runCommand(`sed -i -e 's|-version=\\"[0-9]\\+|-version=\\"${version}|g' apps-cypress/testapp/appinfo/info.xml`)
cy.runOccCommand('app:enable --force testapp')
})
}
@ -66,15 +64,5 @@ export function installTestApp() {
*/
export function uninstallTestApp() {
cy.runOccCommand('app:remove testapp', { failOnNonZeroExit: false })
cy.runCommand('rm -fr apps/testapp/appinfo/info.xml')
}
/**
*
*/
export function getContainerName(): Cypress.Chainable<string> {
return cy.exec('pwd')
.then(({ stdout }) => {
return cy.wrap(`nextcloud-cypress-tests_${basename(stdout).replace(' ', '')}`)
})
cy.runCommand('rm -fr apps-cypress/testapp')
}

@ -0,0 +1,11 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
/**
* Ensure the downloads folder is deleted before each test
*/
export function deleteDownloadsFolderBeforeEach() {
beforeEach(() => cy.task('deleteFolder', Cypress.config('downloadsFolder')))
}

@ -0,0 +1,19 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
export function randomString(length: number) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
const alphaNumeric = characters + '0123456789'
let result = ''
for (let i = 0; i < length; i++) {
// Ensure the first character is alphabetic
if (i === 0) {
result += characters.charAt(Math.floor(Math.random() * characters.length))
continue
}
result += alphaNumeric.charAt(Math.floor(Math.random() * alphaNumeric.length))
}
return result
}

769
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -126,9 +126,8 @@
"@babel/plugin-transform-private-methods": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
"@codecov/webpack-plugin": "^1.9.1",
"@cypress/webpack-preprocessor": "^7.0.0",
"@nextcloud/babel-config": "^1.2.0",
"@nextcloud/cypress": "^1.0.0-beta.15",
"@nextcloud/e2e-test-server": "^0.4.0",
"@nextcloud/eslint-config": "^9.0.0-rc.5",
"@nextcloud/stylelint-config": "^3.1.0",
"@nextcloud/typings": "^1.9.1",
@ -149,15 +148,15 @@
"babel-plugin-module-resolver": "^5.0.2",
"browserslist": "^4.26.3",
"colord": "^2.9.3",
"cypress": "^13.17.0",
"cypress": "^15.5.0",
"cypress-axe": "^1.7.0",
"cypress-delete-downloads-folder": "^0.0.6",
"cypress-if": "^1.13.2",
"cypress-split": "^1.24.24",
"cypress-split": "^1.24.25",
"cypress-vite": "^1.8.0",
"cypress-wait-until": "^3.0.2",
"dockerode": "^4.0.9",
"eslint": "^9.36.0",
"eslint-plugin-cypress": "^5.1.1",
"eslint-plugin-cypress": "^5.2.0",
"eslint-plugin-no-only-tests": "^3.3.0",
"exports-loader": "^5.0.0",
"file-loader": "^6.2.0",
@ -171,7 +170,6 @@
"sass": "^1.93.2",
"stylelint": "^16.24.0",
"stylelint-use-logical": "^2.1.2",
"tar": "^7.5.1",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.9.2",