Browse Source
Extract colour from custom background
Extract colour from custom background
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>pull/34696/head
No known key found for this signature in database
GPG Key ID: 60C25B8C072916CF
37 changed files with 8102 additions and 1040 deletions
-
98.github/workflows/cypress.yml
-
4.gitignore
-
5apps/theming/css/default.css
-
148apps/theming/css/settings-admin.css
-
168apps/theming/css/settings-admin.scss
-
15apps/theming/lib/Controller/UserThemeController.php
-
2apps/theming/lib/ImageManager.php
-
11apps/theming/lib/Service/BackgroundService.php
-
5apps/theming/lib/Themes/CommonThemeTrait.php
-
2apps/theming/lib/ThemingDefaults.php
-
11apps/theming/src/AdminTheming.vue
-
114apps/theming/src/components/BackgroundSettings.vue
-
2apps/theming/tests/Controller/ThemingControllerTest.php
-
7apps/theming/tests/Themes/DefaultThemeTest.php
-
28apps/theming/tests/ThemingDefaultsTest.php
-
5core/css/apps.css
-
2core/css/apps.css.map
-
8core/css/apps.scss
-
10core/css/guest.css
-
5core/css/server.css
-
2core/css/server.css.map
-
85cypress.config.ts
-
243cypress/dockerNode.ts
-
37cypress/e2e/files.cy.ts
-
164cypress/e2e/theming/user-background.cy.ts
-
BINcypress/fixtures/image.jpg
-
86cypress/support/commands.ts
-
22cypress/support/e2e.ts
-
7cypress/tsconfig.json
-
4dist/theming-admin-theming.js
-
2dist/theming-admin-theming.js.map
-
4dist/theming-personal-theming.js
-
2dist/theming-personal-theming.js.map
-
7789package-lock.json
-
20package.json
-
22tsconfig.json
-
3webpack.common.js
@ -0,0 +1,98 @@ |
|||
name: Cypress |
|||
|
|||
on: |
|||
pull_request: |
|||
push: |
|||
branches: |
|||
- master |
|||
- stable* |
|||
|
|||
env: |
|||
APP_NAME: viewer |
|||
BRANCH: ${{ github.base_ref }} |
|||
TESTING: true |
|||
|
|||
jobs: |
|||
init: |
|||
runs-on: ubuntu-latest |
|||
|
|||
steps: |
|||
- name: Checkout server |
|||
uses: actions/checkout@v3 |
|||
|
|||
- name: Read package.json node and npm engines version |
|||
uses: skjnldsv/read-package-engines-version-actions@v1.2 |
|||
id: versions |
|||
with: |
|||
fallbackNode: "^12" |
|||
fallbackNpm: "^6" |
|||
|
|||
- 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 }} |
|||
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}" |
|||
|
|||
- name: Install dependencies & build app |
|||
run: | |
|||
npm ci |
|||
TESTING=true npm run build --if-present |
|||
|
|||
- name: Save context |
|||
uses: actions/cache@v3 |
|||
with: |
|||
key: cypress-context-${{ github.run_id }} |
|||
path: /home/runner/work/server |
|||
|
|||
cypress: |
|||
runs-on: ubuntu-latest |
|||
needs: init |
|||
|
|||
strategy: |
|||
fail-fast: false |
|||
matrix: |
|||
# run multiple copies of the current job in parallel |
|||
containers: [1] |
|||
|
|||
name: runner ${{ matrix.containers }} |
|||
|
|||
steps: |
|||
- name: Restore context |
|||
uses: actions/cache@v3 |
|||
with: |
|||
key: cypress-context-${{ github.run_id }} |
|||
path: /home/runner/work/server |
|||
|
|||
- name: Run E2E cypress tests |
|||
uses: cypress-io/github-action@v4 |
|||
with: |
|||
record: true |
|||
parallel: true |
|||
# cypress env |
|||
ci-build-id: ${{ github.sha }}-${{ github.run_number }} |
|||
tag: ${{ github.event_name }} |
|||
env: |
|||
# Needs to be prefixed with CYPRESS_ |
|||
CYPRESS_BRANCH: ${{ env.BRANCH }} |
|||
CYPRESS_GH: true |
|||
# 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 }} |
|||
|
|||
summary: |
|||
runs-on: ubuntu-latest |
|||
needs: [init, cypress] |
|||
|
|||
if: always() |
|||
|
|||
name: cypress-summary |
|||
|
|||
steps: |
|||
- name: Summary status |
|||
run: if ${{ needs.init.result != 'success' || ( needs.cypress.result != 'success' && needs.cypress.result != 'skipped' ) }}; then exit 1; fi |
|||
@ -1,148 +0,0 @@ |
|||
#theming input { |
|||
width: 230px; |
|||
} |
|||
#theming input:focus, |
|||
#theming input:active { |
|||
padding-right: 30px; |
|||
} |
|||
#theming .fileupload { |
|||
display: none; |
|||
} |
|||
#theming div > label { |
|||
position: relative; |
|||
} |
|||
#theming .theme-undo { |
|||
position: absolute; |
|||
top: -7px; |
|||
right: 4px; |
|||
cursor: pointer; |
|||
opacity: 0.3; |
|||
padding: 7px; |
|||
vertical-align: top; |
|||
display: inline-block; |
|||
visibility: hidden; |
|||
height: 32px; |
|||
width: 32px; |
|||
} |
|||
#theming form.uploadButton { |
|||
width: 411px; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
#theming form .theme-undo, |
|||
#theming .theme-remove-bg { |
|||
cursor: pointer; |
|||
opacity: 0.3; |
|||
padding: 7px; |
|||
vertical-align: top; |
|||
display: inline-block; |
|||
float: right; |
|||
position: relative; |
|||
top: 4px; |
|||
right: 0px; |
|||
visibility: visible; |
|||
height: 32px; |
|||
width: 32px; |
|||
margin-left: auto; |
|||
} |
|||
#theming form .theme-undo:not([style*="display:"]) ~ .theme-remove-bg { |
|||
margin-left: 0; |
|||
} |
|||
#theming input[type=text]:hover + .theme-undo, |
|||
#theming input[type=text] + .theme-undo:hover, |
|||
#theming input[type=text]:focus + .theme-undo, |
|||
#theming input[type=text]:active + .theme-undo, |
|||
#theming input[type=url]:hover + .theme-undo, |
|||
#theming input[type=url] + .theme-undo:hover, |
|||
#theming input[type=url]:focus + .theme-undo, |
|||
#theming input[type=url]:active + .theme-undo { |
|||
visibility: visible; |
|||
} |
|||
#theming label span { |
|||
display: inline-block; |
|||
min-width: 175px; |
|||
max-width: 175px; |
|||
white-space: wrap; |
|||
padding: 8px 0px; |
|||
vertical-align: top; |
|||
} |
|||
#theming .icon-upload, |
|||
#theming .uploadButton .icon-loading-small { |
|||
padding: 8px 20px; |
|||
width: 20px; |
|||
margin: 2px 0px; |
|||
min-height: 32px; |
|||
display: inline-block; |
|||
} |
|||
#theming #theming_settings_status { |
|||
height: 26px; |
|||
margin: 10px; |
|||
} |
|||
#theming #theming_settings_loading { |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
margin-right: 10px; |
|||
} |
|||
#theming #theming_settings_msg { |
|||
vertical-align: middle; |
|||
border-radius: 3px; |
|||
} |
|||
#theming #theming-preview { |
|||
width: 230px; |
|||
height: 140px; |
|||
background-size: cover; |
|||
background-position: center center; |
|||
text-align: center; |
|||
margin-left: 178px; |
|||
margin-top: 10px; |
|||
margin-bottom: 20px; |
|||
cursor: pointer; |
|||
background-color: var(--color-primary-default); |
|||
background-image: var(--image-background-default, var(--image-background-plain, linear-gradient(40deg, #0082c9 0%, #30b6ff 100%))); |
|||
} |
|||
#theming #theming-preview #theming-preview-logo { |
|||
cursor: pointer; |
|||
width: 20%; |
|||
height: 20%; |
|||
margin-top: 20px; |
|||
display: inline-block; |
|||
background-position: center; |
|||
background-repeat: no-repeat; |
|||
background-size: contain; |
|||
background-image: var(--image-logo, url("../../../core/img/logo/logo.svg")); |
|||
} |
|||
#theming .theming-hints { |
|||
margin-top: 20px; |
|||
} |
|||
#theming .image-preview { |
|||
display: inline-block; |
|||
width: 80px; |
|||
height: 36px; |
|||
background-position: center; |
|||
background-repeat: no-repeat; |
|||
background-size: contain; |
|||
} |
|||
#theming #theming-preview-logoheader { |
|||
background-image: var(--image-logoheader); |
|||
} |
|||
#theming #theming-preview-favicon { |
|||
background-image: var(--image-favicon); |
|||
} |
|||
#theming #user-theming { |
|||
margin-top: 44px; |
|||
display: flex; |
|||
} |
|||
#theming #user-theming > div { |
|||
max-width: 400px; |
|||
margin-bottom: 44px; |
|||
} |
|||
|
|||
/* transition effects for theming value changes */ |
|||
#header { |
|||
transition: background-color 500ms linear; |
|||
} |
|||
#header svg, #header img { |
|||
transition: 500ms filter linear; |
|||
} |
|||
|
|||
/*# sourceMappingURL=settings-admin.css.map */ |
|||
@ -1,168 +0,0 @@ |
|||
#theming { |
|||
input { |
|||
width: 230px; |
|||
} |
|||
|
|||
input:focus, |
|||
input:active { |
|||
padding-right: 30px; |
|||
} |
|||
|
|||
.fileupload { |
|||
display: none; |
|||
} |
|||
|
|||
div > label { |
|||
position: relative; |
|||
} |
|||
|
|||
.theme-undo { |
|||
position: absolute; |
|||
top: -7px; // input padding |
|||
right: 4px; // input right margin + border |
|||
cursor: pointer; |
|||
opacity: .3; |
|||
padding: 7px; |
|||
vertical-align: top; |
|||
display: inline-block; |
|||
visibility: hidden; |
|||
height: 32px; // height of input |
|||
width: 32px; // height of input |
|||
} |
|||
form.uploadButton { |
|||
width: 411px; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
form .theme-undo, |
|||
.theme-remove-bg { |
|||
cursor: pointer; |
|||
opacity: .3; |
|||
padding: 7px; |
|||
vertical-align: top; |
|||
display: inline-block; |
|||
float: right; |
|||
position: relative; |
|||
top: 4px; |
|||
right: 0px; |
|||
visibility: visible; |
|||
height: 32px; |
|||
width: 32px; |
|||
// right align |
|||
margin-left: auto; |
|||
} |
|||
form .theme-undo:not([style*="display:"]) ~ .theme-remove-bg { |
|||
// Only align the undo button if both are shown |
|||
margin-left: 0; |
|||
} |
|||
|
|||
input[type='text']:hover + .theme-undo, |
|||
input[type='text'] + .theme-undo:hover, |
|||
input[type='text']:focus + .theme-undo, |
|||
input[type='text']:active + .theme-undo, |
|||
input[type='url']:hover + .theme-undo, |
|||
input[type='url'] + .theme-undo:hover, |
|||
input[type='url']:focus + .theme-undo, |
|||
input[type='url']:active + .theme-undo{ |
|||
visibility: visible; |
|||
} |
|||
|
|||
label span { |
|||
display: inline-block; |
|||
min-width: 175px; |
|||
max-width: 175px; |
|||
white-space: wrap; |
|||
padding: 8px 0px; |
|||
vertical-align: top; |
|||
} |
|||
|
|||
.icon-upload, |
|||
.uploadButton .icon-loading-small { |
|||
padding: 8px 20px; |
|||
width: 20px; |
|||
margin: 2px 0px; |
|||
min-height: 32px; |
|||
display: inline-block; |
|||
} |
|||
|
|||
#theming_settings_status { |
|||
height: 26px; |
|||
margin: 10px; |
|||
} |
|||
|
|||
#theming_settings_loading { |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
margin-right: 10px; |
|||
} |
|||
|
|||
#theming_settings_msg { |
|||
vertical-align: middle; |
|||
border-radius: 3px; |
|||
} |
|||
|
|||
#theming-preview { |
|||
width: 230px; |
|||
height: 140px; |
|||
background-size: cover; |
|||
background-position: center center; |
|||
text-align: center; |
|||
margin-left: 178px; |
|||
margin-top: 10px; |
|||
margin-bottom: 20px; |
|||
cursor: pointer; |
|||
background-color: var(--color-primary-default); |
|||
background-image: var(--image-background-default, var(--image-background-plain, linear-gradient(40deg, #0082c9 0%, #30b6ff 100%))); |
|||
|
|||
#theming-preview-logo { |
|||
cursor: pointer; |
|||
width: 20%; |
|||
height: 20%; |
|||
margin-top: 20px; |
|||
display: inline-block; |
|||
background-position: center; |
|||
background-repeat: no-repeat; |
|||
background-size: contain; |
|||
background-image: var(--image-logo, url('../../../core/img/logo/logo.svg')); |
|||
} |
|||
} |
|||
|
|||
.theming-hints { |
|||
margin-top: 20px; |
|||
} |
|||
|
|||
.image-preview { |
|||
display: inline-block; |
|||
width: 80px; |
|||
height: 36px; |
|||
background-position: center; |
|||
background-repeat: no-repeat; |
|||
background-size: contain; |
|||
} |
|||
|
|||
#theming-preview-logoheader { |
|||
// Only using --image-logoheader to show the custom value only |
|||
background-image: var(--image-logoheader); |
|||
} |
|||
|
|||
#theming-preview-favicon { |
|||
background-image: var(--image-favicon); |
|||
} |
|||
|
|||
#user-theming { |
|||
margin-top: 44px; |
|||
display: flex; |
|||
& > div { |
|||
max-width: 400px; |
|||
margin-bottom: 44px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* transition effects for theming value changes */ |
|||
#header { |
|||
transition: background-color 500ms linear; |
|||
svg, img { |
|||
transition: 500ms filter linear; |
|||
} |
|||
} |
|||
2
core/css/apps.css.map
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
2
core/css/server.css.map
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,85 @@ |
|||
/* eslint-disable node/no-unpublished-import */ |
|||
import { applyChangesToNextcloud, configureNextcloud, preppingNextcloud, startNextcloud, stopNextcloud, waitOnNextcloud } from './cypress/dockerNode' |
|||
import { defineConfig } from 'cypress' |
|||
|
|||
import browserify from '@cypress/browserify-preprocessor' |
|||
|
|||
export default defineConfig({ |
|||
projectId: '37xpdh', |
|||
|
|||
// 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: { |
|||
// Enable session management and disable isolation
|
|||
experimentalSessionAndOrigin: true, |
|||
testIsolation: 'off', |
|||
|
|||
// 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') })) |
|||
|
|||
// 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) |
|||
.then(applyChangesToNextcloud) |
|||
.then(() => { |
|||
return config |
|||
}) |
|||
}, |
|||
}, |
|||
}) |
|||
@ -0,0 +1,243 @@ |
|||
/** |
|||
* @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 node/no-unpublished-import */ |
|||
|
|||
import Docker from 'dockerode' |
|||
import waitOn from 'wait-on' |
|||
import tar from 'tar' |
|||
|
|||
export const docker = new Docker() |
|||
|
|||
const CONTAINER_NAME = 'nextcloud-cypress-tests-server' |
|||
const SERVER_IMAGE = 'ghcr.io/nextcloud/continuous-integration-shallow-server' |
|||
|
|||
/** |
|||
* Start the testing container |
|||
* |
|||
* @param {string} branch the branch of your current work |
|||
*/ |
|||
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) |
|||
|
|||
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}'`) |
|||
const container = await docker.createContainer({ |
|||
Image: SERVER_IMAGE, |
|||
name: CONTAINER_NAME, |
|||
HostConfig: { |
|||
Binds: [], |
|||
}, |
|||
}) |
|||
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() { |
|||
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) |
|||
|
|||
// Enable the app and give status
|
|||
await runExec(container, ['php', 'occ', 'app:enable', '--force', 'viewer'], true) |
|||
// await runExec(container, ['php', 'occ', 'app:list'], true)
|
|||
|
|||
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 const applyChangesToNextcloud = async function() { |
|||
console.log('\nApply local changes to nextcloud...') |
|||
const container = docker.getContainer(CONTAINER_NAME) |
|||
|
|||
const htmlPath = '/var/www/html' |
|||
const folderPaths = [ |
|||
'./apps', |
|||
'./core', |
|||
'./dist', |
|||
'./lib', |
|||
'./ocs', |
|||
] |
|||
|
|||
// Tar-streaming the above folder sinto the container
|
|||
const serverTar = tar.c({ 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 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 |
|||
* |
|||
* @param {Docker.Container} container the container to get the IP from |
|||
*/ |
|||
export const getContainerIP = async function( |
|||
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)) |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
/** |
|||
* @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/>. |
|||
* |
|||
*/ |
|||
describe('Login with a new user and open the files app', function() { |
|||
before(function() { |
|||
cy.createRandomUser().then((user) => { |
|||
cy.login(user) |
|||
}) |
|||
}) |
|||
|
|||
after(function() { |
|||
cy.logout() |
|||
}) |
|||
|
|||
it('See the default file welcome.txt in the files list', function() { |
|||
cy.visit('/apps/files') |
|||
cy.get('.files-fileList tr').should('contain', 'welcome.txt') |
|||
}) |
|||
}) |
|||
@ -0,0 +1,164 @@ |
|||
/** |
|||
* @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 type { User } from '@nextcloud/cypress' |
|||
|
|||
const defaultPrimary = '#006aa3' |
|||
const defaultBackground = 'kamil-porembinski-clouds.jpg' |
|||
|
|||
const validateThemingCss = function(expectedPrimary = '#0082c9', expectedBackground = 'kamil-porembinski-clouds.jpg', bright = false) { |
|||
return cy.window().then((win) => { |
|||
const primary = getComputedStyle(win.document.body).getPropertyValue('--color-primary') |
|||
const background = getComputedStyle(win.document.body).getPropertyValue('--image-background') |
|||
const invertIfBright = getComputedStyle(win.document.body).getPropertyValue('--background-image-invert-if-bright') |
|||
|
|||
// Returning boolean for cy.waitUntil usage
|
|||
return primary === expectedPrimary |
|||
&& background.includes(expectedBackground) |
|||
&& invertIfBright === (bright ? 'invert(100%)' : 'no') |
|||
}) |
|||
} |
|||
|
|||
describe('User default background settings', function() { |
|||
before(function() { |
|||
cy.createRandomUser().then((user: User) => { |
|||
cy.login(user) |
|||
}) |
|||
}) |
|||
|
|||
it('See the user background settings', function() { |
|||
cy.visit('/settings/user/theming') |
|||
cy.get('[data-user-theming-background-settings]').scrollIntoView().should('be.visible') |
|||
}) |
|||
|
|||
// Default cloud background is not rendered if admin theming background remains unchanged
|
|||
it('Default cloud background is not rendered', function() { |
|||
cy.get(`[data-user-theming-background-shipped="${defaultBackground}"]`).should('not.exist') |
|||
}) |
|||
|
|||
it('Default is selected on new users', function() { |
|||
cy.get('[data-user-theming-background-default]').should('be.visible') |
|||
cy.get('[data-user-theming-background-default]').should('have.class', 'background--active') |
|||
}) |
|||
}) |
|||
|
|||
describe('User select shipped backgrounds', function() { |
|||
before(function() { |
|||
cy.createRandomUser().then((user: User) => { |
|||
cy.login(user) |
|||
}) |
|||
}) |
|||
|
|||
it('See the user background settings', function() { |
|||
cy.visit('/settings/user/theming') |
|||
cy.get('[data-user-theming-background-settings]').scrollIntoView().should('be.visible') |
|||
}) |
|||
|
|||
it('Select a shipped background', function() { |
|||
const background = 'anatoly-mikhaltsov-butterfly-wing-scale.jpg' |
|||
cy.intercept('*/apps/theming/background/shipped').as('setBackground') |
|||
|
|||
// Select background
|
|||
cy.get(`[data-user-theming-background-shipped="${background}"]`).click() |
|||
|
|||
// Validate changed background and primary
|
|||
cy.wait('@setBackground') |
|||
cy.waitUntil(() => validateThemingCss('#a53c17', background)) |
|||
}) |
|||
|
|||
it('Select a bright shipped background', function() { |
|||
const background = 'bernie-cetonia-aurata-take-off-composition.jpg' |
|||
cy.intercept('*/apps/theming/background/shipped').as('setBackground') |
|||
|
|||
// Select background
|
|||
cy.get(`[data-user-theming-background-shipped="${background}"]`).click() |
|||
|
|||
// Validate changed background and primary
|
|||
cy.wait('@setBackground') |
|||
cy.waitUntil(() => validateThemingCss('#56633d', background, true)) |
|||
}) |
|||
|
|||
it('Remove background', function() { |
|||
cy.intercept('*/apps/theming/background/custom').as('clearBackground') |
|||
|
|||
// Clear background
|
|||
cy.get('[data-user-theming-background-clear]').click() |
|||
|
|||
// Validate clear background
|
|||
cy.wait('@clearBackground') |
|||
cy.waitUntil(() => validateThemingCss('#56633d', '')) |
|||
}) |
|||
}) |
|||
|
|||
describe('User select a custom color', function() { |
|||
before(function() { |
|||
cy.createRandomUser().then((user: User) => { |
|||
cy.login(user) |
|||
}) |
|||
}) |
|||
|
|||
it('See the user background settings', function() { |
|||
cy.visit('/settings/user/theming') |
|||
cy.get('[data-user-theming-background-settings]').scrollIntoView().should('be.visible') |
|||
}) |
|||
|
|||
it('Select a custom color', function() { |
|||
cy.intercept('*/apps/theming/background/color').as('setColor') |
|||
|
|||
cy.get('[data-user-theming-background-color]').click() |
|||
cy.get('.color-picker__simple-color-circle:eq(3)').click() |
|||
|
|||
// Validate clear background
|
|||
cy.wait('@setColor') |
|||
cy.waitUntil(() => cy.window().then((win) => { |
|||
const primary = getComputedStyle(win.document.body).getPropertyValue('--color-primary') |
|||
return primary !== defaultPrimary |
|||
})) |
|||
}) |
|||
}) |
|||
|
|||
describe('User select a custom background', function() { |
|||
const image = 'image.jpg' |
|||
before(function() { |
|||
cy.createRandomUser().then((user: User) => { |
|||
cy.uploadFile(user, image, 'image/jpeg') |
|||
cy.login(user) |
|||
}) |
|||
}) |
|||
|
|||
it('See the user background settings', function() { |
|||
cy.visit('/settings/user/theming') |
|||
cy.get('[data-user-theming-background-settings]').scrollIntoView().should('be.visible') |
|||
}) |
|||
|
|||
it('Select a custom background', function() { |
|||
cy.intercept('*/apps/theming/background/custom').as('setBackground') |
|||
|
|||
// Pick background
|
|||
cy.get('[data-user-theming-background-custom]').click() |
|||
cy.get(`#picker-filestable tr[data-entryname="${image}"]`).click() |
|||
cy.get('#oc-dialog-filepicker-content ~ .oc-dialog-buttonrow button.primary').click() |
|||
|
|||
// Wait for background to be set
|
|||
cy.wait('@setBackground') |
|||
cy.waitUntil(() => validateThemingCss('#4c0c04', 'apps/theming/background?v=')) |
|||
}) |
|||
}) |
|||
|
After Width: 3000 | Height: 1688 | Size: 1.5 MiB |
@ -0,0 +1,86 @@ |
|||
/** |
|||
* @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 node/no-unpublished-import */ |
|||
import axios from '@nextcloud/axios' |
|||
import { addCommands, type 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> { |
|||
uploadFile(user: User, fixture: string, mimeType: string, target ?: 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, mimeType, target = `/${fixture}`) => { |
|||
cy.clearCookies() |
|||
const fileName = basename(target) |
|||
|
|||
// get fixture
|
|||
return cy.fixture(fixture, 'base64').then(async file => { |
|||
// convert the base64 string to a blob
|
|||
const blob = Cypress.Blob.base64StringToBlob(file, mimeType) |
|||
|
|||
// 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 ${fixture} as ${fileName}`, response) |
|||
}) |
|||
} catch (error) { |
|||
cy.log('error', error) |
|||
throw new Error(`Unable to process fixture ${fixture}`) |
|||
} |
|||
}) |
|||
}) |
|||
@ -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' |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"extends": "../tsconfig.json", |
|||
"include": ["./**/*.ts"], |
|||
"compilerOptions": { |
|||
"types": ["cypress", "dockerode", "cypress-wait-until"], |
|||
} |
|||
} |
|||
4
dist/theming-admin-theming.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
2
dist/theming-admin-theming.js.map
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
4
dist/theming-personal-theming.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
2
dist/theming-personal-theming.js.map
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
7789
package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,22 @@ |
|||
{ |
|||
"extends": "@vue/tsconfig/tsconfig.json", |
|||
"include": ["./**/*.ts"], |
|||
"compilerOptions": { |
|||
"types": ["node"], |
|||
"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" |
|||
} |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue