Browse Source

Extract colour from custom background

Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
tags/v26.0.0beta1
John Molakvoæ 1 year ago
parent
commit
064fa10ecf
No account linked to committer's email address

+ 98
- 0
.github/workflows/cypress.yml View File

@@ -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

+ 4
- 0
.gitignore View File

@@ -163,3 +163,7 @@ composer.phar

./.htaccess
core/js/mimetypelist.js

# Tests - cypress
cypress/snapshots
cypress/videos

+ 2
- 3
apps/theming/css/default.css View File

@@ -54,9 +54,6 @@
--background-invert-if-dark: no;
--background-invert-if-bright: invert(100%);
--background-image-invert-if-bright: no;
--image-background: url('/core/img/app-background.jpg');
--image-background-default: url('/core/img/app-background.jpg');
--color-background-plain: #0082c9;
--primary-invert-if-bright: no;
--color-primary: #006aa3;
--color-primary-default: #0082c9;
@@ -75,4 +72,6 @@
--color-primary-element-light-hover: #dbe5ea;
--color-primary-element-text-dark: #ededed;
--gradient-primary-background: linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
--image-background-default: url('/apps/theming/img/background/kamil-porembinski-clouds.jpg');
--color-background-plain: #0082c9;
}

+ 0
- 148
apps/theming/css/settings-admin.css View File

@@ -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 */

+ 0
- 168
apps/theming/css/settings-admin.scss View File

@@ -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;
}
}

+ 10
- 5
apps/theming/lib/Controller/UserThemeController.php View File

@@ -168,9 +168,15 @@ class UserThemeController extends OCSController {
/**
* @NoAdminRequired
*/
public function setBackground(string $type = BackgroundService::BACKGROUND_DEFAULT, string $value = ''): JSONResponse {
public function setBackground(string $type = BackgroundService::BACKGROUND_DEFAULT, string $value = '', string $color = null): JSONResponse {
$currentVersion = (int)$this->config->getUserValue($this->userId, Application::APP_ID, 'userCacheBuster', '0');

// Set color if provided
if ($color) {
$this->backgroundService->setColorBackground($color);
}

// Set background image if provided
try {
switch ($type) {
case BackgroundService::BACKGROUND_SHIPPED:
@@ -179,14 +185,13 @@ class UserThemeController extends OCSController {
case BackgroundService::BACKGROUND_CUSTOM:
$this->backgroundService->setFileBackground($value);
break;
case 'color':
$this->backgroundService->setColorBackground($value);
break;
case BackgroundService::BACKGROUND_DEFAULT:
$this->backgroundService->setDefaultBackground();
break;
default:
return new JSONResponse(['error' => 'Invalid type provided'], Http::STATUS_BAD_REQUEST);
if (!$color) {
return new JSONResponse(['error' => 'Invalid type provided'], Http::STATUS_BAD_REQUEST);
}
}
} catch (\InvalidArgumentException $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);

+ 1
- 1
apps/theming/lib/ImageManager.php View File

@@ -94,7 +94,7 @@ class ImageManager {
case 'favicon':
return $this->urlGenerator->imagePath('core', 'logo/logo.png') . '?v=' . $cacheBusterCounter;
case 'background':
return $this->urlGenerator->linkTo(Application::APP_ID, "img/background/" . BackgroundService::DEFAULT_BACKGROUND);
return $this->urlGenerator->linkTo(Application::APP_ID, 'img/background/' . BackgroundService::DEFAULT_BACKGROUND);
}
return '';
}

+ 6
- 5
apps/theming/lib/Service/BackgroundService.php View File

@@ -30,7 +30,7 @@ namespace OCA\Theming\Service;
use InvalidArgumentException;
use OC\User\NoUserException;
use OCA\Theming\AppInfo\Application;
use OCP\Files\AppData\IAppDataFactory;
use OCA\Theming\ThemingDefaults;
use OCP\Files\File;
use OCP\Files\IAppData;
use OCP\Files\IRootFolder;
@@ -140,13 +140,13 @@ class BackgroundService {
private IAppData $appData;
private IConfig $config;
private string $userId;
private IAppDataFactory $appDataFactory;
private ThemingDefaults $themingDefaults;

public function __construct(IRootFolder $rootFolder,
IAppData $appData,
IConfig $config,
?string $userId,
IAppDataFactory $appDataFactory) {
ThemingDefaults $themingDefaults) {
if ($userId === null) {
return;
}
@@ -155,11 +155,12 @@ class BackgroundService {
$this->config = $config;
$this->userId = $userId;
$this->appData = $appData;
$this->appDataFactory = $appDataFactory;
$this->themingDefaults = $themingDefaults;
}

public function setDefaultBackground(): void {
$this->config->deleteUserValue($this->userId, Application::APP_ID, 'background_image');
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_color', $this->themingDefaults->getDefaultColorPrimary());
}

/**
@@ -171,7 +172,7 @@ class BackgroundService {
* @throws NoUserException
*/
public function setFileBackground($path): void {
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_DEFAULT);
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_CUSTOM);
$userFolder = $this->rootFolder->getUserFolder($this->userId);

/** @var File $file */

+ 2
- 3
apps/theming/lib/Themes/CommonThemeTrait.php View File

@@ -97,7 +97,7 @@ trait CommonThemeTrait {
if ($backgroundDeleted) {
$variables['--color-background-plain'] = $this->themingDefaults->getColorPrimary();
if ($this->themingDefaults->isUserThemingDisabled() || $user === null) {
$variables['--image-background-plain'] = 'true';
$variables['--image-background-plain'] = 'yes';
}
}

@@ -108,13 +108,12 @@ trait CommonThemeTrait {
if ($image === 'background') {
// If background deleted is set, ignoring variable
if ($backgroundDeleted) {
$variables['--image-background-default'] = 'no';
continue;
}
$variables['--image-background-size'] = 'cover';
$variables['--image-background-default'] = "url('" . $imageUrl . "')";
}
// --image-background is overriden by user theming
// --image-background is overridden by user theming
$variables["--image-$image"] = "url('" . $imageUrl . "')";
}
}

+ 1
- 1
apps/theming/lib/ThemingDefaults.php View File

@@ -247,7 +247,7 @@ class ThemingDefaults extends \OC_Defaults {
* Return the default color primary
*/
public function getDefaultColorPrimary(): string {
$color = $this->config->getAppValue(Application::APP_ID, 'color');
$color = $this->config->getAppValue(Application::APP_ID, 'color', '');
if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) {
$color = '#0082c9';
}

+ 9
- 2
apps/theming/src/AdminTheming.vue View File

@@ -285,8 +285,15 @@ export default {
background-position: center;
text-align: center;
margin-top: 10px;
background-color: var(--color-primary-default);
background-image: var(--image-background-default, var(--image-background-plain, url('../../../core/img/app-background.jpg'), linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)));
/* This is basically https://github.com/nextcloud/server/blob/master/core/css/guest.css
But without the user variables. That way the admin can preview the render as guest*/
/* As guest, there is no user color color-background-plain */
background-color: var(--color-primary-default, #0082c9);
/* As guest, there is no user background (--image-background)
1. Empty background if defined
2. Else default background
3. Finally default gradient (should not happened, the background is always defined anyway) */
background-image: var(--image-background-plain, var(--image-background-default, linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)));

&-logo {
width: 20%;

+ 88
- 26
apps/theming/src/components/BackgroundSettings.vue View File

@@ -1,10 +1,10 @@
<!--
- @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
- @copyright Copyright (c) 2022 Greta Doci <gretadoci@gmail.com>
-
- @author Julius Härtl <jus@bitgrid.net>
- @author Greta Doci <gretadoci@gmail.com>
- @author Christopher Ng <chrng8@gmail.com>
- @author Greta Doci <gretadoci@gmail.com>
- @author John Molakvoæ <skjnldsv@protonmail.com>
- @author Julius Härtl <jus@bitgrid.net>
-
- @license GNU AGPL version 3 or any later version
-
@@ -24,13 +24,16 @@
-->

<template>
<div class="background-selector">
<div class="background-selector" data-user-theming-background-settings>
<!-- Custom background -->
<button class="background background__filepicker"
:class="{ 'background--active': backgroundImage === 'custom' }"
:class="{ 'icon-loading': loading === 'custom', 'background--active': backgroundImage === 'custom' }"
:data-color-bright="invertTextColor(Theming.color)"
data-user-theming-background-custom
tabindex="0"
@click="pickFile">
{{ t('theming', 'Custom background') }}
<Check :size="44" />
</button>

<!-- Default background -->
@@ -38,6 +41,7 @@
:class="{ 'icon-loading': loading === 'default', 'background--active': backgroundImage === 'default' }"
:data-color-bright="invertTextColor(Theming.defaultColor)"
:style="{ '--border-color': Theming.defaultColor }"
data-user-theming-background-default
tabindex="0"
@click="setDefault">
{{ t('theming', 'Default background') }}
@@ -50,6 +54,7 @@
:data-color="Theming.color"
:data-color-bright="invertTextColor(Theming.color)"
:style="{ backgroundColor: Theming.color, '--border-color': Theming.color}"
data-user-theming-background-color
tabindex="0">
{{ t('theming', 'Change color') }}
</button>
@@ -61,6 +66,7 @@
v-tooltip="shippedBackground.details.attribution"
:class="{ 'icon-loading': loading === shippedBackground.name, 'background--active': backgroundImage === shippedBackground.name }"
:data-color-bright="shippedBackground.details.theming === 'dark'"
:data-user-theming-background-shipped="shippedBackground.name"
:style="{ backgroundImage: 'url(' + shippedBackground.preview + ')', '--border-color': shippedBackground.details.primary_color }"
class="background background__shipped"
tabindex="0"
@@ -70,16 +76,17 @@

<!-- Remove background -->
<button class="background background__delete"
data-user-theming-background-clear
tabindex="0"
@click="removeBackground">
{{ t('theming', 'Remove background') }}
<Close :size="24" />
<Close :size="32" />
</button>
</div>
</template>

<script>
import { generateFilePath, generateUrl } from '@nextcloud/router'
import { generateFilePath, generateRemoteUrl, generateUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
import axios from '@nextcloud/axios'
import Check from 'vue-material-design-icons/Check.vue'
@@ -87,6 +94,10 @@ import Close from 'vue-material-design-icons/Close.vue'
import debounce from 'debounce'
import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker'
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
import Vibrant from 'node-vibrant'
import { Palette } from 'node-vibrant/lib/color'
import { getFilePickerBuilder } from '@nextcloud/dialogs'
import { getCurrentUser } from '@nextcloud/auth'

const backgroundColor = loadState('theming', 'backgroundColor')
const backgroundImage = loadState('theming', 'backgroundImage')
@@ -95,6 +106,12 @@ const themingDefaultBackground = loadState('theming', 'themingDefaultBackground'
const defaultShippedBackground = loadState('theming', 'defaultShippedBackground')

const prefixWithBaseUrl = (url) => generateFilePath('theming', '', 'img/background/') + url
const picker = getFilePickerBuilder(t('theming', 'Select a background from your files'))
.setMultiSelect(false)
.setModal(true)
.setType(1)
.setMimeTypeFilter(['image/png', 'image/gif', 'image/jpeg', 'image/svg+xml', 'image/svg'])
.build()

export default {
name: 'BackgroundSettings',
@@ -213,9 +230,9 @@ export default {
this.update(result.data)
},

async setFile(path) {
async setFile(path, color = null) {
this.loading = 'custom'
const result = await axios.post(generateUrl('/apps/theming/background/custom'), { value: path })
const result = await axios.post(generateUrl('/apps/theming/background/custom'), { value: path, color })
this.update(result.data)
},

@@ -228,19 +245,55 @@ export default {
async pickColor(event) {
this.loading = 'color'
const color = event?.target?.dataset?.color || this.Theming?.color || '#0082c9'
const result = await axios.post(generateUrl('/apps/theming/background/color'), { value: color })
const result = await axios.post(generateUrl('/apps/theming/background/color'), { color })
this.update(result.data)
},
debouncePickColor: debounce(function() {
this.pickColor(...arguments)
}, 200),

pickFile() {
window.OC.dialogs.filepicker(t('theming', 'Select a background from your files'), (path, type) => {
if (type === OC.dialogs.FILEPICKER_TYPE_CHOOSE) {
this.setFile(path)
}
}, false, ['image/png', 'image/gif', 'image/jpeg', 'image/svg'], true, OC.dialogs.FILEPICKER_TYPE_CHOOSE)
async pickFile() {
const path = await picker.pick()
this.loading = 'custom'

// Extract primary color from image
let response = null
let color = null
try {
const fileUrl = generateRemoteUrl('dav/files/' + getCurrentUser().uid + path)
response = await axios.get(fileUrl, { responseType: 'blob' })
const blobUrl = URL.createObjectURL(response.data)
const palette = await this.getColorPaletteFromBlob(blobUrl)

// DarkVibrant is accessible AND visually pleasing
// Vibrant is not accessible enough and others are boring
color = palette?.DarkVibrant?.hex
this.setFile(path, color)

// Log data
console.debug('Extracted colour', color, 'from custom image', path, palette)
} catch (error) {
this.setFile(path)
console.error('Unable to extract colour from custom image', { error, path, response, color })
}
},

/**
* Extract a Vibrant color palette from a blob URL
*
* @param {string} blobUrl the blob URL
* @return {Promise<Palette>}
*/
getColorPaletteFromBlob(blobUrl) {
return new Promise((resolve, reject) => {
const vibrant = new Vibrant(blobUrl)
vibrant.getPalette((error, palette) => {
if (error) {
reject(error)
}
resolve(palette)
})
})
},
},
}
@@ -263,6 +316,13 @@ export default {
background-position: center center;
background-size: cover;

&__filepicker {
&.background--active {
color: white;
background-image: var(--image-background);
}
}

&__default {
background-color: var(--color-primary-default);
background-image: var(--image-background-default);
@@ -277,6 +337,12 @@ export default {
background-color: var(--color-primary-default);
}

// Over a background image
&__default,
&__shipped {
color: white;
}

// Text and svg icon dark on bright background
&[data-color-bright] {
color: black;
@@ -294,18 +360,14 @@ export default {
margin: 4px;
}

&__default,
&__shipped {
color: white;
span {
display: none;
}
&__filepicker span,
&__default span,
&__shipped span {
display: none;
}

&--active:not(.icon-loading) {
span {
display: block;
}
&--active:not(.icon-loading) span {
display: block !important;
}
}
}

+ 1
- 1
apps/theming/tests/Controller/ThemingControllerTest.php View File

@@ -680,7 +680,7 @@ class ThemingControllerTest extends TestCase {

public function testGetLoginBackground() {
$file = $this->createMock(ISimpleFile::class);
$file->method('getName')->willReturn('app-background.jpg');
$file->method('getName')->willReturn('background.png');
$file->method('getMTime')->willReturn(42);
$this->imageManager->expects($this->once())
->method('getImage')

+ 7
- 0
apps/theming/tests/Themes/DefaultThemeTest.php View File

@@ -22,8 +22,10 @@
*/
namespace OCA\Theming\Tests\Service;

use OCA\Theming\AppInfo\Application;
use OCA\Theming\ImageManager;
use OCA\Theming\ITheme;
use OCA\Theming\Service\BackgroundService;
use OCA\Theming\Themes\DefaultTheme;
use OCA\Theming\ThemingDefaults;
use OCA\Theming\Util;
@@ -80,6 +82,11 @@ class DefaultThemeTest extends TestCase {
->method('getDefaultColorPrimary')
->willReturn('#0082c9');

$this->themingDefaults
->expects($this->any())
->method('getBackground')
->willReturn('/apps/' . Application::APP_ID . '/img/background/' . BackgroundService::DEFAULT_BACKGROUND);

$this->l10n
->expects($this->any())
->method('t')

+ 14
- 14
apps/theming/tests/ThemingDefaultsTest.php View File

@@ -473,6 +473,7 @@ class ThemingDefaultsTest extends TestCase {
public function testGetColorPrimaryWithCustomBackground() {
$backgroundIndex = 2;
$background = array_values(BackgroundService::SHIPPED_BACKGROUNDS)[$backgroundIndex];

$user = $this->createMock(IUser::class);
$this->userSession->expects($this->any())
->method('getUser')
@@ -484,14 +485,15 @@ class ThemingDefaultsTest extends TestCase {
$this->config
->expects($this->once())
->method('getUserValue')
->with('user', 'theming', 'background_image', '')
->willReturn(array_keys(BackgroundService::SHIPPED_BACKGROUNDS)[$backgroundIndex]);
->with('user', 'theming', 'background_color', '')
->willReturn($background['primary_color']);

$this->config
->expects($this->exactly(2))
->method('getAppValue')
->willReturnMap([
['theming', 'disable-user-theming', 'no', 'no'],
['theming', 'color', '', ''],
['theming', 'disable-user-theming', 'no', 'no'],
]);

$this->assertEquals($background['primary_color'], $this->template->getColorPrimary());
@@ -509,14 +511,14 @@ class ThemingDefaultsTest extends TestCase {
$this->config
->expects($this->once())
->method('getUserValue')
->with('user', 'theming', 'background_image', '')
->with('user', 'theming', 'background_color', '')
->willReturn('#fff');
$this->config
->expects($this->exactly(2))
->method('getAppValue')
->willReturnMap([
['theming', 'disable-user-theming', 'no', 'no'],
['theming', 'color', '', ''],
['theming', 'disable-user-theming', 'no', 'no'],
]);

$this->assertEquals('#fff', $this->template->getColorPrimary());
@@ -534,14 +536,14 @@ class ThemingDefaultsTest extends TestCase {
$this->config
->expects($this->once())
->method('getUserValue')
->with('user', 'theming', 'background_image', '')
->with('user', 'theming', 'background_color', '')
->willReturn('nextcloud');
$this->config
->expects($this->exactly(3))
->method('getAppValue')
->willReturnMap([
['theming', 'disable-user-theming', 'no', 'no'],
['theming', 'color', '', ''],
['theming', 'disable-user-theming', 'no', 'no'],
]);

$this->assertEquals($this->template->getDefaultColorPrimary(), $this->template->getColorPrimary());
@@ -650,16 +652,14 @@ class ThemingDefaultsTest extends TestCase {
->method('deleteAppValue')
->with('theming', 'color');
$this->config
->expects($this->exactly(3))
->expects($this->exactly(2))
->method('getAppValue')
->withConsecutive(
['theming', 'cachebuster', '0'],
['theming', 'color', null],
['theming', 'disable-user-theming', 'no'],
)->willReturnOnConsecutiveCalls(
'15',
$this->defaults->getColorPrimary(),
'no',
);
$this->config
->expects($this->once())
@@ -778,10 +778,10 @@ class ThemingDefaultsTest extends TestCase {
$this->imageManager->expects($this->exactly(4))
->method('getImageUrl')
->willReturnMap([
['logo', true, 'custom-logo?v=0'],
['logoheader', true, 'custom-logoheader?v=0'],
['favicon', true, 'custom-favicon?v=0'],
['background_image', true, 'custom-background?v=0'],
['logo', 'custom-logo?v=0'],
['logoheader', 'custom-logoheader?v=0'],
['favicon', 'custom-favicon?v=0'],
['background', 'custom-background?v=0'],
]);

$expected = [

+ 1
- 4
core/css/apps.css View File

@@ -90,14 +90,11 @@ html {
height: 100%;
position: absolute;
background-color: var(--color-background-plain, var(--color-main-background));
background-image: var(--image-background);
background-size: cover;
background-position: center;
}

body {
background-color: var(--color-background-plain, var(--color-main-background));
background-image: var(--image-background-plain, var(--image-background, var(--image-background-default)));
background-image: var(--image-background, var(--image-background-default));
background-size: cover;
background-position: center;
position: fixed;

+ 1
- 1
core/css/apps.css.map
File diff suppressed because it is too large
View File


+ 4
- 4
core/css/apps.scss View File

@@ -39,15 +39,15 @@ html {
width: 100%;
height: 100%;
position: absolute;
// color-background-plain should always be defined. It is the primary user colour
background-color: var(--color-background-plain, var(--color-main-background));
background-image: var(--image-background);
background-size: cover;
background-position: center;
}

body {
// color-background-plain should always be defined. It is the primary user colour
background-color: var(--color-background-plain, var(--color-main-background));
background-image: var(--image-background-plain, var(--image-background, var(--image-background-default)));
// color-background-plain should always be defined. It is the primary user colour
background-image: var(--image-background, var(--image-background-default));
background-size: cover;
background-position: center;
position: fixed;

+ 8
- 2
core/css/guest.css View File

@@ -23,8 +23,14 @@ body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
color: var(--color-text);
text-align: center;
background-color: var(--color-main-background-not-plain, var(--color-primary));
background-image: var(--image-background, var(--image-background-plain, linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)));
/* As guest, there is no color-background-plain */
background-color: var(--color-background-plain, var(--color-primary-default, #0082c9));
/* As guest, there is no user background (--image-background)
1. User background if logged in ('no' if removed, that way the variable is _defined_)
2. Empty background if enabled ('yes' is used, that way the variable is _defined_)
3. Else default background
4. Finally default gradient (should not happened, the background is always defined anyway) */
background-image: var(--image-background, var(--image-background-plain, var(--image-background-default, linear-gradient(40deg, #0082c9 0%, #30b6ff 100%))));
background-attachment: fixed;
min-height: 100%; /* fix sticky footer */
height: auto;

+ 1
- 4
core/css/server.css View File

@@ -2672,14 +2672,11 @@ html {
height: 100%;
position: absolute;
background-color: var(--color-background-plain, var(--color-main-background));
background-image: var(--image-background);
background-size: cover;
background-position: center;
}

body {
background-color: var(--color-background-plain, var(--color-main-background));
background-image: var(--image-background-plain, var(--image-background, var(--image-background-default)));
background-image: var(--image-background, var(--image-background-default));
background-size: cover;
background-position: center;
position: fixed;

+ 1
- 1
core/css/server.css.map
File diff suppressed because it is too large
View File


+ 85
- 0
cypress.config.ts 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
})
},
},
})

+ 243
- 0
cypress/dockerNode.ts View File

@@ -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))
}

+ 37
- 0
cypress/e2e/files.cy.ts View File

@@ -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')
})
})

+ 164
- 0
cypress/e2e/theming/user-background.cy.ts View File

@@ -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='))
})
})

BIN
cypress/fixtures/image.jpg View File


+ 86
- 0
cypress/support/commands.ts View File

@@ -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}`)
}
})
})

+ 22
- 0
cypress/support/e2e.ts 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'

+ 7
- 0
cypress/tsconfig.json View File

@@ -0,0 +1,7 @@
{
"extends": "../tsconfig.json",
"include": ["./**/*.ts"],
"compilerOptions": {
"types": ["cypress", "dockerode", "cypress-wait-until"],
}
}

+ 2
- 2
dist/theming-admin-theming.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/theming-admin-theming.js.map
File diff suppressed because it is too large
View File


+ 2
- 2
dist/theming-personal-theming.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/theming-personal-theming.js.map
File diff suppressed because it is too large
View File


+ 7150
- 639
package-lock.json
File diff suppressed because it is too large
View File


+ 19
- 1
package.json View File

@@ -18,7 +18,10 @@
"test:jsunit": "karma start tests/karma.config.js --single-run",
"sass": "sass --load-path core/css core/css/ apps/*/css",
"sass:watch": "sass --watch --load-path core/css core/css/ apps/*/css",
"sass:icons": "babel-node core/src/icons.js"
"sass:icons": "babel-node core/src/icons.js",
"cypress": "npm run cypress:e2e",
"cypress:e2e": "cypress run --e2e",
"cypress:gui": "cypress open --e2e"
},
"repository": {
"type": "git",
@@ -78,6 +81,7 @@
"moment": "^2.29.4",
"moment-timezone": "^0.5.38",
"nextcloud-vue-collections": "^0.10.0",
"node-vibrant": "^3.1.6",
"p-limit": "^4.0.0",
"p-queue": "^7.3.0",
"path": "^0.12.7",
@@ -107,18 +111,28 @@
},
"devDependencies": {
"@babel/node": "^7.20.0",
"@cypress/browserify-preprocessor": "^3.0.2",
"@nextcloud/babel-config": "^1.0.0",
"@nextcloud/cypress": "^1.0.0-beta.1",
"@nextcloud/eslint-config": "^8.0.0",
"@nextcloud/stylelint-config": "^2.1.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/user-event": "^14.4.3",
"@testing-library/vue": "^5.8.3",
"@types/dockerode": "^3.3.14",
"@typescript-eslint/eslint-plugin": "^5.44.0",
"@typescript-eslint/parser": "^5.44.0",
"@vue/test-utils": "^1.3.0",
"@vue/tsconfig": "^0.1.3",
"@vue/vue2-jest": "^29.1.1",
"babel-jest": "^29.0.3",
"babel-loader": "^8.2.5",
"babel-loader-exclude-node-modules-except": "^1.2.1",
"css-loader": "^6.7.1",
"cypress": "^11.2.0",
"cypress-wait-until": "^1.7.2",
"dockerode": "^3.3.4",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-es": "^4.1.0",
"exports-loader": "^4.0.0",
"file-loader": "^6.2.0",
@@ -143,8 +157,12 @@
"sass-loader": "^12.6.0",
"sinon": "<= 5.0.7",
"style-loader": "^3.3.1",
"ts-node": "^10.9.1",
"tslib": "^2.4.1",
"typescript": "^4.9.3",
"vue-loader": "^15.9.8",
"vue-template-compiler": "^2.7.13",
"wait-on": "^6.0.1",
"webpack": "^5.75.0",
"webpack-cli": "^4.9.2",
"webpack-merge": "^5.8.0"

+ 22
- 0
tsconfig.json 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"
}
}
}

+ 2
- 1
webpack.common.js View File

@@ -166,8 +166,9 @@ module.exports = {
extensions: ['*', '.js', '.vue'],
symlinks: true,
fallback: {
stream: require.resolve('stream-browserify'),
buffer: require.resolve('buffer'),
fs: false,
stream: require.resolve('stream-browserify'),
},
},
}

Loading…
Cancel
Save