Copy how cypress is run on the server

Signed-off-by: Louis Chemineau <louis@chmn.me>
This commit is contained in:
Louis Chemineau 2023-02-01 16:21:06 +01:00
parent efa2bf5471
commit e66d96dc34
24 changed files with 2248 additions and 1193 deletions

View File

@ -10,8 +10,6 @@ on:
env:
APP_NAME: photos
BRANCH: ${{ github.base_ref }}
CYPRESS_baseUrl: http://127.0.0.1:8082/index.php
TESTING: true
jobs:
init:
@ -22,15 +20,16 @@ jobs:
uses: actions/checkout@v3
- name: Read package.json node and npm engines version
uses: skjnldsv/read-package-engines-version-actions@v1.1
uses: skjnldsv/read-package-engines-version-actions@v2.0
id: versions
with:
fallbackNode: '^12'
fallbackNpm: '^6'
fallbackNode: "^14"
fallbackNpm: "^7"
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@v3
with:
cache: "npm"
node-version: ${{ steps.versions.outputs.nodeVersion }}
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
@ -40,7 +39,6 @@ jobs:
run: |
npm ci
TESTING=true npm run build --if-present
- name: Save context
uses: actions/cache@v3
with:
@ -55,7 +53,7 @@ jobs:
fail-fast: false
matrix:
# run multiple copies of the current job in parallel
containers: [1, 2, 3, 4, 5, 6, 7, 8]
containers: ["component", 1, 2]
name: runner ${{ matrix.containers }}
@ -66,21 +64,9 @@ jobs:
key: cypress-context-${{ github.run_id }}
path: /home/runner/work/photos
- name: Setup server
run: |
cd cypress
docker-compose up -d
- name: Run ${{ matrix.containers == 'component' && 'component' || 'E2E' }} cypress tests
- name: Wait for server
run: npm run wait-on $CYPRESS_baseUrl
- name: Enable app & configure server
run: |
cd cypress
docker-compose exec --env APP_NAME=${{ env.APP_NAME }} --env BRANCH=${{ env.BRANCH }} -T nextcloud bash /initserver.sh
- name: Cypress run
uses: cypress-io/github-action@v4
uses: cypress-io/github-action@v5
with:
record: true
parallel: true
@ -88,10 +74,14 @@ jobs:
ci-build-id: ${{ github.sha }}-${{ github.run_number }}
tag: ${{ github.event_name }}
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
# Needs to be prefixed with CYPRESS_
CYPRESS_BRANCH: ${{ env.BRANCH }}
# https://github.com/cypress-io/github-action/issues/124
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
# Needed for some specific code workarounds
TESTING: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
- name: Upload snapshots
uses: actions/upload-artifact@v3

View File

@ -1,28 +0,0 @@
const { defineConfig } = require("cypress");
const browserify = require('@cypress/browserify-preprocessor')
module.exports = defineConfig({
projectId: 'okzqgr',
viewportWidth: 1280,
viewportHeight: 720,
defaultCommandTimeout: 6000,
retries: 1,
env: {
failSilently: false,
type: 'actual',
},
screenshotsFolder: 'cypress/snapshots/actual',
trashAssetsBeforeRuns: true,
e2e: {
baseUrl: 'http://localhost:8082/index.php',
setupNodeEvents(on, config) {
// Fix browserslist extend https://github.com/cypress-io/cypress/issues/2983#issuecomment-570616682
on('file:preprocessor', browserify())
},
},
});

89
cypress.config.ts Normal file
View File

@ -0,0 +1,89 @@
import {
configureNextcloud,
startNextcloud,
stopNextcloud,
waitOnNextcloud,
} from './cypress/dockerNode'
import { defineConfig } from 'cypress'
import browserify from '@cypress/browserify-preprocessor'
import getCompareSnapshotsPlugin from 'cypress-visual-regression/dist/plugin'
export default defineConfig({
projectId: 'okzqgr',
// 16/9 screen ratio
viewportWidth: 1280,
viewportHeight: 720,
// Tries again 2 more times on failure
retries: {
runMode: 2,
// do not retry in `cypress open`
openMode: 0,
},
// Needed to trigger `after:run` events with cypress open
experimentalInteractiveRunEvents: true,
// faster video processing
videoCompression: false,
// Visual regression testing
env: {
failSilently: false,
type: 'actual',
},
screenshotsFolder: 'cypress/snapshots/actual',
trashAssetsBeforeRuns: true,
e2e: {
testIsolation: false,
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
async setupNodeEvents(on, config) {
// Fix browserslist extend https://github.com/cypress-io/cypress/issues/2983#issuecomment-570616682
on('file:preprocessor', browserify({ typescript: require.resolve('typescript') }))
getCompareSnapshotsPlugin(on, config)
// Disable spell checking to prevent rendering differences
on('before:browser:launch', (browser, launchOptions) => {
if (browser.family === 'chromium' && browser.name !== 'electron') {
launchOptions.preferences.default['browser.enable_spellchecking'] = false
return launchOptions
}
if (browser.family === 'firefox') {
launchOptions.preferences['layout.spellcheckDefault'] = 0
return launchOptions
}
if (browser.name === 'electron') {
launchOptions.preferences.spellcheck = false
return launchOptions
}
})
// Remove container after run
on('after:run', () => {
stopNextcloud()
})
// Before the browser launches
// starting Nextcloud testing container
return startNextcloud(process.env.BRANCH)
.then((ip) => {
// Setting container's IP as base Url
config.baseUrl = `http://${ip}/index.php`
return ip
})
.then(waitOnNextcloud)
.then(() => configureNextcloud(process.env.BRANCH))
.then(() => {
return config
})
},
},
})

224
cypress/dockerNode.ts Normal file
View File

@ -0,0 +1,224 @@
/**
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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/>.
*
*/
/* eslint-disable no-console */
/* eslint-disable n/no-unpublished-import */
/* eslint-disable n/no-extraneous-import */
import Docker from 'dockerode'
import path from 'path'
import waitOn from 'wait-on'
import pkg from '../package.json'
export const docker = new Docker()
const APP_PATH = path.resolve(__dirname, '../')
const APP_NAME = pkg.name
const CONTAINER_NAME = 'nextcloud-cypress-tests-' + APP_NAME
const SERVER_IMAGE = 'ghcr.io/nextcloud/continuous-integration-shallow-server'
/**
* Start the testing container
*/
export const startNextcloud = async function(branch: string = 'master'): Promise<any> {
try {
// Pulling images
console.log('\nPulling images... ⏳')
await new Promise((resolve, reject): any => docker.pull(SERVER_IMAGE, (err, stream) => {
if (err) {
reject(err)
}
// https://github.com/apocas/dockerode/issues/357
docker.modem.followProgress(stream, onFinished)
/**
*
* @param err
*/
function onFinished(err) {
if (!err) {
resolve(true)
return
}
reject(err)
}
}))
console.log('└─ Done')
// 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 (error) {
console.log('└─ None found!')
}
// Starting container
console.log('\nStarting Nextcloud container... 🚀')
console.log(`├─ Using branch '${branch}'`)
console.log(`├─ And binding app '${APP_NAME}' from '${APP_PATH}'`)
const container = await docker.createContainer({
Image: SERVER_IMAGE,
name: CONTAINER_NAME,
HostConfig: {
Binds: [
// TODO: improve local app directory detection
`${APP_PATH}/:/var/www/html/apps/${APP_NAME}`,
],
},
Env: [
`BRANCH=${branch}`,
],
})
await container.start()
// 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(err)
stopNextcloud()
throw new Error('Unable to start the container')
}
}
/**
* Configure Nextcloud
*/
export const configureNextcloud = async function(branch: string = 'master') {
console.log('\nConfiguring nextcloud...')
const container = docker.getContainer(CONTAINER_NAME)
await runExec(container, ['php', 'occ', '--version'], true)
// Clone the viewer app
await runExec(container, ['git', 'clone', '--depth', '1', '--branch', branch, 'https://github.com/nextcloud/viewer.git', '/var/www/html/apps/viewer'], 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)
console.log('└─ Nextcloud is now ready to use 🎉')
}
/**
* Force stop the testing container
*/
export const stopNextcloud = async function() {
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
*/
export const getContainerIP = async function(
container: Docker.Container = docker.getContainer(CONTAINER_NAME)
): Promise<string> {
let ip = ''
let tries = 0
while (ip === '' && tries < 10) {
tries++
await container.inspect(function(err, data) {
if (err) {
throw err
}
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
export const waitOnNextcloud = async function(ip: string) {
console.log('├─ Waiting for Nextcloud to be ready... ⏳')
await waitOn({ resources: [`http://${ip}/index.php`] })
console.log('└─ Done')
}
const runExec = async function(
container: Docker.Container,
command: string[],
verbose = false,
user = 'www-data'
) {
const exec = await container.exec({
Cmd: command,
AttachStdout: true,
AttachStderr: true,
User: user,
})
return new Promise((resolve, reject) => {
exec.start({}, (err, stream) => {
if (err) {
reject(err)
}
if (stream) {
stream.setEncoding('utf-8')
stream.on('data', str => {
if (verbose && str.trim() !== '') {
console.log(`├─ ${str.trim().replace(/\n/gi, '\n├─ ')}`)
}
})
stream.on('end', resolve)
}
})
})
}
const sleep = function(milliseconds: number) {
return new Promise((resolve) => setTimeout(resolve, milliseconds))
}

View File

@ -19,116 +19,126 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { randHash } from '../utils'
const randUser = randHash()
import {
addFilesToAlbumFromAlbum,
createAnAlbumFromAlbums,
deleteAnAlbumFromAlbumContent,
removeSelectionFromAlbum,
} from './albumsUtils'
import {
downloadSelection,
favoriteSelection,
selectMedia,
unfavoriteSelection,
unselectMedia,
uploadTestMedia,
} from './photosUtils'
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
Cypress.on('uncaught:exception', (err) => {
/* returning false here prevents Cypress from failing the test */
if (resizeObserverLoopErrRe.test(err.message)) {
return false
}
/* returning false here prevents Cypress from failing the test */
if (resizeObserverLoopErrRe.test(err.message)) {
return false
}
})
describe('Manage albums', () => {
before(function () {
cy.logout()
cy.nextcloudCreateUser(randUser, 'password')
before(function() {
cy.createRandomUser()
.then((user) => {
uploadTestMedia(user)
cy.login(user)
cy.visit('/apps/photos')
})
})
cy.login(randUser, 'password')
cy.uploadTestMedia()
beforeEach(() => {
cy.visit(`${Cypress.env('baseUrl')}/index.php/apps/photos/albums`)
createAnAlbumFromAlbums('albums_test')
addFilesToAlbumFromAlbum('albums_test', [0, 1, 2])
})
// wait a bit for things to be settled
cy.wait(1000)
})
afterEach(() => {
deleteAnAlbumFromAlbumContent()
cy.contains('There is no album yet!').click()
})
beforeEach(() => {
cy.visit(`${Cypress.env('baseUrl')}/index.php/apps/photos/albums`)
cy.createAnAlbumFromAlbums('albums_test')
cy.addFilesToAlbumFromAlbum('albums_test', [0, 1, 2])
})
it('Add and remove a file to an album from an album', () => {
selectMedia([0])
removeSelectionFromAlbum()
})
afterEach(() => {
cy.deleteAnAlbumFromAlbumContent()
cy.contains("There is no album yet!").click()
})
it('Add and remove multiple files to an album from an album', () => {
selectMedia([0, 1])
removeSelectionFromAlbum()
})
it('Add and remove a file to an album from an album', () => {
cy.selectMedia([0])
cy.removeSelectionFromAlbum()
})
it('Favorite a file from an album', () => {
selectMedia([0])
favoriteSelection()
cy.get('[data-test="media"]').eq(0).find('[aria-label="The file is in the favorites"]')
unfavoriteSelection()
unselectMedia([0])
cy.get('[aria-label="The file is in the favorites"]').should('not.exist')
})
it('Add and remove multiple files to an album from an album', () => {
cy.selectMedia([0, 1])
cy.removeSelectionFromAlbum()
})
it('Favorite multiple files from an album', () => {
selectMedia([1, 2])
favoriteSelection()
cy.get('[data-test="media"]').eq(1).find('[aria-label="The file is in the favorites"]')
cy.get('[data-test="media"]').eq(2).find('[aria-label="The file is in the favorites"]')
unfavoriteSelection()
unselectMedia([1, 2])
cy.get('[aria-label="The file is in the favorites"]').should('not.exist')
})
it('Favorite a file from an album', () => {
cy.selectMedia([0])
cy.favoriteSelection()
cy.get('[data-test="media"]').eq(0).find('[aria-label="The file is in the favorites"]')
cy.unfavoriteSelection()
cy.unselectMedia([0])
cy.get('[aria-label="The file is in the favorites"]').should('not.exist')
})
it('Download a file from an album', () => {
selectMedia([0])
downloadSelection()
unselectMedia([0])
})
it('Favorite multiple files from an album', () => {
cy.selectMedia([1, 2])
cy.favoriteSelection()
cy.get('[data-test="media"]').eq(1).find('[aria-label="The file is in the favorites"]')
cy.get('[data-test="media"]').eq(2).find('[aria-label="The file is in the favorites"]')
cy.unfavoriteSelection()
cy.unselectMedia([1, 2])
cy.get('[aria-label="The file is in the favorites"]').should('not.exist')
})
it('Download multiple files from an album', () => {
selectMedia([1, 2])
downloadSelection()
unselectMedia([1, 2])
})
it('Download a file from an album', () => {
cy.selectMedia([0])
cy.downloadSelection()
cy.unselectMedia([0])
})
it('Download all files from an album', () => {
selectMedia([1, 2])
downloadSelection()
unselectMedia([1, 2])
})
it('Download multiple files from an album', () => {
cy.selectMedia([1, 2])
cy.downloadSelection()
cy.unselectMedia([1, 2])
})
it('Edit an album\'s name', () => {
cy.get('[aria-label="Open actions menu"]').click()
cy.contains('Edit album details').click()
cy.get('form [name="name"]').clear().type('New name')
cy.contains('Save').click()
it('Download all files from an album', () => {
cy.selectMedia([1, 2])
cy.downloadSelection()
cy.unselectMedia([1, 2])
})
cy.reload()
it('Edit an album\'s name', () => {
cy.get('[aria-label="Open actions menu"]').click()
cy.contains('Edit album details').click()
cy.get('form [name="name"]').clear().type("New name")
cy.contains('Save').click()
cy.contains('New name')
cy.reload()
cy.get('[aria-label="Open actions menu"]').click()
cy.contains('Edit album details').click()
cy.get('form [name="name"]').clear().type('albums_test')
cy.contains('Save').click()
})
cy.contains('New name')
it('Edit an album\'s location', () => {
cy.get('[aria-label="Open actions menu"]').click()
cy.contains('Edit album details').click()
cy.get('form [name="location"]').clear().type('New location')
cy.contains('Save').click()
cy.get('[aria-label="Open actions menu"]').click()
cy.contains('Edit album details').click()
cy.get('form [name="name"]').clear().type("albums_test")
cy.contains('Save').click()
})
cy.reload()
it('Edit an album\'s location', () => {
cy.get('[aria-label="Open actions menu"]').click()
cy.contains('Edit album details').click()
cy.get('form [name="location"]').clear().type("New location")
cy.contains('Save').click()
cy.contains('New location')
cy.reload()
cy.contains('New location')
cy.get('[aria-label="Open actions menu"]').click()
cy.contains('Edit album details').click()
cy.get('form [name="location"]').clear()
cy.contains('Save').click()
})
cy.get('[aria-label="Open actions menu"]').click()
cy.contains('Edit album details').click()
cy.get('form [name="location"]').clear()
cy.contains('Save').click()
})
})

View File

@ -0,0 +1,87 @@
/**
* @copyright Copyright (c) 2023 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 { selectMedia } from "./photosUtils"
export function createAnAlbumFromTimeline(albumName: string) {
cy.contains('Add').click()
cy.contains('Create new album').click()
cy.get('form [name="name"]').type(albumName)
cy.contains('Create album').click()
}
export function createAnAlbumFromAlbums(albumName: string) {
cy.contains('New album').click()
cy.get('form [name="name"]').type(albumName)
cy.contains('Create album').click()
}
export function deleteAnAlbumFromAlbumContent() {
cy.get('[aria-label="Open actions menu"]').click()
cy.contains('Delete album').click()
}
export function addFilesToAlbumFromTimeline(albumName: string) {
cy.contains('Add to album').click()
cy.get('.album-picker ul').contains(albumName).click()
}
export function addFilesToAlbumFromAlbum(albumName: string, itemsIndex: number[]) {
cy.get('[aria-label="Add photos to this album"]').click()
cy.get('.file-picker__file-list').within(() => {
selectMedia(itemsIndex)
})
cy.contains(`Add to ${albumName}`).click()
}
export function removeSelectionFromAlbum() {
cy.get('[aria-label="Open actions menu"]').click()
cy.contains('Remove selection from album').click()
}
export function goToAlbum(albumName: string) {
cy.get('.app-navigation__list').contains('Albums').click()
cy.get('ul.collections__list').contains(albumName).click()
}
export function addCollaborators(collaborators: string[]) {
cy.get('[aria-label="Manage collaborators for this album"]').click()
collaborators.forEach((collaborator: string) => {
cy.get('[aria-label="Search for collaborators"').type(collaborator)
cy.contains(collaborator).click()
})
cy.contains('Save').click()
}
export function removeCollaborators(collaborators: string[]) {
cy.get('[aria-label="Manage collaborators for this album"]').click()
collaborators.forEach((collaborator: string) => {
cy.get('.manage-collaborators')
.within(() => {
cy.contains(collaborator)
.parentsUntil('ul')
.get(`[aria-label="Remove ${collaborator} from the collaborators list"]`)
.click()
})
})
cy.contains('Save').click()
}

View File

@ -0,0 +1,84 @@
/**
* @copyright Copyright (c) 2023 Louis Chmn <louis@chmn.me>
*
* @author Louis Chmn <louis@chmn.me>
*
* 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"
export function uploadTestMedia(user: User) {
cy.exec('ls cypress/fixtures/media')
.then((result) => {
for (const fileName of result.stdout.split('\n')) {
cy.uploadFile(user, `media/${fileName}`, 'image/png', `/${fileName}`)
}
})
}
export function selectMedia(indexes: number[]) {
indexes.forEach((index: number) => {
cy.get('[data-test="media"]').eq(index)
.find('a').focus()
.parent().find('input').check({ force: true })
})
}
export function unselectMedia(indexes: number[]) {
indexes.forEach((index: number) => {
cy.get('[data-test="media"]').eq(index)
.find('a').focus()
.parent().find('input').uncheck({ force: true })
})
}
export function favoriteSelection() {
cy.get('[aria-label="Open actions menu"]').click()
cy.get('[aria-label="Mark selection as favorite"]').click()
}
export function unfavoriteSelection() {
cy.get('[aria-label="Open actions menu"]').click()
cy.get('[aria-label="Remove selection from favorites"]').click()
}
export function downloadSelection() {
cy.get('[aria-label="Open actions menu"]').click()
cy.get('[aria-label="Download selected files"]').trigger('click')
}
export function downloadAllFiles() {
cy.get('[aria-label="Open actions menu"]').click()
cy.get('[aria-label="Download all files in album"]').trigger('click')
}
export function deleteSelection() {
cy.intercept({ method: 'DELETE' }).as('deleteRequests')
cy.get('[aria-label="Open actions menu"]').click()
cy.contains('Delete selection')
.click()
.wait('@deleteRequests')
}
export function goToSharedAlbum(albumName: string) {
cy.get('.app-navigation__list').contains('Collaborative albums').click()
cy.get('ul.collections__list').contains(albumName).click()
}
export function removeSharedAlbums() {
cy.get('[aria-label="Open actions menu"]').click()
cy.contains('Delete album').click()
}

View File

@ -0,0 +1,31 @@
/**
* @copyright Copyright (c) 2023 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/>.
*
*/
export function goToSharedAlbum(albumName: string) {
cy.get('.app-navigation__list').contains('Collaborative albums').click()
cy.get('ul.collections__list').contains(albumName).click()
}
export function removeSharedAlbums() {
cy.get('[aria-label="Open actions menu"]').click()
cy.contains('Delete album').click()
}

View File

@ -1,187 +0,0 @@
/**
* @copyright Copyright (c) 2022 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 { randHash } from '../utils'
const alice = `alice_${randHash()}`
const bob = `bob_${randHash()}`
const charlie = `charlie_${randHash()}`
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
Cypress.on('uncaught:exception', (err) => {
/* returning false here prevents Cypress from failing the test */
if (resizeObserverLoopErrRe.test(err.message)) {
return false
}
})
describe('Manage shared albums', () => {
before(() => {
cy.visit('')
cy.logout()
cy.nextcloudCreateUser(alice, 'password')
cy.nextcloudCreateUser(bob, 'password')
cy.nextcloudCreateUser(charlie, 'password')
cy.login(bob, 'password')
cy.uploadTestMedia()
cy.logout()
})
beforeEach(() => {
cy.logout()
cy.login(bob, 'password', '/apps/photos/sharedalbums')
})
context('Adding and removing files in a shared album', () => {
before(() => {
cy.logout()
cy.login(alice, 'password')
cy.visit(`${Cypress.env('baseUrl')}/index.php/apps/photos/albums`)
cy.createAnAlbumFromAlbums('shared_album_test1')
cy.addCollaborators([bob])
cy.logout()
})
it('Add and remove a file to a shared album from a shared album', () => {
cy.goToSharedAlbum('shared_album_test1')
cy.get('[data-test="media"]').should('have.length', 0)
cy.addFilesToAlbumFromAlbum('shared_album_test1', [0])
cy.get('[data-test="media"]').should('have.length', 1)
cy.selectMedia([0])
cy.removeSelectionFromAlbum()
cy.get('[data-test="media"]').should('have.length', 0)
})
it('Add and remove multiple files to a shared album from a shared album', () => {
cy.goToSharedAlbum('shared_album_test1')
cy.get('[data-test="media"]').should('have.length', 0)
cy.addFilesToAlbumFromAlbum('shared_album_test1', [1, 2])
cy.get('[data-test="media"]').should('have.length', 2)
cy.selectMedia([0, 1])
cy.removeSelectionFromAlbum()
cy.get('[data-test="media"]').should('have.length', 0)
})
})
context('Download files from a shared album', () => {
before(() => {
cy.logout()
cy.login(alice, 'password')
cy.visit(`${Cypress.env('baseUrl')}/index.php/apps/photos/albums`)
cy.createAnAlbumFromAlbums('shared_album_test2')
cy.addCollaborators([bob])
cy.logout()
cy.login(bob, 'password')
cy.visit(`${Cypress.env('baseUrl')}/index.php/apps/photos/sharedalbums`)
cy.goToSharedAlbum('shared_album_test2')
cy.addFilesToAlbumFromAlbum('shared_album_test2', [0, 1, 2])
cy.logout()
})
xit('Download a file from a shared album', () => {
cy.goToSharedAlbum('shared_album_test2')
cy.selectMedia([0])
cy.downloadSelection()
cy.unselectMedia([0])
})
xit('Download multiple files from a shared album', () => {
cy.goToSharedAlbum('shared_album_test2')
cy.selectMedia([1, 2])
cy.downloadSelection()
cy.unselectMedia([1, 2])
})
xit('Download all files from a shared album', () => {
cy.goToSharedAlbum('shared_album_test2')
cy.downloadAllFiles()
})
})
context('Delete a received shared album', () => {
before(() => {
cy.logout()
cy.login(alice, 'password')
cy.visit(`${Cypress.env('baseUrl')}/index.php/apps/photos/albums`)
cy.createAnAlbumFromAlbums('shared_album_test3')
cy.addCollaborators([bob])
cy.logout()
})
it('Remove shared album', () => {
cy.goToSharedAlbum('shared_album_test3')
cy.removeSharedAlbums()
})
})
context('Remove a collaborator from an album', () => {
before(() => {
cy.logout()
cy.login(alice, 'password', '/apps/photos/albums')
cy.createAnAlbumFromAlbums('shared_album_test4')
cy.addCollaborators([bob])
cy.logout()
})
it('Remove collaborator from an album', () => {
cy.get('ul.collections__list li')
.should('contain', `shared_album_test4 (${alice})`)
cy.logout()
cy.login(alice, 'password', '/apps/photos')
cy.goToAlbum('shared_album_test4')
cy.removeCollaborators([bob])
cy.logout()
cy.login(bob, 'password', '/apps/photos/sharedalbums')
cy.get('body')
.should('not.contain', `shared_album_test4 (${alice})`)
})
})
context('Two shared albums with the same name', () => {
before(() => {
cy.logout()
cy.login(alice, 'password')
cy.visit(`${Cypress.env('baseUrl')}/index.php/apps/photos/albums`)
cy.createAnAlbumFromAlbums('shared_album_test5')
cy.addCollaborators([bob])
cy.logout()
cy.login(charlie, 'password')
cy.visit(`${Cypress.env('baseUrl')}/index.php/apps/photos/albums`)
cy.createAnAlbumFromAlbums('shared_album_test5')
cy.addCollaborators([bob])
cy.logout()
})
it('It should display two shared albums', () => {
cy.get('ul.collections__list li')
.contains(`shared_album_test5 (${alice})`)
cy.get('ul.collections__list li')
.contains(`shared_album_test5 (${charlie})`)
})
})
})

View File

@ -0,0 +1,194 @@
import { randHash } from '../utils'
/**
* @copyright Copyright (c) 2022 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 { User } from '@nextcloud/cypress'
import {
addCollaborators,
addFilesToAlbumFromAlbum,
createAnAlbumFromAlbums,
goToAlbum,
removeCollaborators,
removeSelectionFromAlbum,
} from './albumsUtils'
import {
downloadAllFiles,
downloadSelection,
goToSharedAlbum,
removeSharedAlbums,
selectMedia,
uploadTestMedia,
} from './photosUtils'
import { randHash } from '../utils'
const alice = new User(`alice_${randHash()}`)
const bob = new User(`bob_${randHash()}`)
const charlie = new User(`charlie_${randHash()}`)
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
Cypress.on('uncaught:exception', (err) => {
/* returning false here prevents Cypress from failing the test */
if (resizeObserverLoopErrRe.test(err.message)) {
return false
}
})
describe('Manage shared albums', () => {
before(() => {
cy.createUser(alice)
cy.createUser(bob).then(() => {
uploadTestMedia(bob)
})
cy.createUser(charlie)
})
context('Adding and removing files in a shared album', () => {
before(() => {
cy.login(alice)
cy.visit('apps/photos/albums')
createAnAlbumFromAlbums('shared_album_test1')
addCollaborators([bob.userId])
})
it('Add and remove a file to a shared album from a shared album', () => {
cy.login(bob)
cy.visit('apps/photos/albums')
goToSharedAlbum('shared_album_test1')
cy.get('[data-test="media"]').should('have.length', 0)
addFilesToAlbumFromAlbum('shared_album_test1', [0])
cy.get('[data-test="media"]').should('have.length', 1)
selectMedia([0])
removeSelectionFromAlbum()
cy.get('[data-test="media"]').should('have.length', 0)
})
it('Add and remove multiple files to a shared album from a shared album', () => {
goToSharedAlbum('shared_album_test1')
cy.get('[data-test="media"]').should('have.length', 0)
addFilesToAlbumFromAlbum('shared_album_test1', [1, 2])
cy.get('[data-test="media"]').should('have.length', 2)
selectMedia([0, 1])
removeSelectionFromAlbum()
cy.get('[data-test="media"]').should('have.length', 0)
})
})
context('Download files from a shared album', () => {
before(() => {
cy.login(alice)
cy.visit('apps/photos/albums')
createAnAlbumFromAlbums('shared_album_test2')
addCollaborators([bob.userId])
cy.login(bob)
cy.visit('apps/photos/sharedalbums')
goToSharedAlbum('shared_album_test2')
addFilesToAlbumFromAlbum('shared_album_test2', [0, 1, 2])
})
xit('Download a file from a shared album', () => {
goToSharedAlbum('shared_album_test2')
selectMedia([0])
downloadSelection()
selectMedia([0])
})
xit('Download multiple files from a shared album', () => {
goToSharedAlbum('shared_album_test2')
selectMedia([1, 2])
downloadSelection()
selectMedia([1, 2])
})
xit('Download all files from a shared album', () => {
goToSharedAlbum('shared_album_test2')
downloadAllFiles()
})
})
context('Delete a received shared album', () => {
before(() => {
cy.login(alice)
cy.visit('apps/photos/albums')
createAnAlbumFromAlbums('shared_album_test3')
addCollaborators([bob.userId])
})
it('Remove shared album', () => {
cy.login(bob)
cy.visit('apps/photos/albums')
goToSharedAlbum('shared_album_test3')
removeSharedAlbums()
})
})
context('Remove a collaborator from an album', () => {
before(() => {
cy.login(alice)
cy.visit('/apps/photos/albums')
createAnAlbumFromAlbums('shared_album_test4')
addCollaborators([bob.userId])
})
it('Remove collaborator from an album', () => {
cy.login(bob)
cy.visit('apps/photos/sharedalbums')
cy.get('ul.collections__list li')
.should('contain', `shared_album_test4 (${alice.userId})`)
cy.login(alice)
cy.visit('/apps/photos')
goToAlbum('shared_album_test4')
removeCollaborators([bob.userId])
cy.login(bob)
cy.visit('/apps/photos/sharedalbums')
cy.get('body')
.should('not.contain', `shared_album_test4 (${alice.userId})`)
})
})
context('Two shared albums with the same name', () => {
before(() => {
cy.login(alice)
cy.visit('apps/photos/albums')
createAnAlbumFromAlbums('shared_album_test5')
addCollaborators([bob.userId])
cy.login(charlie)
cy.visit('apps/photos/albums')
createAnAlbumFromAlbums('shared_album_test5')
addCollaborators([bob.userId])
})
it('It should display two shared albums', () => {
cy.login(bob)
cy.visit('/apps/photos/sharedalbums')
cy.get('ul.collections__list li')
.contains(`shared_album_test5 (${alice.userId})`)
cy.get('ul.collections__list li')
.contains(`shared_album_test5 (${charlie.userId})`)
})
})
})

View File

@ -19,93 +19,105 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { randHash } from '../utils'
const randUser = randHash()
import {
addFilesToAlbumFromTimeline,
createAnAlbumFromTimeline,
deleteAnAlbumFromAlbumContent,
goToAlbum,
} from './albumsUtils'
import {
deleteSelection,
downloadSelection,
favoriteSelection,
selectMedia,
unfavoriteSelection,
unselectMedia,
uploadTestMedia,
} from './photosUtils'
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
Cypress.on('uncaught:exception', (err) => {
/* returning false here prevents Cypress from failing the test */
if (resizeObserverLoopErrRe.test(err.message)) {
return false
}
/* returning false here prevents Cypress from failing the test */
if (resizeObserverLoopErrRe.test(err.message)) {
return false
}
})
describe('View list of photos in the main timeline', () => {
before(() => {
cy.logout()
cy.nextcloudCreateUser(randUser, 'password')
before(() => {
cy.createRandomUser()
.then((user) => {
uploadTestMedia(user)
cy.login(user)
cy.visit('/apps/photos')
})
})
cy.login(randUser, 'password')
cy.uploadTestMedia()
beforeEach(() => {
cy.visit(`${Cypress.env('baseUrl')}/index.php/apps/photos`)
})
// wait a bit for things to be settled
cy.wait(1000)
})
it('Favorite a file from a timeline', () => {
selectMedia([0])
favoriteSelection()
cy.get('[data-test="media"]').eq(0).find('[aria-label="The file is in the favorites"]')
unfavoriteSelection()
unselectMedia([0])
cy.get('[aria-label="The file is in the favorites"]').should('not.exist')
})
beforeEach(() => {
cy.visit(`${Cypress.env('baseUrl')}/index.php/apps/photos`)
})
it('Favorite multiple files from a timeline', () => {
selectMedia([1, 2])
favoriteSelection()
cy.get('[data-test="media"]').eq(1).find('[aria-label="The file is in the favorites"]')
cy.get('[data-test="media"]').eq(2).find('[aria-label="The file is in the favorites"]')
unfavoriteSelection()
unselectMedia([1, 2])
cy.get('[aria-label="The file is in the favorites"]').should('not.exist')
})
it('Favorite a file from a timeline', () => {
cy.selectMedia([0])
cy.favoriteSelection()
cy.get('[data-test="media"]').eq(0).find('[aria-label="The file is in the favorites"]')
cy.unfavoriteSelection()
cy.unselectMedia([0])
cy.get('[aria-label="The file is in the favorites"]').should('not.exist')
})
it('Download a file from a timeline', () => {
selectMedia([0])
downloadSelection()
unselectMedia([0])
})
it('Favorite multiple files from a timeline', () => {
cy.selectMedia([1, 2])
cy.favoriteSelection()
cy.get('[data-test="media"]').eq(1).find('[aria-label="The file is in the favorites"]')
cy.get('[data-test="media"]').eq(2).find('[aria-label="The file is in the favorites"]')
cy.unfavoriteSelection()
cy.unselectMedia([1, 2])
cy.get('[aria-label="The file is in the favorites"]').should('not.exist')
})
it('Download multiple files from a timeline', () => {
selectMedia([1, 2])
downloadSelection()
unselectMedia([1, 2])
})
it('Download a file from a timeline', () => {
cy.selectMedia([0])
cy.downloadSelection()
cy.unselectMedia([0])
})
it('Add file to an album from a timeline', () => {
createAnAlbumFromTimeline('timeline_test_single')
selectMedia([0])
addFilesToAlbumFromTimeline('timeline_test_single')
goToAlbum('timeline_test_single')
cy.get('[data-test="media"]').should('have.length', 1)
deleteAnAlbumFromAlbumContent()
})
it('Download multiple files from a timeline', () => {
cy.selectMedia([1, 2])
cy.downloadSelection()
cy.unselectMedia([1, 2])
})
it('Add multiple files to an album from a timeline', () => {
createAnAlbumFromTimeline('timeline_test_multiple')
selectMedia([1, 2])
addFilesToAlbumFromTimeline('timeline_test_multiple')
goToAlbum('timeline_test_multiple')
cy.get('[data-test="media"]').should('have.length', 2)
deleteAnAlbumFromAlbumContent()
})
it('Add file to an album from a timeline', () => {
cy.createAnAlbumFromTimeline('timeline_test_single')
cy.selectMedia([0])
cy.addFilesToAlbumFromTimeline('timeline_test_single')
cy.goToAlbum('timeline_test_single')
cy.get('[data-test="media"]').should('have.length', 1)
cy.deleteAnAlbumFromAlbumContent()
})
it('Delete a file from photos', () => {
cy.get('[data-test="media"]').should('have.length', 5)
selectMedia([0])
deleteSelection()
cy.get('[data-test="media"]').should('have.length', 4)
})
it('Add multiple files to an album from a timeline', () => {
cy.createAnAlbumFromTimeline('timeline_test_multiple')
cy.selectMedia([1, 2])
cy.addFilesToAlbumFromTimeline('timeline_test_multiple')
cy.goToAlbum('timeline_test_multiple')
cy.get('[data-test="media"]').should('have.length', 2)
cy.deleteAnAlbumFromAlbumContent()
})
it('Delete a file from photos', () => {
cy.get('[data-test="media"]').should('have.length', 5)
cy.selectMedia([0])
cy.deleteSelection()
cy.get('[data-test="media"]').should('have.length', 4)
})
it('Delete multiple files from photos', () => {
cy.get('[data-test="media"]').should('have.length', 4)
cy.selectMedia([1, 2])
cy.deleteSelection()
cy.get('[data-test="media"]').should('have.length', 2)
})
})
it('Delete multiple files from photos', () => {
cy.get('[data-test="media"]').should('have.length', 4)
selectMedia([1, 2])
deleteSelection()
cy.get('[data-test="media"]').should('have.length', 2)
})
})

View File

@ -1,16 +0,0 @@
#!/usr/bin/env bash
echo "APP_NAME: $APP_NAME"
echo "BRANCH: $BRANCH"
chown -R www-data:www-data /var/www/html/data
su www-data -c "
php occ config:system:set force_language --value en
php occ config:system:set enforce_theme --value light
php occ app:enable $APP_NAME
php occ app:list
"
cd apps
git clone --depth 1 https://github.com/nextcloud/viewer.git

View File

@ -1,18 +0,0 @@
#!/usr/bin/env bash
# RUN THIS SCRIPT FROM THE ROOT FOLDER OF YOUR APP
APP_NAME=${PWD##*/}
CYPRESS_baseUrl=http://127.0.0.1:8082/index.php
if [[ $APP_NAME == "cypress" ]]
then
echo "Please run this app from your app root folder."
else
echo "Launching docker server for the $APP_NAME app"
cd cypress
docker-compose pull
docker-compose up -d --force-recreate
npm run wait-on $CYPRESS_baseUrl
echo "Nextcloud successfully installed"
docker-compose exec --env APP_NAME=$APP_NAME -T nextcloud bash /initserver.sh
echo "Nextcloud successfully configured"
fi

View File

@ -1,12 +0,0 @@
#!/usr/bin/env bash
# RUN THIS SCRIPT FROM THE ROOT FOLDER OF YOUR APP
appname=${PWD##*/}
if [[ $appname == "cypress" ]]
then
echo "Please run this app from your app root folder."
else
echo "Killing server for the $appname app"
cd cypress
docker-compose stop
fi

View File

@ -1,247 +0,0 @@
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @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/>.
*
*/
/// <reference types="Cypress" />
const url = Cypress.config('baseUrl').replace(/\/index.php\/?$/g, '')
Cypress.env('baseUrl', url)
Cypress.Commands.add('login', (user, password, route = '/apps/files') => {
Cypress.Cookies.defaults({
preserve: /^(oc|nc)/,
})
cy.visit(route)
cy.get('input[name=user]').type(user)
cy.get('input[name=password]').type(password)
cy.get('form[name=login] [type=submit]').click()
cy.url().should('include', route)
})
Cypress.Commands.add('logout', () => {
cy.getCookies()
.then(cookies => {
if (cookies.length === 0) {
cy.log('Not logged, skipping logout...')
return
}
return cy.get("body")
.then($body => {
const $settingsButton = $body.find('#settings #expand')
if ($settingsButton.length === 0) {
cy.log("Not logged in.")
return
}
$settingsButton.click()
cy.contains('Log out').click()
})
})
})
Cypress.Commands.add('nextcloudCreateUser', (user, password) => {
cy.request({
method: 'POST',
url: `${Cypress.env('baseUrl')}/ocs/v1.php/cloud/users?format=json`,
form: true,
body: {
userid: user,
password,
},
auth: { user: 'admin', pass: 'admin' },
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'OCS-ApiRequest': 'true',
Authorization: `Basic ${Buffer.from('admin:admin').toString('base64')}`,
},
})
cy.clearCookies()
})
Cypress.Commands.add('uploadTestMedia', () => {
cy.exec('ls cypress/fixtures/media')
.then((result) => {
for (const fileName of result.stdout.split('\n')) {
cy.uploadFile(`media/${fileName}`, 'image/png', '', fileName)
}
})
})
/**
* cy.uploadedFile - uploads a file from the fixtures folder
*
* @param {string} fixtureFileName
* @param {string} mimeType eg. image/png
* @param {string} path to the folder in which this file should be uploaded
* @param {string} uploadedFileName alternative name to give the file while uploading
*/
Cypress.Commands.add('uploadFile', (fixtureFileName, mimeType, path = '', uploadedFileName = null) => {
if (uploadedFileName === null) {
uploadedFileName = fixtureFileName;
}
let file = null
return cy.fixture(fixtureFileName, 'base64')
.then(fileBase64 => {
// convert the logo base64 string to a blob
const blob = Cypress.Blob.base64StringToBlob(fileBase64, mimeType)
file = new File([blob], uploadedFileName, { type: mimeType })
return cy.window()
})
.then(window => {
const encodedPath = path.split("/")
.map(encodeURIComponent)
.join("/")
const url = `${Cypress.env('baseUrl')}/remote.php/webdav${encodedPath}/${encodeURIComponent(uploadedFileName)}`
return cy.request({
method: 'PUT',
url,
body: file,
encoding: 'binary',
headers: {
'Content-Type': mimeType,
requesttoken: window.OC.requestToken,
},
})
})
})
Cypress.Commands.add('selectMedia', (indexes) => {
indexes.forEach(index => {
cy.get('[data-test="media"]').eq(index)
.find('a').focus()
.parent().find('input').check({ force: true })
})
})
Cypress.Commands.add('unselectMedia', indexes => {
indexes.forEach(index => {
cy.get('[data-test="media"]').eq(index)
.find('a').focus()
.parent().find('input').uncheck({ force: true })
})
})
Cypress.Commands.add('favoriteSelection', () => {
cy.get('[aria-label="Open actions menu"]').click()
cy.get('[aria-label="Mark selection as favorite"]').click()
})
Cypress.Commands.add('unfavoriteSelection', () => {
cy.get('[aria-label="Open actions menu"]').click()
cy.get('[aria-label="Remove selection from favorites"]').click()
})
Cypress.Commands.add('downloadSelection', () => {
cy.get('[aria-label="Open actions menu"]').click()
cy.get('[aria-label="Download selected files"]').trigger('click')
})
Cypress.Commands.add('downloadAllFiles', () => {
cy.get('[aria-label="Open actions menu"]').click()
cy.get('[aria-label="Download all files in album"]').trigger('click')
})
Cypress.Commands.add('createAnAlbumFromTimeline', albumName => {
cy.contains('Add').click()
cy.contains('Create new album').click()
cy.get('form [name="name"]').type(albumName)
cy.contains('Create album').click()
})
Cypress.Commands.add('createAnAlbumFromAlbums', albumName => {
cy.contains('New album').click()
cy.get('form [name="name"]').type(albumName)
cy.contains('Create album').click()
})
Cypress.Commands.add('deleteAnAlbumFromAlbumContent', albumName => {
cy.get('[aria-label="Open actions menu"]').click()
cy.contains('Delete album').click()
})
Cypress.Commands.add('addFilesToAlbumFromTimeline', albumName => {
cy.contains('Add to album').click()
cy.get('.album-picker ul').contains(albumName).click()
})
Cypress.Commands.add('addFilesToAlbumFromAlbum', (albumName, itemsIndex) => {
cy.get('[aria-label="Add photos to this album"]').click()
cy.get('.file-picker__file-list').within(() => {
cy.selectMedia(itemsIndex)
})
cy.contains(`Add to ${albumName}`).click()
})
Cypress.Commands.add('deleteSelection', () => {
cy.intercept({ method: 'DELETE' }).as("deleteRequests");
cy.get('[aria-label="Open actions menu"]').click()
cy.contains("Delete selection")
.click()
.wait('@deleteRequests')
})
Cypress.Commands.add('removeSelectionFromAlbum', () => {
cy.get('[aria-label="Open actions menu"]').click()
cy.contains("Remove selection from album").click()
})
Cypress.Commands.add('goToAlbum', albumName => {
cy.get('.app-navigation__list').contains('Albums').click()
cy.get('ul.collections__list').contains(albumName).click()
})
Cypress.Commands.add('goToSharedAlbum', albumName => {
cy.get('.app-navigation__list').contains('Collaborative albums').click()
cy.get('ul.collections__list').contains(albumName).click()
})
Cypress.Commands.add('addCollaborators', collaborators => {
cy.get('[aria-label="Manage collaborators for this album"]').click()
collaborators.forEach((collaborator) => {
cy.get('[aria-label="Search for collaborators"').type(collaborator)
cy.contains(collaborator).click()
})
cy.contains('Save').click()
})
Cypress.Commands.add('removeCollaborators', collaborators => {
cy.get('[aria-label="Manage collaborators for this album"]').click()
collaborators.forEach((collaborator) => {
cy.get('.manage-collaborators')
.within(() => {
cy.contains(collaborator)
.parentsUntil('ul')
.get(`[aria-label="Remove ${collaborator} from the collaborators list"]`)
.click()
})
})
cy.contains('Save').click()
})
Cypress.Commands.add('removeSharedAlbums', () => {
cy.get('[aria-label="Open actions menu"]').click()
cy.contains("Delete album").click()
})

119
cypress/support/commands.ts Normal file
View File

@ -0,0 +1,119 @@
/**
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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/>.
*
*/
/* eslint-disable n/no-unpublished-import */
import axios from '@nextcloud/axios'
import { addCommands, User } from '@nextcloud/cypress'
import { basename } from 'path'
// Add custom commands
import 'cypress-wait-until'
addCommands()
// Register this file's custom commands types
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable<Subject = any> {
/**
* Upload a file from the fixtures folder to a given user storage.
* **Warning**: Using this function will reset the previous session
*/
uploadFile(user: User, fixture?: string, mimeType?: string, target?: string): Cypress.Chainable<void>,
/**
* 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): Cypress.Chainable<void>,
/**
* Run an occ command in the docker container.
*/
runOccCommand(command: string): Cypress.Chainable<void>,
}
}
}
const url = (Cypress.config('baseUrl') || '').replace(/\/index.php\/?$/g, '')
Cypress.env('baseUrl', url)
/**
* cy.uploadedFile - uploads a file from the fixtures folder
* TODO: standardise in @nextcloud/cypress
*
* @param {User} user the owner of the file, e.g. admin
* @param {string} fixture the fixture file name, e.g. image1.jpg
* @param {string} mimeType e.g. image/png
* @param {string} [target] the target of the file relative to the user root
*/
Cypress.Commands.add('uploadFile', (user, fixture = 'image.jpg', mimeType = 'image/jpeg', target = `/${fixture}`) => {
// get fixture
return cy.fixture(fixture, 'base64').then(async file => {
// convert the base64 string to a blob
const blob = Cypress.Blob.base64StringToBlob(file, mimeType)
cy.uploadContent(user, blob, mimeType, target)
})
})
/**
* cy.uploadedContent - uploads a raw content
* TODO: standardise in @nextcloud/cypress
*
* @param {User} user the owner of the file, e.g. admin
* @param {Blob} blob the content to upload
* @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) => {
cy.clearCookies()
.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 })
await axios({
url: `${rootPath}${filePath}`,
method: 'PUT',
data: file,
headers: {
'Content-Type': mimeType,
},
auth: {
username: user.userId,
password: user.password,
},
}).then(response => {
cy.log(`Uploaded content as ${fileName}`, response)
})
} catch (error) {
cy.log('error', error)
throw new Error(`Unable to process fixture`)
}
})
})
Cypress.Commands.add('runOccCommand', (command: string) => {
cy.exec(`docker exec --user www-data nextcloud-cypress-tests-server php ./occ ${command}`)
})

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>

View File

@ -0,0 +1,57 @@
/**
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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 { mount } from 'cypress/vue2'
// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a <reference path="./component" /> at the top of your spec.
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}
// Example use:
// cy.mount(MyComponent)
Cypress.Commands.add('mount', (component, optionsOrProps) => {
let instance = null
const oldMounted = component?.mounted || false
// Override the mounted method to expose
// the component instance to cypress
component.mounted = function() {
// eslint-disable-next-line
instance = this
if (oldMounted) {
oldMounted()
}
}
// Expose the component with cy.get('@component')
return mount(component, optionsOrProps).then(() => {
return cy.wrap(instance).as('component')
})
})

View File

@ -1,17 +0,0 @@
// ***********************************************************
// This example support/e2e.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands.js'

22
cypress/support/e2e.ts Normal file
View File

@ -0,0 +1,22 @@
/**
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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 './commands'

4
cypress/tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["./**/*.ts"],
}

1563
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -30,11 +30,11 @@
"lint:fix": "eslint --ext .js,.vue src --fix",
"stylelint": "stylelint src",
"stylelint:fix": "stylelint src --fix",
"e2e": "./cypress/start.sh; cypress run; ./cypress/stop.sh",
"e2e:gui": "./cypress/start.sh; cypress open; ./cypress/stop.sh",
"precypress:update-snapshots": "TESTING=true npm run dev",
"cypress:update-snapshots": "TESTING=true cypress run --env type=base --spec cypress/e2e/visual-regression.cy.js --config screenshotsFolder=cypress/snapshots/base",
"wait-on": "wait-on -i 500 -t 300000"
"cypress": "npm run cypress:component && npm run cypress:e2e",
"cypress:component": "cypress run --component",
"cypress:e2e": "cypress run --e2e",
"cypress:gui": "cypress open",
"precypress:update-snapshots": "TESTING=true npm run dev"
},
"dependencies": {
"@essentials/request-timeout": "^1.3.0",
@ -53,6 +53,7 @@
"@nextcloud/upload": "^1.0.0-beta.8",
"@nextcloud/vue": "^7.3.0",
"camelcase": "^7.0.0",
"cypress": "^12.5.0",
"debounce": "^1.2.1",
"he": "^1.2.0",
"jest-environment-jsdom": "^29.3.1",
@ -79,20 +80,29 @@
"@cypress/browserify-preprocessor": "^3.0.2",
"@nextcloud/babel-config": "^1.0.0",
"@nextcloud/browserslist-config": "^2.3.0",
"@nextcloud/cypress": "^1.0.0-beta.2",
"@nextcloud/eslint-config": "^8.0.0",
"@nextcloud/stylelint-config": "^2.3.0",
"@nextcloud/webpack-vue-config": "^5.4.0",
"@types/dockerode": "^3.3.14",
"@vue/test-utils": "^1.3.3",
"@vue/tsconfig": "^0.1.3",
"autoprefixer": "^10.4.13",
"babel-loader-exclude-node-modules-except": "^1.2.1",
"cypress": "^11.2.0",
"cypress-visual-regression": "^2.1.0",
"cypress-wait-until": "^1.7.2",
"dockerode": "^3.3.4",
"eslint-plugin-cypress": "^2.12.1",
"jest": "^29.4.1",
"module-replace-webpack-plugin": "0.0.12",
"postcss": "^8.4.19",
"postcss-loader": "^7.0.2",
"raw-loader": "^4.0.2",
"ts-node": "^10.9.1",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"vue-jest": "^3.0.7",
"wait-on": "^6.0.1",
"workbox-webpack-plugin": "^6.5.4"
}
}
}

22
tsconfig.json Normal file
View File

@ -0,0 +1,22 @@
{
"extends": "@vue/tsconfig/tsconfig.json",
"include": ["./src/**/*.ts"],
"compilerOptions": {
"types": ["cypress", "node", "dockerode"],
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"target": "ESNext",
"module": "esnext",
"declaration": true,
"strict": true,
"noImplicitAny": false,
"resolveJsonModule": true
},
"ts-node": {
// these options are overrides used only by ts-node
// same as our --compilerOptions flag and our TS_NODE_COMPILER_OPTIONS environment variable
"compilerOptions": {
"module": "commonjs"
}
}
}