Merge pull request #3696 from nextcloud/feature/setup-cypress-test

Setup cypress test
This commit is contained in:
Julius Härtl
2022-05-11 16:08:18 +02:00
committed by GitHub
20 changed files with 2839 additions and 138 deletions

112
.github/workflows/cypress.yml vendored Normal file
View File

@@ -0,0 +1,112 @@
name: Cypress
on:
pull_request:
push:
branches:
- master
- stable*
env:
APP_NAME: deck
CYPRESS_baseUrl: http://localhost:8081/index.php
jobs:
cypress:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [14.x]
# containers: [1, 2, 3]
php-versions: [ '7.4' ]
databases: [ 'sqlite' ]
server-versions: [ 'master' ]
steps:
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Set up npm7
run: npm i -g npm@7
- name: Checkout server
uses: actions/checkout@v2
with:
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout submodules
shell: bash
run: |
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
git submodule sync --recursive
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
- name: Checkout ${{ env.APP_NAME }}
uses: actions/checkout@v2
with:
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, zip, gd, apcu
ini-values:
apc.enable_cli=on
coverage: none
- name: Set up Nextcloud
env:
DB_PORT: 4444
PHP_CLI_SERVER_WORKERS: 10
run: |
mkdir data
php occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
php occ config:system:set memcache.local --value="\\OC\\Memcache\\APCu"
php occ config:system:set debug --value=true --type=boolean
php -f index.php
php -S 0.0.0.0:8081 &
export OC_PASS=1234561
php occ user:add --password-from-env user1
php occ user:add --password-from-env user2
php occ app:enable deck
php occ app:list
cd apps/deck
composer install
npm ci
npm run build
cd ../../
curl -v http://localhost:8081/index.php/login
- name: Cypress run
uses: cypress-io/github-action@v2
with:
record: true
parallel: false
wait-on: '${{ env.CYPRESS_baseUrl }}'
working-directory: 'apps/${{ env.APP_NAME }}'
config: defaultCommandTimeout=10000,video=false
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
npm_package_name: ${{ env.APP_NAME }}
- name: Upload test failure screenshots
uses: actions/upload-artifact@v2
if: failure()
with:
name: Upload screenshots
path: apps/${{ env.APP_NAME }}/cypress/screenshots/
retention-days: 5
- name: Upload nextcloud logs
uses: actions/upload-artifact@v2
if: failure()
with:
name: Upload nextcloud log
path: data/nextcloud.log
retention-days: 5

7
cypress.json Normal file
View File

@@ -0,0 +1,7 @@
{
"baseUrl": "http://nextcloud.local/index.php",
"projectId": "1s7wkc",
"viewportWidth": 1280,
"viewportHeight": 720,
"experimentalSessionAndOrigin": true
}

View File

@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -0,0 +1,41 @@
import { randHash } from "../utils"
const randUser = randHash()
describe('Board', function () {
const password = 'pass123'
before(function () {
cy.nextcloudCreateUser(randUser, password)
})
beforeEach(function() {
cy.login(randUser, password)
})
it('Can create a board', function () {
let board = 'Test'
cy.intercept({
method: 'POST',
url: '/index.php/apps/deck/boards',
}).as('createBoardRequest')
// Click "Add board"
cy.openLeftSidebar()
cy.get('#app-navigation-vue .app-navigation__list .app-navigation-entry')
.eq(3).find('a').first().click({force: true})
// Type the board title
cy.get('.board-create form input[type=text]')
.type(board, {force: true})
// Submit
cy.get('.board-create form input[type=submit]')
.first().click({force: true})
cy.wait('@createBoardRequest').its('response.statusCode').should('equal', 200)
cy.get('.app-navigation__list .app-navigation-entry__children .app-navigation-entry')
.contains(board).should('be.visible')
})
})

View File

@@ -0,0 +1,38 @@
import { randHash } from '../utils'
const randUser = randHash()
describe('Card', function () {
const board = 'TestBoard'
const list = 'TestList'
const password = 'pass123'
before(function () {
cy.nextcloudCreateUser(randUser, password)
cy.deckCreateBoard({ user: randUser, password }, board)
cy.deckCreateList({ user: randUser, password }, list)
})
beforeEach(function () {
cy.login(randUser, password)
})
it('Can add a card', function () {
let card = 'Card 1'
cy.openLeftSidebar()
cy.get('#app-navigation-vue .app-navigation__list .app-navigation-entry')
.eq(3).find('a.app-navigation-entry-link')
.first().click({force: true})
cy.get('.board .stack').eq(0).within(() => {
cy.get('button.action-item.action-item--single.icon-add')
.first().click()
cy.get('.stack__card-add form input#new-stack-input-main')
.type(card)
cy.get('.stack__card-add form input[type=submit]')
.first().click()
cy.get('.card').first().contains(card).should('be.visible')
})
})
})

View File

@@ -0,0 +1,32 @@
import { randHash } from '../utils'
const randUser = randHash()
describe('Deck dashboard', function() {
const password = 'pass123'
before(function () {
cy.nextcloudCreateUser(randUser, password)
})
beforeEach(function() {
cy.login(randUser, password)
})
it('Can show the right title on the dashboard', function() {
cy.get('.board-title h2')
.should('have.length', 1).first()
.should('have.text', 'Upcoming cards')
})
it('Can see the default "Personal Board" created for user by default', function () {
const defaultBoard = 'Personal'
cy.openLeftSidebar()
cy.get('.app-navigation__list .app-navigation-entry')
.eq(1)
.find('ul.app-navigation-entry__children li.app-navigation-entry')
.first()
.contains(defaultBoard)
.should('be.visible')
})
})

View File

@@ -0,0 +1,33 @@
import { randHash } from "../utils";
const randUser = randHash();
describe("Stack", function () {
const board = "TestBoard";
const password = "pass123";
const stack = "List 1";
before(function () {
cy.nextcloudCreateUser(randUser, password)
cy.deckCreateBoard({ user: randUser, password }, board)
})
beforeEach(function () {
cy.logout()
cy.login(randUser, password)
})
it("Can create a stack", function () {
cy.openLeftSidebar()
cy.get("#app-navigation-vue .app-navigation__list .app-navigation-entry")
.eq(3)
.find("a.app-navigation-entry-link")
.first()
.click({ force: true })
cy.get("#stack-add button").first().click()
cy.get("#stack-add form input#new-stack-input-main").type(stack)
cy.get("#stack-add form input[type=submit]").first().click()
cy.get(".board .stack").eq(0).contains(stack).should("be.visible")
})
});

22
cypress/plugins/index.js Normal file
View File

@@ -0,0 +1,22 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

113
cypress/support/commands.js Normal file
View File

@@ -0,0 +1,113 @@
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
const url = Cypress.config("baseUrl").replace(/\/index.php\/?$/g, "");
Cypress.env("baseUrl", url);
Cypress.Commands.add("login", (user, password, route = "/apps/deck/") => {
let session = `${user}-${Date.now()}`;
cy.session(session, function () {
cy.visit(route);
cy.get("input[name=user]").type(user);
cy.get("input[name=password]").type(password);
cy.get(".submit-wrapper input[type=submit]").click();
cy.url().should("include", route);
});
// in case the session already existed but we are on a different route...
cy.visit(route);
});
Cypress.Commands.add("logout", (route = "/") => {
cy.session("_guest", function () {});
});
Cypress.Commands.add("nextcloudCreateUser", (user, password) => {
cy.clearCookies();
cy.request({
method: "POST",
url: `${Cypress.env("baseUrl")}/ocs/v1.php/cloud/users?format=json`,
form: true,
body: {
userid: user,
password: password,
},
auth: { user: "admin", pass: "admin" },
headers: {
"OCS-ApiRequest": "true",
"Content-Type": "application/x-www-form-urlencoded",
},
}).then((response) => {
cy.log(`Created user ${user}`, response.status);
});
});
Cypress.Commands.add("nextcloudUpdateUser", (user, password, key, value) => {
cy.request({
method: "PUT",
url: `${Cypress.env("baseUrl")}/ocs/v2.php/cloud/users/${user}`,
form: true,
body: { key, value },
auth: { user, pass: password },
headers: {
"OCS-ApiRequest": "true",
"Content-Type": "application/x-www-form-urlencoded",
},
}).then((response) => {
cy.log(`Updated user ${user} ${key} to ${value}`, response.status);
});
});
Cypress.Commands.add("openLeftSidebar", () => {
cy.get(".app-navigation button.app-navigation-toggle").click();
});
Cypress.Commands.add("deckCreateBoard", ({ user, password }, title) => {
cy.login(user, password);
cy.get(".app-navigation button.app-navigation-toggle").click();
cy.get("#app-navigation-vue .app-navigation__list .app-navigation-entry")
.eq(3)
.find("a")
.first()
.click({ force: true });
cy.get(".board-create form input[type=text]").type(title, { force: true });
cy.get(".board-create form input[type=submit]")
.first()
.click({ force: true });
});
Cypress.Commands.add("deckCreateList", ({ user, password }, title) => {
cy.login(user, password);
cy.get(".app-navigation button.app-navigation-toggle").click();
cy.get("#app-navigation-vue .app-navigation__list .app-navigation-entry")
.eq(3)
.find("a.app-navigation-entry-link")
.first()
.click({ force: true });
cy.get("#stack-add button").first().click();
cy.get("#stack-add form input#new-stack-input-main").type(title);
cy.get("#stack-add form input[type=submit]").first().click();
});

20
cypress/support/index.js Normal file
View File

@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.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'
// Alternatively you can use CommonJS syntax:
// require('./commands')

1
cypress/utils/index.js Normal file
View File

@@ -0,0 +1 @@
export const randHash = () => Math.random().toString(36).replace(/[^a-z]+/g, '').slice(0, 10)

2534
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -46,8 +46,8 @@
"dompurify": "^2.3.6",
"lodash": "^4.17.21",
"markdown-it": "^13.0.1",
"markdown-it-task-lists": "^2.1.1",
"markdown-it-link-attributes": "^4.0.0",
"markdown-it-task-lists": "^2.1.1",
"moment": "^2.29.3",
"nextcloud-vue-collections": "^0.10.0",
"p-queue": "^6.6.2",
@@ -77,6 +77,7 @@
"@nextcloud/webpack-vue-config": "^5.1.0",
"@relative-ci/agent": "^3.1.3",
"@vue/test-utils": "^1.3.0",
"cypress": "^9.6.0",
"jest": "^28.1.0",
"jest-serializer-vue": "^2.0.2",
"vue-jest": "^3.0.7"

View File

@@ -163,8 +163,8 @@ export default {
<style lang="scss" scoped>
@import '../../css/animations.scss';
@import '../../css/variables.scss';
@import '../../css/animations';
@import '../../css/variables';
form {
text-align: center;

View File

@@ -276,7 +276,7 @@ export default {
@import './../../css/variables';
.stack {
width: $stack-width + $stack-spacing*3;
width: $stack-width + $stack-spacing * 3;
margin-left: math.div($stack-spacing, 2);
margin-right: math.div($stack-spacing, 2);
}

View File

@@ -216,12 +216,12 @@ export default {
.modal__card .app-sidebar {
$modal-padding: 14px;
border: 0;
min-width: calc(100% - #{$modal-padding*2});
min-width: calc(100% - #{$modal-padding * 2});
position: relative;
top: 0;
left: 0;
right: 0;
max-width: calc(100% - #{$modal-padding*2});
max-width: calc(100% - #{$modal-padding * 2});
padding: 0 14px;
max-height: 100%;
overflow: initial;

View File

@@ -263,6 +263,7 @@ export default {
overflow-x: auto;
&::v-deep {
/* stylelint-disable-next-line no-invalid-position-at-import-rule */
@import './../../css/markdown';
}

View File

@@ -232,6 +232,7 @@ export default {
}
}
/* stylelint-disable-next-line no-invalid-position-at-import-rule */
@import './../../css/labels';
.card-controls {

View File

@@ -173,7 +173,7 @@ export default {
</script>
<style lang="scss" scoped>
@import '../../css/variables.scss';
@import '../../css/variables';
.global-search {
width: 100%;

View File

@@ -54,7 +54,7 @@ export default {
</script>
<style lang="scss" scoped>
@import '../../css/variables.scss';
@import '../../css/variables';
$clickable-area: 44px;
.card--placeholder {