Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>tags/v26.0.0beta1
@@ -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 |
@@ -163,3 +163,7 @@ composer.phar | |||
./.htaccess | |||
core/js/mimetypelist.js | |||
# Tests - cypress | |||
cypress/snapshots | |||
cypress/videos |
@@ -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; | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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); |
@@ -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 ''; | |||
} |
@@ -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 */ |
@@ -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 . "')"; | |||
} | |||
} |
@@ -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'; | |||
} |
@@ -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%; |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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') |
@@ -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') |
@@ -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 = [ |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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=')) | |||
}) | |||
}) |
@@ -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"], | |||
} | |||
} |
@@ -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" |
@@ -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" | |||
} | |||
} | |||
} |
@@ -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'), | |||
}, | |||
}, | |||
} |