Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>pull/42977/head
@@ -190,9 +190,9 @@ export default { | |||
.draggable__number { | |||
border-radius: 20px; | |||
border: 2px solid var(--color-primary-default); | |||
color: var(--color-primary-default); | |||
padding: 0px 7px; | |||
border: 2px solid var(--color-primary-element); | |||
color: var(--color-primary-element); | |||
padding: 0px 7px; | |||
margin-right: 3px; | |||
} | |||
@@ -71,7 +71,6 @@ | |||
--primary-invert-if-bright: no; | |||
--primary-invert-if-dark: invert(100%); | |||
--color-primary: #00679e; | |||
--color-primary-default: #0082c9; | |||
--color-primary-text: #ffffff; | |||
--color-primary-hover: #3285b1; | |||
--color-primary-light: #e5eff5; | |||
@@ -87,4 +86,5 @@ | |||
--gradient-primary-background: linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%); | |||
--color-background-plain: #00679e; | |||
--color-background-plain-text: #ffffff; | |||
--image-background: url('/apps/theming/img/background/kamil-porembinski-clouds.jpg'); | |||
} |
@@ -43,7 +43,7 @@ use OCP\Lock\LockedException; | |||
use OCP\PreConditionNotMetException; | |||
class BackgroundService { | |||
public const DEFAULT_COLOR = '#0082c9'; | |||
public const DEFAULT_COLOR = '#00679e'; | |||
public const DEFAULT_BACKGROUND_COLOR = '#00679e'; | |||
/** | |||
@@ -300,9 +300,10 @@ class BackgroundService { | |||
$meanColor = $this->calculateMeanColor($image); | |||
if ($meanColor !== false) { | |||
$this->config->setAppValue(Application::APP_ID, 'background_color', $meanColor); | |||
return $meanColor; | |||
} | |||
return $meanColor; | |||
} | |||
return null; | |||
} | |||
/** |
@@ -61,7 +61,6 @@ trait CommonThemeTrait { | |||
'--primary-invert-if-dark' => $this->util->invertTextColor($colorPrimaryElement) ? 'no' : 'invert(100%)', | |||
'--color-primary' => $this->primaryColor, | |||
'--color-primary-default' => $this->defaultPrimaryColor, | |||
'--color-primary-text' => $this->util->invertTextColor($this->primaryColor) ? '#000000' : '#ffffff', | |||
'--color-primary-hover' => $this->util->mix($this->primaryColor, $colorMainBackground, 60), | |||
'--color-primary-light' => $colorPrimaryLight, | |||
@@ -105,7 +104,7 @@ trait CommonThemeTrait { | |||
if ($this->imageManager->hasImage($image)) { | |||
$imageUrl = $this->imageManager->getImageUrl($image); | |||
$variables["--image-$image"] = "url('" . $imageUrl . "')"; | |||
} else if ($image === 'background') { | |||
} elseif ($image === 'background') { | |||
// Apply default background if nothing is configured | |||
$variables['--image-background'] = "url('" . $this->themingDefaults->getBackground() . "')"; | |||
} |
@@ -93,7 +93,7 @@ class Util { | |||
$contrast = $this->colorContrast($color, $blurredBackground); | |||
// Min. element contrast is 3:1 but we need to keep hover states in mind -> min 3.2:1 | |||
$minContrast = $highContrast ? 5.5 : 3.2; | |||
$minContrast = $highContrast ? 5.6 : 3.2; | |||
while ($contrast < $minContrast && $iteration++ < 100) { | |||
$hsl = Color::hexToHsl($color); |
@@ -63,6 +63,7 @@ | |||
"color-element-dark", | |||
"logo", | |||
"background", | |||
"background-text", | |||
"background-plain", | |||
"background-default", | |||
"logoheader", | |||
@@ -99,6 +100,9 @@ | |||
"background": { | |||
"type": "string" | |||
}, | |||
"background-text": { | |||
"type": "string" | |||
}, | |||
"background-plain": { | |||
"type": "boolean" | |||
}, |
@@ -30,6 +30,7 @@ namespace OCA\Theming\Tests\Settings; | |||
use OCA\Theming\AppInfo\Application; | |||
use OCA\Theming\ImageManager; | |||
use OCA\Theming\ITheme; | |||
use OCA\Theming\Service\BackgroundService; | |||
use OCA\Theming\Service\ThemesService; | |||
use OCA\Theming\Settings\Personal; | |||
use OCA\Theming\Themes\DarkHighContrastTheme; | |||
@@ -116,18 +117,23 @@ class PersonalTest extends TestCase { | |||
->with('enforce_theme', '') | |||
->willReturn($enforcedTheme); | |||
$this->config->expects($this->once()) | |||
$this->config->expects($this->any()) | |||
->method('getUserValue') | |||
->with('admin', 'core', 'apporder') | |||
->willReturn('[]'); | |||
->willReturnMap([ | |||
['admin', 'core', 'apporder', '[]', '[]'], | |||
['admin', 'theming', 'background_image', BackgroundService::BACKGROUND_DEFAULT], | |||
]); | |||
$this->appManager->expects($this->once()) | |||
->method('getDefaultAppForUser') | |||
->willReturn('forcedapp'); | |||
$this->initialStateService->expects($this->exactly(4)) | |||
$this->initialStateService->expects($this->exactly(7)) | |||
->method('provideInitialState') | |||
->withConsecutive( | |||
['shippedBackgrounds', BackgroundService::SHIPPED_BACKGROUNDS], | |||
['themingDefaults'], | |||
['userBackgroundImage'], | |||
['themes', $themesState], | |||
['enforceTheme', $enforcedTheme], | |||
['isUserThemingDisabled', false], |
@@ -455,82 +455,38 @@ class ThemingDefaultsTest extends TestCase { | |||
'with fallback default' => [ | |||
'disableTheming' => 'no', | |||
'primaryColor' => '', | |||
'backgroundColor' => '', | |||
'userBackgroundColor' => '', | |||
'userPrimaryColor' => '', | |||
'expected' => BackgroundService::DEFAULT_COLOR, | |||
], | |||
'with custom admin background' => [ | |||
'disableTheming' => 'no', | |||
'primaryColor' => '', | |||
'backgroundColor' => '#123', | |||
'userBackgroundColor' => '', | |||
'userPrimaryColor' => '', | |||
'expected' => '#123', | |||
], | |||
'with custom invalid admin background' => [ | |||
'disableTheming' => 'no', | |||
'primaryColor' => '', | |||
'backgroundColor' => 'invalid-name', | |||
'userBackgroundColor' => '', | |||
'userPrimaryColor' => '', | |||
'expected' => BackgroundService::DEFAULT_COLOR, | |||
], | |||
'with custom admin primary' => [ | |||
'disableTheming' => 'no', | |||
'primaryColor' => '#aaa', | |||
'backgroundColor' => '', | |||
'userBackgroundColor' => '', | |||
'userPrimaryColor' => '', | |||
'expected' => '#aaa', | |||
], | |||
'with custom invalid admin primary' => [ | |||
'disableTheming' => 'no', | |||
'primaryColor' => 'invalid', | |||
'backgroundColor' => '', | |||
'userBackgroundColor' => '', | |||
'userPrimaryColor' => '', | |||
'expected' => BackgroundService::DEFAULT_COLOR, | |||
], | |||
'with custom user background' => [ | |||
'disableTheming' => 'no', | |||
'primaryColor' => '', | |||
'backgroundColor' => '', | |||
'userBackgroundColor' => '#456', | |||
'userPrimaryColor' => '#456', | |||
'expected' => '#456', | |||
], | |||
'with custom invalid user primary' => [ | |||
'disableTheming' => 'no', | |||
'primaryColor' => '', | |||
'backgroundColor' => '', | |||
'userBackgroundColor' => '', | |||
'userPrimaryColor' => 'invalid-name', | |||
'expected' => BackgroundService::DEFAULT_COLOR, | |||
], | |||
'with custom user primary' => [ | |||
'disableTheming' => 'no', | |||
'primaryColor' => '', | |||
'backgroundColor' => '', | |||
'userBackgroundColor' => '', | |||
'userPrimaryColor' => '#bbb', | |||
'expected' => '#bbb', | |||
], | |||
'with custom invalid user background' => [ | |||
'disableTheming' => 'no', | |||
'primaryColor' => '', | |||
'backgroundColor' => '', | |||
'userBackgroundColor' => 'invalid-name', | |||
'userPrimaryColor' => '', | |||
'expected' => BackgroundService::DEFAULT_COLOR, | |||
], | |||
'with custom admin and user background' => [ | |||
'disableTheming' => 'no', | |||
'primaryColor' => '', | |||
'backgroundColor' => '#123', | |||
'userBackgroundColor' => '#456', | |||
'userPrimaryColor' => '#456', | |||
'expected' => '#456', | |||
'with disabled user theming primary' => [ | |||
'disableTheming' => 'yes', | |||
'primaryColor' => '#aaa', | |||
'userPrimaryColor' => '#bbb', | |||
'expected' => '#aaa', | |||
], | |||
]; | |||
} | |||
@@ -538,7 +494,7 @@ class ThemingDefaultsTest extends TestCase { | |||
/** | |||
* @dataProvider dataGetColorPrimary | |||
*/ | |||
public function testGetColorPrimary(string $disableTheming, string $primaryColor, string $backgroundColor, string $userBackgroundColor, $userPrimaryColor, $expected) { | |||
public function testGetColorPrimary(string $disableTheming, string $primaryColor, string $userPrimaryColor, string $expected) { | |||
$user = $this->createMock(IUser::class); | |||
$this->userSession->expects($this->any()) | |||
->method('getUser') | |||
@@ -552,15 +508,12 @@ class ThemingDefaultsTest extends TestCase { | |||
->willReturnMap([ | |||
['theming', 'disable-user-theming', 'no', $disableTheming], | |||
['theming', 'primary_color', '', $primaryColor], | |||
['theming', 'background_color', '', $backgroundColor], | |||
]); | |||
$this->config | |||
->expects($this->any()) | |||
->method('getUserValue') | |||
->willReturnMap([ | |||
['user', 'theming', 'background_color', '', $userBackgroundColor], | |||
['user', 'theming', 'primary_color', $userBackgroundColor, $userPrimaryColor], | |||
]); | |||
->with('user', 'theming', 'primary_color', '') | |||
->willReturn($userPrimaryColor); | |||
$this->assertEquals($expected, $this->template->getColorPrimary()); | |||
} | |||
@@ -668,15 +621,10 @@ class ThemingDefaultsTest extends TestCase { | |||
->method('deleteAppValue') | |||
->with('theming', 'primary_color'); | |||
$this->config | |||
->expects($this->exactly(2)) | |||
->expects($this->once()) | |||
->method('getAppValue') | |||
->withConsecutive( | |||
['theming', 'cachebuster', '0'], | |||
['theming', 'primary_color', null], | |||
)->willReturnOnConsecutiveCalls( | |||
'15', | |||
$this->defaults->getColorPrimary(), | |||
); | |||
->with('theming', 'cachebuster', '0') | |||
->willReturn('15'); | |||
$this->config | |||
->expects($this->once()) | |||
->method('setAppValue') |
@@ -21,9 +21,15 @@ | |||
*/ | |||
/* eslint-disable n/no-unpublished-import */ | |||
import { User } from '@nextcloud/cypress' | |||
import { colord } from 'colord' | |||
import { defaultPrimary, defaultBackground, pickRandomColor, validateBodyThemingCss, validateUserThemingDefaultCss } from './themingUtils' | |||
import { | |||
defaultPrimary, | |||
defaultBackground, | |||
pickRandomColor, | |||
validateBodyThemingCss, | |||
validateUserThemingDefaultCss, | |||
expectBackgroundColor, | |||
} from './themingUtils' | |||
const admin = new User('admin', 'admin') | |||
@@ -36,15 +42,24 @@ describe('Admin theming settings visibility check', function() { | |||
it('See the admin theming section', function() { | |||
cy.visit('/settings/admin/theming') | |||
cy.get('[data-admin-theming-settings]').should('exist').scrollIntoView() | |||
cy.get('[data-admin-theming-settings]') | |||
.should('exist') | |||
.scrollIntoView() | |||
cy.get('[data-admin-theming-settings]').should('be.visible') | |||
}) | |||
it('See the default settings', function() { | |||
cy.get('[data-admin-theming-setting-primary-color-picker]').should('exist') | |||
cy.get('[data-admin-theming-setting-primary-color-reset]').should('not.exist') | |||
cy.get('[data-admin-theming-setting-color-picker]').should('exist') | |||
cy.get('[data-admin-theming-setting-file-reset]').should('not.exist') | |||
cy.get('[data-admin-theming-setting-file-remove]').should('be.visible') | |||
cy.get('[data-admin-theming-setting-file-remove]').should('exist') | |||
cy.get( | |||
'[data-admin-theming-setting-primary-color] [data-admin-theming-setting-color]', | |||
).then(($el) => expectBackgroundColor($el, defaultPrimary)) | |||
cy.get( | |||
'[data-admin-theming-setting-background-color] [data-admin-theming-setting-color]', | |||
).then(($el) => expectBackgroundColor($el, defaultPrimary)) | |||
}) | |||
}) | |||
@@ -59,24 +74,42 @@ describe('Change the primary color and reset it', function() { | |||
it('See the admin theming section', function() { | |||
cy.visit('/settings/admin/theming') | |||
cy.get('[data-admin-theming-settings]').should('exist').scrollIntoView() | |||
cy.get('[data-admin-theming-settings]') | |||
.should('exist') | |||
.scrollIntoView() | |||
cy.get('[data-admin-theming-settings]').should('be.visible') | |||
}) | |||
it('Change the primary color', function() { | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor') | |||
pickRandomColor().then(color => { selectedColor = color }) | |||
pickRandomColor('[data-admin-theming-setting-primary-color]').then( | |||
(color) => { | |||
selectedColor = color | |||
}, | |||
) | |||
cy.wait('@setColor') | |||
cy.waitUntil(() => validateBodyThemingCss(selectedColor, defaultBackground)) | |||
cy.waitUntil(() => | |||
validateBodyThemingCss( | |||
selectedColor, | |||
defaultBackground, | |||
defaultPrimary, | |||
), | |||
) | |||
}) | |||
it('Screenshot the login page and validate login page', function() { | |||
cy.logout() | |||
cy.visit('/') | |||
cy.waitUntil(() => validateBodyThemingCss(selectedColor, defaultBackground)) | |||
cy.waitUntil(() => | |||
validateBodyThemingCss( | |||
selectedColor, | |||
defaultBackground, | |||
defaultPrimary, | |||
), | |||
) | |||
cy.screenshot() | |||
}) | |||
@@ -98,21 +131,29 @@ describe('Remove the default background and restore it', function() { | |||
it('See the admin theming section', function() { | |||
cy.visit('/settings/admin/theming') | |||
cy.get('[data-admin-theming-settings]').should('exist').scrollIntoView() | |||
cy.get('[data-admin-theming-settings]') | |||
.should('exist') | |||
.scrollIntoView() | |||
cy.get('[data-admin-theming-settings]').should('be.visible') | |||
}) | |||
it('Remove the default background', function() { | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('removeBackground') | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as( | |||
'removeBackground', | |||
) | |||
cy.get('[data-admin-theming-setting-file-remove]').click() | |||
cy.wait('@removeBackground') | |||
cy.waitUntil(() => validateBodyThemingCss(defaultPrimary, null)) | |||
cy.waitUntil(() => cy.window().then((win) => { | |||
const backgroundPlain = getComputedStyle(win.document.body).getPropertyValue('--image-background-plain') | |||
return backgroundPlain !== '' | |||
})) | |||
cy.waitUntil(() => | |||
cy.window().then((win) => { | |||
const backgroundPlain = getComputedStyle( | |||
win.document.body, | |||
).getPropertyValue('--image-background') | |||
return backgroundPlain !== '' | |||
}), | |||
) | |||
}) | |||
it('Screenshot the login page and validate login page', function() { | |||
@@ -132,7 +173,7 @@ describe('Remove the default background and restore it', function() { | |||
}) | |||
}) | |||
describe('Remove the default background with a custom primary color', function() { | |||
describe('Remove the default background with a custom background color', function() { | |||
let selectedColor = '' | |||
before(function() { | |||
@@ -143,23 +184,40 @@ describe('Remove the default background with a custom primary color', function() | |||
it('See the admin theming section', function() { | |||
cy.visit('/settings/admin/theming') | |||
cy.get('[data-admin-theming-settings]').should('exist').scrollIntoView() | |||
cy.get('[data-admin-theming-settings]') | |||
.should('exist') | |||
.scrollIntoView() | |||
cy.get('[data-admin-theming-settings]').should('be.visible') | |||
}) | |||
it('Change the primary color', function() { | |||
it('Change the background color', function() { | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor') | |||
pickRandomColor().then(color => { selectedColor = color }) | |||
pickRandomColor('[data-admin-theming-setting-background-color]').then( | |||
(color) => { | |||
selectedColor = color | |||
}, | |||
) | |||
cy.wait('@setColor') | |||
cy.waitUntil(() => validateBodyThemingCss(selectedColor, defaultBackground)) | |||
cy.waitUntil(() => | |||
validateBodyThemingCss( | |||
defaultPrimary, | |||
defaultBackground, | |||
selectedColor, | |||
), | |||
) | |||
}) | |||
it('Remove the default background', function() { | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('removeBackground') | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as( | |||
'removeBackground', | |||
) | |||
cy.get('[data-admin-theming-setting-file-remove]').click() | |||
cy.get('[data-admin-theming-setting-file-remove]').scrollIntoView() | |||
cy.get('[data-admin-theming-setting-file-remove]').click({ | |||
force: true, | |||
}) | |||
cy.wait('@removeBackground') | |||
}) | |||
@@ -168,7 +226,9 @@ describe('Remove the default background with a custom primary color', function() | |||
cy.logout() | |||
cy.visit('/') | |||
cy.waitUntil(() => validateBodyThemingCss(selectedColor, null)) | |||
cy.waitUntil(() => | |||
validateBodyThemingCss(defaultPrimary, null, selectedColor), | |||
) | |||
cy.screenshot() | |||
}) | |||
@@ -182,6 +242,8 @@ describe('Remove the default background with a custom primary color', function() | |||
}) | |||
describe('Remove the default background with a bright color', function() { | |||
let selectedColor = '' | |||
before(function() { | |||
// Just in case previous test failed | |||
cy.resetAdminTheming() | |||
@@ -191,37 +253,51 @@ describe('Remove the default background with a bright color', function() { | |||
it('See the admin theming section', function() { | |||
cy.visit('/settings/admin/theming') | |||
cy.get('[data-admin-theming-settings]').should('exist').scrollIntoView() | |||
cy.get('[data-admin-theming-settings]') | |||
.should('exist') | |||
.scrollIntoView() | |||
cy.get('[data-admin-theming-settings]').should('be.visible') | |||
}) | |||
it('Remove the default background', function() { | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('removeBackground') | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as( | |||
'removeBackground', | |||
) | |||
cy.get('[data-admin-theming-setting-file-remove]').click() | |||
cy.wait('@removeBackground') | |||
}) | |||
it('Change the primary color', function() { | |||
it('Change the background color', function() { | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor') | |||
// Pick one of the bright color preset | |||
cy.get('[data-admin-theming-setting-primary-color-picker]').click() | |||
cy.get('.color-picker__simple-color-circle:eq(4)').click() | |||
pickRandomColor( | |||
'[data-admin-theming-setting-background-color]', | |||
4, | |||
).then((color) => { | |||
selectedColor = color | |||
}) | |||
cy.wait('@setColor') | |||
cy.waitUntil(() => validateBodyThemingCss('#ddcb55', null)) | |||
cy.waitUntil(() => | |||
validateBodyThemingCss(defaultPrimary, null, selectedColor), | |||
) | |||
}) | |||
it('See the header being inverted', function() { | |||
cy.waitUntil(() => cy.window().then((win) => { | |||
const firstEntry = win.document.querySelector('.app-menu-main li img') | |||
if (!firstEntry) { | |||
return false | |||
} | |||
return getComputedStyle(firstEntry).filter === 'invert(1)' | |||
})) | |||
cy.waitUntil(() => | |||
cy.window().then((win) => { | |||
const firstEntry = win.document.querySelector( | |||
'.app-menu-main li img', | |||
) | |||
if (!firstEntry) { | |||
return false | |||
} | |||
return getComputedStyle(firstEntry).filter === 'invert(1)' | |||
}), | |||
) | |||
}) | |||
}) | |||
@@ -238,7 +314,9 @@ describe('Change the login fields then reset them', function() { | |||
it('See the admin theming section', function() { | |||
cy.visit('/settings/admin/theming') | |||
cy.get('[data-admin-theming-settings]').should('exist').scrollIntoView() | |||
cy.get('[data-admin-theming-settings]') | |||
.should('exist') | |||
.scrollIntoView() | |||
cy.get('[data-admin-theming-settings]').should('be.visible') | |||
}) | |||
@@ -246,42 +324,54 @@ describe('Change the login fields then reset them', function() { | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('updateFields') | |||
// Name | |||
cy.get('[data-admin-theming-setting-field="name"] input[type="text"]') | |||
.scrollIntoView() | |||
cy.get('[data-admin-theming-setting-field="name"] input[type="text"]') | |||
.type(`{selectall}${name}{enter}`) | |||
cy.get( | |||
'[data-admin-theming-setting-field="name"] input[type="text"]', | |||
).scrollIntoView() | |||
cy.get( | |||
'[data-admin-theming-setting-field="name"] input[type="text"]', | |||
).type(`{selectall}${name}{enter}`) | |||
cy.wait('@updateFields') | |||
// Url | |||
cy.get('[data-admin-theming-setting-field="url"] input[type="url"]') | |||
.scrollIntoView() | |||
cy.get('[data-admin-theming-setting-field="url"] input[type="url"]') | |||
.type(`{selectall}${url}{enter}`) | |||
cy.get( | |||
'[data-admin-theming-setting-field="url"] input[type="url"]', | |||
).scrollIntoView() | |||
cy.get( | |||
'[data-admin-theming-setting-field="url"] input[type="url"]', | |||
).type(`{selectall}${url}{enter}`) | |||
cy.wait('@updateFields') | |||
// Slogan | |||
cy.get('[data-admin-theming-setting-field="slogan"] input[type="text"]') | |||
.scrollIntoView() | |||
cy.get('[data-admin-theming-setting-field="slogan"] input[type="text"]') | |||
.type(`{selectall}${slogan}{enter}`) | |||
cy.get( | |||
'[data-admin-theming-setting-field="slogan"] input[type="text"]', | |||
).scrollIntoView() | |||
cy.get( | |||
'[data-admin-theming-setting-field="slogan"] input[type="text"]', | |||
).type(`{selectall}${slogan}{enter}`) | |||
cy.wait('@updateFields') | |||
}) | |||
it('Ensure undo button presence', function() { | |||
cy.get('[data-admin-theming-setting-field="name"] .input-field__trailing-button') | |||
.scrollIntoView() | |||
cy.get('[data-admin-theming-setting-field="name"] .input-field__trailing-button') | |||
.should('be.visible') | |||
cy.get('[data-admin-theming-setting-field="url"] .input-field__trailing-button') | |||
.scrollIntoView() | |||
cy.get('[data-admin-theming-setting-field="url"] .input-field__trailing-button') | |||
.should('be.visible') | |||
cy.get('[data-admin-theming-setting-field="slogan"] .input-field__trailing-button') | |||
.scrollIntoView() | |||
cy.get('[data-admin-theming-setting-field="slogan"] .input-field__trailing-button') | |||
.should('be.visible') | |||
cy.get( | |||
'[data-admin-theming-setting-field="name"] .input-field__trailing-button', | |||
).scrollIntoView() | |||
cy.get( | |||
'[data-admin-theming-setting-field="name"] .input-field__trailing-button', | |||
).should('be.visible') | |||
cy.get( | |||
'[data-admin-theming-setting-field="url"] .input-field__trailing-button', | |||
).scrollIntoView() | |||
cy.get( | |||
'[data-admin-theming-setting-field="url"] .input-field__trailing-button', | |||
).should('be.visible') | |||
cy.get( | |||
'[data-admin-theming-setting-field="slogan"] .input-field__trailing-button', | |||
).scrollIntoView() | |||
cy.get( | |||
'[data-admin-theming-setting-field="slogan"] .input-field__trailing-button', | |||
).should('be.visible') | |||
}) | |||
it('Validate login screen changes', function() { | |||
@@ -317,19 +407,29 @@ describe('Disable user theming and enable it back', function() { | |||
it('See the admin theming section', function() { | |||
cy.visit('/settings/admin/theming') | |||
cy.get('[data-admin-theming-settings]').should('exist').scrollIntoView() | |||
cy.get('[data-admin-theming-settings]') | |||
.should('exist') | |||
.scrollIntoView() | |||
cy.get('[data-admin-theming-settings]').should('be.visible') | |||
}) | |||
it('Disable user background theming', function() { | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('disableUserTheming') | |||
cy.get('[data-admin-theming-setting-disable-user-theming]') | |||
.scrollIntoView() | |||
cy.get('[data-admin-theming-setting-disable-user-theming]') | |||
.should('be.visible') | |||
cy.get('[data-admin-theming-setting-disable-user-theming] input[type="checkbox"]').check({ force: true }) | |||
cy.get('[data-admin-theming-setting-disable-user-theming] input[type="checkbox"]').should('be.checked') | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as( | |||
'disableUserTheming', | |||
) | |||
cy.get( | |||
'[data-admin-theming-setting-disable-user-theming]', | |||
).scrollIntoView() | |||
cy.get('[data-admin-theming-setting-disable-user-theming]').should( | |||
'be.visible', | |||
) | |||
cy.get( | |||
'[data-admin-theming-setting-disable-user-theming] input[type="checkbox"]', | |||
).check({ force: true }) | |||
cy.get( | |||
'[data-admin-theming-setting-disable-user-theming] input[type="checkbox"]', | |||
).should('be.checked') | |||
cy.wait('@disableUserTheming') | |||
}) | |||
@@ -343,8 +443,9 @@ describe('Disable user theming and enable it back', function() { | |||
it('User cannot not change background settings', function() { | |||
cy.visit('/settings/user/theming') | |||
cy.get('[data-user-theming-background-disabled]').scrollIntoView() | |||
cy.get('[data-user-theming-background-disabled]').should('be.visible') | |||
cy.contains( | |||
'Customization has been disabled by your administrator', | |||
).should('exist') | |||
}) | |||
}) | |||
@@ -363,40 +464,60 @@ describe('The user default background settings reflect the admin theming setting | |||
it('See the admin theming section', function() { | |||
cy.visit('/settings/admin/theming') | |||
cy.get('[data-admin-theming-settings]').should('exist').scrollIntoView() | |||
cy.get('[data-admin-theming-settings]') | |||
.should('exist') | |||
.scrollIntoView() | |||
cy.get('[data-admin-theming-settings]').should('be.visible') | |||
}) | |||
it('Change the primary color', function() { | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor') | |||
pickRandomColor().then(color => { selectedColor = color }) | |||
cy.wait('@setColor') | |||
cy.waitUntil(() => cy.window().then(($window) => { | |||
const primary = $window.getComputedStyle($window.document.body).getPropertyValue('--color-primary-default') | |||
return colord(primary).isEqual(selectedColor) | |||
})) | |||
}) | |||
it('Change the default background', function() { | |||
cy.intercept('*/apps/theming/ajax/uploadImage').as('setBackground') | |||
cy.fixture('image.jpg', null).as('background') | |||
cy.get('[data-admin-theming-setting-file="background"] input[type="file"]').selectFile('@background', { force: true }) | |||
cy.get( | |||
'[data-admin-theming-setting-file="background"] input[type="file"]', | |||
).selectFile('@background', { force: true }) | |||
cy.wait('@setBackground') | |||
cy.waitUntil(() => cy.window().then((win) => { | |||
const currentBackgroundDefault = getComputedStyle(win.document.body).getPropertyValue('--image-background-default') | |||
return currentBackgroundDefault.includes('/apps/theming/image/background?v=') | |||
})) | |||
cy.waitUntil(() => | |||
validateBodyThemingCss( | |||
defaultPrimary, | |||
'/apps/theming/image/background?v=', | |||
null, | |||
), | |||
) | |||
}) | |||
it('Change the background color', function() { | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor') | |||
pickRandomColor('[data-admin-theming-setting-background-color]').then( | |||
(color) => { | |||
selectedColor = color | |||
}, | |||
) | |||
cy.wait('@setColor') | |||
cy.waitUntil(() => | |||
validateBodyThemingCss( | |||
defaultPrimary, | |||
'/apps/theming/image/background?v=', | |||
selectedColor, | |||
), | |||
) | |||
}) | |||
it('Login page should match admin theming settings', function() { | |||
cy.logout() | |||
cy.visit('/') | |||
cy.waitUntil(() => validateBodyThemingCss(selectedColor, '/apps/theming/image/background?v=')) | |||
cy.waitUntil(() => | |||
validateBodyThemingCss( | |||
defaultPrimary, | |||
'/apps/theming/image/background?v=', | |||
selectedColor, | |||
), | |||
) | |||
}) | |||
it('Login as user', function() { | |||
@@ -413,9 +534,17 @@ describe('The user default background settings reflect the admin theming setting | |||
it('Default user background settings should match admin theming settings', function() { | |||
cy.get('[data-user-theming-background-default]').should('be.visible') | |||
cy.get('[data-user-theming-background-default]').should('have.class', 'background--active') | |||
cy.waitUntil(() => validateUserThemingDefaultCss(selectedColor, '/apps/theming/image/background?v=')) | |||
cy.get('[data-user-theming-background-default]').should( | |||
'have.class', | |||
'background--active', | |||
) | |||
cy.waitUntil(() => | |||
validateUserThemingDefaultCss( | |||
selectedColor, | |||
'/apps/theming/image/background?v=', | |||
), | |||
) | |||
}) | |||
}) | |||
@@ -432,12 +561,16 @@ describe('The user default background settings reflect the admin theming setting | |||
it('See the admin theming section', function() { | |||
cy.visit('/settings/admin/theming') | |||
cy.get('[data-admin-theming-settings]').should('exist').scrollIntoView() | |||
cy.get('[data-admin-theming-settings]') | |||
.should('exist') | |||
.scrollIntoView() | |||
cy.get('[data-admin-theming-settings]').should('be.visible') | |||
}) | |||
it('Remove the default background', function() { | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('removeBackground') | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as( | |||
'removeBackground', | |||
) | |||
cy.get('[data-admin-theming-setting-file-remove]').click() | |||
@@ -466,7 +599,10 @@ describe('The user default background settings reflect the admin theming setting | |||
it('Default user background settings should match admin theming settings', function() { | |||
cy.get('[data-user-theming-background-default]').should('be.visible') | |||
cy.get('[data-user-theming-background-default]').should('have.class', 'background--active') | |||
cy.get('[data-user-theming-background-default]').should( | |||
'have.class', | |||
'background--active', | |||
) | |||
cy.waitUntil(() => validateUserThemingDefaultCss(defaultPrimary, null)) | |||
}) |
@@ -21,29 +21,54 @@ | |||
*/ | |||
import { colord } from 'colord' | |||
const defaultNextcloudBlue = '#0082c9' | |||
export const defaultPrimary = '#00679e' | |||
export const defaultBackground = 'kamil-porembinski-clouds.jpg' | |||
/** | |||
* Check if a CSS variable is set to a specific color | |||
* @param variable Variable to check | |||
* @param expectedColor Color that is expected | |||
*/ | |||
export function validateCSSVariable(variable: string, expectedColor: string) { | |||
const value = window.getComputedStyle(Cypress.$('body').get(0)).getPropertyValue(variable) | |||
console.debug(`${variable}, is: ${colord(value).toHex()} expected: ${expectedColor}`) | |||
return colord(value).isEqual(expectedColor) | |||
} | |||
/** | |||
* Validate the current page body css variables | |||
* | |||
* @param {string} expectedColor the expected color | |||
* @param {string} expectedColor the expected primary color | |||
* @param {string|null} expectedBackground the expected background | |||
* @param {string|null} expectedBackgroundColor the expected background color (null to ignore) | |||
*/ | |||
export const validateBodyThemingCss = function(expectedColor = defaultPrimary, expectedBackground: string|null = defaultBackground) { | |||
export function validateBodyThemingCss(expectedColor = defaultPrimary, expectedBackground: string|null = defaultBackground, expectedBackgroundColor: string|null = defaultPrimary) { | |||
// We must use `Cypress.$` here as any assertions (get is an assertion) is not allowed in wait-until's check function, see documentation | |||
const guestBackgroundColor = Cypress.$('body').css('background-color') | |||
const guestBackgroundImage = Cypress.$('body').css('background-image') | |||
const isValidBackgroundColor = colord(guestBackgroundColor).isEqual(expectedColor) | |||
const isValidBackgroundColor = expectedBackgroundColor === null || colord(guestBackgroundColor).isEqual(expectedBackgroundColor) | |||
const isValidBackgroundImage = !expectedBackground | |||
? guestBackgroundImage === 'none' | |||
: guestBackgroundImage.includes(expectedBackground) | |||
console.debug({ guestBackgroundColor: colord(guestBackgroundColor).toHex(), guestBackgroundImage, expectedColor, expectedBackground, isValidBackgroundColor, isValidBackgroundImage }) | |||
console.debug({ | |||
isValidBackgroundColor, | |||
isValidBackgroundImage, | |||
guestBackgroundColor: colord(guestBackgroundColor).toHex(), | |||
guestBackgroundImage, | |||
}) | |||
return isValidBackgroundColor && isValidBackgroundImage && validateCSSVariable('--color-primary', expectedColor) | |||
} | |||
return isValidBackgroundColor && isValidBackgroundImage | |||
/** | |||
* Check background color of element | |||
* @param element JQuery element to check | |||
* @param color expected color | |||
*/ | |||
export function expectBackgroundColor(element: JQuery<HTMLElement>, color: string) { | |||
expect(colord(element.css('background-color')).toHex()).equal(colord(color).toHex()) | |||
} | |||
/** | |||
@@ -58,28 +83,28 @@ export const validateUserThemingDefaultCss = function(expectedColor = defaultPri | |||
return false | |||
} | |||
const defaultOptionBackground = defaultSelectButton.css('background-image') | |||
const colorPickerOptionColor = defaultSelectButton.css('background-color') | |||
const isNextcloudBlue = colord(colorPickerOptionColor).isEqual('#0082c9') | |||
const backgroundImage = defaultSelectButton.css('background-image') | |||
const backgroundColor = defaultSelectButton.css('background-color') | |||
const isValidBackgroundImage = !expectedBackground | |||
? defaultOptionBackground === 'none' | |||
: defaultOptionBackground.includes(expectedBackground) | |||
console.debug({ colorPickerOptionColor: colord(colorPickerOptionColor).toHex(), expectedColor, isValidBackgroundImage, isNextcloudBlue }) | |||
? (backgroundImage === 'none' || Cypress.$('body').css('background-image') === 'none') | |||
: backgroundImage.includes(expectedBackground) | |||
console.debug({ | |||
colorPickerOptionColor: colord(backgroundColor).toHex(), | |||
expectedColor, | |||
isValidBackgroundImage, | |||
backgroundImage, | |||
}) | |||
return isValidBackgroundImage && ( | |||
colord(colorPickerOptionColor).isEqual(expectedColor) | |||
// we replace nextcloud blue with the the default rpimary (apps/theming/lib/Themes/DefaultTheme.php line 76) | |||
|| (isNextcloudBlue && colord(expectedColor).isEqual(defaultPrimary)) | |||
) | |||
return isValidBackgroundImage && colord(backgroundColor).isEqual(expectedColor) | |||
} | |||
export const pickRandomColor = function(): Cypress.Chainable<string> { | |||
export const pickRandomColor = function(context: string, index?: number): Cypress.Chainable<string> { | |||
// Pick one of the first 8 options | |||
const randColour = Math.floor(Math.random() * 8) | |||
const randColour = index ?? Math.floor(Math.random() * 8) | |||
const colorPreviewSelector = '[data-user-theming-background-color],[data-admin-theming-setting-primary-color]' | |||
const colorPreviewSelector = `${context} [data-admin-theming-setting-color]` | |||
let oldColor = '' | |||
cy.get(colorPreviewSelector).then(($el) => { | |||
@@ -87,7 +112,8 @@ export const pickRandomColor = function(): Cypress.Chainable<string> { | |||
}) | |||
// Open picker | |||
cy.contains('button', 'Change color').click() | |||
cy.get(`${context} [data-admin-theming-setting-color-picker]`).scrollIntoView() | |||
cy.get(`${context} [data-admin-theming-setting-color-picker]`).click({ force: true }) | |||
// Click on random color | |||
cy.get('.color-picker__simple-color-circle').eq(randColour).click() |
@@ -80,7 +80,7 @@ describe('User select shipped backgrounds and remove background', function() { | |||
// Validate changed background and primary | |||
cy.wait('@setBackground') | |||
cy.waitUntil(() => validateBodyThemingCss('#a53c17', background)) | |||
cy.waitUntil(() => validateBodyThemingCss('#a53c17', background, '#652e11')) | |||
}) | |||
it('Select a bright shipped background', function() { | |||
@@ -95,21 +95,21 @@ describe('User select shipped backgrounds and remove background', function() { | |||
// Validate changed background and primary | |||
cy.wait('@setBackground') | |||
cy.waitUntil(() => validateBodyThemingCss('#869171', background)) | |||
cy.waitUntil(() => validateBodyThemingCss('#56633d', background, '#dee0d3')) | |||
}) | |||
it('Remove background', function() { | |||
cy.intercept('*/apps/theming/background/custom').as('clearBackground') | |||
cy.intercept('*/apps/theming/background/color').as('clearBackground') | |||
// Clear background | |||
cy.get('[data-user-theming-background-clear]').click() | |||
cy.get('[data-user-theming-background-color]').click() | |||
// Set the accessibility state | |||
cy.get('[data-user-theming-background-clear]').should('have.attr', 'aria-pressed', 'true') | |||
cy.get('[data-user-theming-background-color]').should('have.attr', 'aria-pressed', 'true') | |||
// Validate clear background | |||
cy.wait('@clearBackground') | |||
cy.waitUntil(() => validateBodyThemingCss('#869171', null)) | |||
cy.waitUntil(() => validateBodyThemingCss('#56633d', null, '#dee0d3')) | |||
}) | |||
}) | |||
@@ -129,14 +129,12 @@ describe('User select a custom color', function() { | |||
it('Select a custom color', function() { | |||
cy.intercept('*/apps/theming/background/color').as('setColor') | |||
pickRandomColor() | |||
cy.get('[data-user-theming-background-color]').click() | |||
cy.get('.color-picker__simple-color-circle').eq(5).click() | |||
// Validate custom colour change | |||
cy.wait('@setColor') | |||
cy.waitUntil(() => cy.window().then((win) => { | |||
const primary = getComputedStyle(win.document.body).getPropertyValue('--color-primary') | |||
return primary !== defaultPrimary && primary !== defaultPrimary | |||
})) | |||
cy.waitUntil(() => validateBodyThemingCss(defaultPrimary, null, '#a5b872')) | |||
}) | |||
}) | |||
@@ -154,10 +152,11 @@ describe('User select a bright custom color and remove background', function() { | |||
}) | |||
it('Remove background', function() { | |||
cy.intercept('*/apps/theming/background/custom').as('clearBackground') | |||
cy.intercept('*/apps/theming/background/color').as('clearBackground') | |||
// Clear background | |||
cy.get('[data-user-theming-background-clear]').click() | |||
cy.get('[data-user-theming-background-color]').click() | |||
cy.get('[data-user-theming-background-color]').click() | |||
// Validate clear background | |||
cy.wait('@clearBackground') | |||
@@ -168,7 +167,8 @@ describe('User select a bright custom color and remove background', function() { | |||
cy.intercept('*/apps/theming/background/color').as('setColor') | |||
// Pick one of the bright color preset | |||
cy.contains('button', 'Change color').click() | |||
cy.get('[data-user-theming-background-color]').scrollIntoView() | |||
cy.get('[data-user-theming-background-color]').click() | |||
cy.get('.color-picker__simple-color-circle:eq(4)').click() | |||
// Validate custom colour change | |||
@@ -194,7 +194,7 @@ describe('User select a bright custom color and remove background', function() { | |||
// Validate changed background and primary | |||
cy.wait('@setBackground') | |||
cy.waitUntil(() => validateBodyThemingCss('#a53c17', background)) | |||
cy.waitUntil(() => validateBodyThemingCss('#a53c17', background, '#652e11')) | |||
}) | |||
it('See the header NOT being inverted this time', function() { | |||
@@ -240,15 +240,13 @@ describe('User select a custom background', function() { | |||
// Wait for background to be set | |||
cy.wait('@setBackground') | |||
cy.waitUntil(() => validateBodyThemingCss('#4c0c04', 'apps/theming/background?v=')) | |||
cy.waitUntil(() => validateBodyThemingCss(defaultPrimary, 'apps/theming/background?v=', '#2f2221')) | |||
}) | |||
}) | |||
describe('User changes settings and reload the page', function() { | |||
const image = 'image.jpg' | |||
const primaryFromImage = '#4c0c04' | |||
let selectedColor = '' | |||
const colorFromImage = '#2f2221' | |||
before(function() { | |||
cy.createRandomUser().then((user: User) => { | |||
@@ -280,28 +278,39 @@ describe('User changes settings and reload the page', function() { | |||
// Wait for background to be set | |||
cy.wait('@setBackground') | |||
cy.waitUntil(() => validateBodyThemingCss(primaryFromImage, 'apps/theming/background?v=')) | |||
cy.waitUntil(() => validateBodyThemingCss(defaultPrimary, 'apps/theming/background?v=', colorFromImage)) | |||
}) | |||
it('Select a custom color', function() { | |||
cy.intercept('*/apps/theming/background/color').as('setColor') | |||
cy.contains('button', 'Change color').click() | |||
cy.get('[data-user-theming-background-color]').click() | |||
cy.get('.color-picker__simple-color-circle:eq(5)').click() | |||
cy.get('[data-user-theming-background-color]').click() | |||
// Validate clear background | |||
cy.wait('@setColor') | |||
cy.waitUntil(() => cy.window().then((win) => { | |||
selectedColor = getComputedStyle(win.document.body).getPropertyValue('--color-primary') | |||
return selectedColor !== primaryFromImage | |||
})) | |||
cy.waitUntil(() => validateBodyThemingCss(defaultPrimary, null, '#a5b872')) | |||
}) | |||
it('Select a custom primary color', function() { | |||
cy.intercept('/ocs/v2.php/apps/provisioning_api/api/v1/config/users/theming/primary_color').as('setPrimaryColor') | |||
cy.get('[data-user-theming-primary-color-trigger]').scrollIntoView() | |||
cy.get('[data-user-theming-primary-color-trigger]').click() | |||
// eslint-disable-next-line cypress/no-unnecessary-waiting | |||
cy.wait(500) | |||
cy.get('.color-picker__simple-color-circle').should('be.visible') | |||
cy.get('.color-picker__simple-color-circle:eq(2)').click() | |||
cy.get('[data-user-theming-primary-color-trigger]').click() | |||
// Validate clear background | |||
cy.wait('@setPrimaryColor') | |||
cy.waitUntil(() => validateBodyThemingCss('#c98879', null, '#a5b872')) | |||
}) | |||
it('Reload the page and validate persistent changes', function() { | |||
cy.reload() | |||
cy.waitUntil(() => validateBodyThemingCss(selectedColor, 'apps/theming/background?v=')) | |||
// validate accessibility state | |||
cy.get('[data-user-theming-background-custom]').should('have.attr', 'aria-pressed', 'true') | |||
cy.waitUntil(() => validateBodyThemingCss('#c98879', null, '#a5b872')) | |||
}) | |||
}) |
@@ -1185,14 +1185,20 @@ class Server extends ServerContainer implements IServerContainer { | |||
} | |||
if ($classExists && $c->get(\OCP\IConfig::class)->getSystemValueBool('installed', false) && $c->get(IAppManager::class)->isInstalled('theming') && $c->get(TrustedDomainHelper::class)->isTrustedDomain($c->getRequest()->getInsecureServerHost())) { | |||
$backgroundService = new BackgroundService( | |||
$c->get(IRootFolder::class), | |||
$c->getAppDataDir('theming'), | |||
$c->get(\OCP\IConfig::class), | |||
$c->get(ISession::class)->get('user_id'), | |||
); | |||
$imageManager = new ImageManager( | |||
$c->get(\OCP\IConfig::class), | |||
$c->getAppDataDir('theming'), | |||
$c->get(IURLGenerator::class), | |||
$this->get(ICacheFactory::class), | |||
$this->get(LoggerInterface::class), | |||
$this->get(ITempManager::class), | |||
$this->get(BackgroundService::class), | |||
$c->get(ICacheFactory::class), | |||
$c->get(LoggerInterface::class), | |||
$c->get(ITempManager::class), | |||
$backgroundService, | |||
); | |||
return new ThemingDefaults( | |||
$c->get(\OCP\IConfig::class), | |||
@@ -1204,7 +1210,7 @@ class Server extends ServerContainer implements IServerContainer { | |||
$imageManager, | |||
$c->get(IAppManager::class), | |||
$c->get(INavigationManager::class), | |||
$c->get(BackgroundService::class), | |||
$backgroundService, | |||
); | |||
} | |||
return new \OC_Defaults(); |
@@ -71,8 +71,8 @@ class OC_Defaults { | |||
$this->defaultFDroidClientUrl = $config->getSystemValue('customclient_fdroid', 'https://f-droid.org/packages/com.nextcloud.client/'); | |||
$this->defaultDocBaseUrl = 'https://docs.nextcloud.com'; | |||
$this->defaultDocVersion = \OC_Util::getVersion()[0]; // used to generate doc links | |||
$this->defaultColorBackground = '#0069c3'; | |||
$this->defaultColorPrimary = '#0082c9'; | |||
$this->defaultColorBackground = '#00679e'; | |||
$this->defaultColorPrimary = '#00679e'; | |||
$this->defaultTextColorPrimary = '#ffffff'; | |||
$this->defaultProductName = 'Nextcloud'; | |||