Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>tags/v26.0.0beta1
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 |
./.htaccess | ./.htaccess | ||||
core/js/mimetypelist.js | core/js/mimetypelist.js | ||||
# Tests - cypress | |||||
cypress/snapshots | |||||
cypress/videos |
--background-invert-if-dark: no; | --background-invert-if-dark: no; | ||||
--background-invert-if-bright: invert(100%); | --background-invert-if-bright: invert(100%); | ||||
--background-image-invert-if-bright: no; | --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; | --primary-invert-if-bright: no; | ||||
--color-primary: #006aa3; | --color-primary: #006aa3; | ||||
--color-primary-default: #0082c9; | --color-primary-default: #0082c9; | ||||
--color-primary-element-light-hover: #dbe5ea; | --color-primary-element-light-hover: #dbe5ea; | ||||
--color-primary-element-text-dark: #ededed; | --color-primary-element-text-dark: #ededed; | ||||
--gradient-primary-background: linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%); | --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; | |||||
} | } |
#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 */ |
#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; | |||||
} | |||||
} |
/** | /** | ||||
* @NoAdminRequired | * @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'); | $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 { | try { | ||||
switch ($type) { | switch ($type) { | ||||
case BackgroundService::BACKGROUND_SHIPPED: | case BackgroundService::BACKGROUND_SHIPPED: | ||||
case BackgroundService::BACKGROUND_CUSTOM: | case BackgroundService::BACKGROUND_CUSTOM: | ||||
$this->backgroundService->setFileBackground($value); | $this->backgroundService->setFileBackground($value); | ||||
break; | break; | ||||
case 'color': | |||||
$this->backgroundService->setColorBackground($value); | |||||
break; | |||||
case BackgroundService::BACKGROUND_DEFAULT: | case BackgroundService::BACKGROUND_DEFAULT: | ||||
$this->backgroundService->setDefaultBackground(); | $this->backgroundService->setDefaultBackground(); | ||||
break; | break; | ||||
default: | 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) { | } catch (\InvalidArgumentException $e) { | ||||
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); | return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); |
case 'favicon': | case 'favicon': | ||||
return $this->urlGenerator->imagePath('core', 'logo/logo.png') . '?v=' . $cacheBusterCounter; | return $this->urlGenerator->imagePath('core', 'logo/logo.png') . '?v=' . $cacheBusterCounter; | ||||
case 'background': | 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 ''; | return ''; | ||||
} | } |
use InvalidArgumentException; | use InvalidArgumentException; | ||||
use OC\User\NoUserException; | use OC\User\NoUserException; | ||||
use OCA\Theming\AppInfo\Application; | use OCA\Theming\AppInfo\Application; | ||||
use OCP\Files\AppData\IAppDataFactory; | |||||
use OCA\Theming\ThemingDefaults; | |||||
use OCP\Files\File; | use OCP\Files\File; | ||||
use OCP\Files\IAppData; | use OCP\Files\IAppData; | ||||
use OCP\Files\IRootFolder; | use OCP\Files\IRootFolder; | ||||
private IAppData $appData; | private IAppData $appData; | ||||
private IConfig $config; | private IConfig $config; | ||||
private string $userId; | private string $userId; | ||||
private IAppDataFactory $appDataFactory; | |||||
private ThemingDefaults $themingDefaults; | |||||
public function __construct(IRootFolder $rootFolder, | public function __construct(IRootFolder $rootFolder, | ||||
IAppData $appData, | IAppData $appData, | ||||
IConfig $config, | IConfig $config, | ||||
?string $userId, | ?string $userId, | ||||
IAppDataFactory $appDataFactory) { | |||||
ThemingDefaults $themingDefaults) { | |||||
if ($userId === null) { | if ($userId === null) { | ||||
return; | return; | ||||
} | } | ||||
$this->config = $config; | $this->config = $config; | ||||
$this->userId = $userId; | $this->userId = $userId; | ||||
$this->appData = $appData; | $this->appData = $appData; | ||||
$this->appDataFactory = $appDataFactory; | |||||
$this->themingDefaults = $themingDefaults; | |||||
} | } | ||||
public function setDefaultBackground(): void { | public function setDefaultBackground(): void { | ||||
$this->config->deleteUserValue($this->userId, Application::APP_ID, 'background_image'); | $this->config->deleteUserValue($this->userId, Application::APP_ID, 'background_image'); | ||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'background_color', $this->themingDefaults->getDefaultColorPrimary()); | |||||
} | } | ||||
/** | /** | ||||
* @throws NoUserException | * @throws NoUserException | ||||
*/ | */ | ||||
public function setFileBackground($path): void { | 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); | $userFolder = $this->rootFolder->getUserFolder($this->userId); | ||||
/** @var File $file */ | /** @var File $file */ |
if ($backgroundDeleted) { | if ($backgroundDeleted) { | ||||
$variables['--color-background-plain'] = $this->themingDefaults->getColorPrimary(); | $variables['--color-background-plain'] = $this->themingDefaults->getColorPrimary(); | ||||
if ($this->themingDefaults->isUserThemingDisabled() || $user === null) { | if ($this->themingDefaults->isUserThemingDisabled() || $user === null) { | ||||
$variables['--image-background-plain'] = 'true'; | |||||
$variables['--image-background-plain'] = 'yes'; | |||||
} | } | ||||
} | } | ||||
if ($image === 'background') { | if ($image === 'background') { | ||||
// If background deleted is set, ignoring variable | // If background deleted is set, ignoring variable | ||||
if ($backgroundDeleted) { | if ($backgroundDeleted) { | ||||
$variables['--image-background-default'] = 'no'; | |||||
continue; | continue; | ||||
} | } | ||||
$variables['--image-background-size'] = 'cover'; | $variables['--image-background-size'] = 'cover'; | ||||
$variables['--image-background-default'] = "url('" . $imageUrl . "')"; | $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 . "')"; | $variables["--image-$image"] = "url('" . $imageUrl . "')"; | ||||
} | } | ||||
} | } |
* Return the default color primary | * Return the default color primary | ||||
*/ | */ | ||||
public function getDefaultColorPrimary(): string { | 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)) { | if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) { | ||||
$color = '#0082c9'; | $color = '#0082c9'; | ||||
} | } |
background-position: center; | background-position: center; | ||||
text-align: center; | text-align: center; | ||||
margin-top: 10px; | 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 { | &-logo { | ||||
width: 20%; | width: 20%; |
<!-- | <!-- | ||||
- @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net> | - @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 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 | - @license GNU AGPL version 3 or any later version | ||||
- | - | ||||
--> | --> | ||||
<template> | <template> | ||||
<div class="background-selector"> | |||||
<div class="background-selector" data-user-theming-background-settings> | |||||
<!-- Custom background --> | <!-- Custom background --> | ||||
<button class="background background__filepicker" | <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" | tabindex="0" | ||||
@click="pickFile"> | @click="pickFile"> | ||||
{{ t('theming', 'Custom background') }} | {{ t('theming', 'Custom background') }} | ||||
<Check :size="44" /> | |||||
</button> | </button> | ||||
<!-- Default background --> | <!-- Default background --> | ||||
:class="{ 'icon-loading': loading === 'default', 'background--active': backgroundImage === 'default' }" | :class="{ 'icon-loading': loading === 'default', 'background--active': backgroundImage === 'default' }" | ||||
:data-color-bright="invertTextColor(Theming.defaultColor)" | :data-color-bright="invertTextColor(Theming.defaultColor)" | ||||
:style="{ '--border-color': Theming.defaultColor }" | :style="{ '--border-color': Theming.defaultColor }" | ||||
data-user-theming-background-default | |||||
tabindex="0" | tabindex="0" | ||||
@click="setDefault"> | @click="setDefault"> | ||||
{{ t('theming', 'Default background') }} | {{ t('theming', 'Default background') }} | ||||
:data-color="Theming.color" | :data-color="Theming.color" | ||||
:data-color-bright="invertTextColor(Theming.color)" | :data-color-bright="invertTextColor(Theming.color)" | ||||
:style="{ backgroundColor: Theming.color, '--border-color': Theming.color}" | :style="{ backgroundColor: Theming.color, '--border-color': Theming.color}" | ||||
data-user-theming-background-color | |||||
tabindex="0"> | tabindex="0"> | ||||
{{ t('theming', 'Change color') }} | {{ t('theming', 'Change color') }} | ||||
</button> | </button> | ||||
v-tooltip="shippedBackground.details.attribution" | v-tooltip="shippedBackground.details.attribution" | ||||
:class="{ 'icon-loading': loading === shippedBackground.name, 'background--active': backgroundImage === shippedBackground.name }" | :class="{ 'icon-loading': loading === shippedBackground.name, 'background--active': backgroundImage === shippedBackground.name }" | ||||
:data-color-bright="shippedBackground.details.theming === 'dark'" | :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 }" | :style="{ backgroundImage: 'url(' + shippedBackground.preview + ')', '--border-color': shippedBackground.details.primary_color }" | ||||
class="background background__shipped" | class="background background__shipped" | ||||
tabindex="0" | tabindex="0" | ||||
<!-- Remove background --> | <!-- Remove background --> | ||||
<button class="background background__delete" | <button class="background background__delete" | ||||
data-user-theming-background-clear | |||||
tabindex="0" | tabindex="0" | ||||
@click="removeBackground"> | @click="removeBackground"> | ||||
{{ t('theming', 'Remove background') }} | {{ t('theming', 'Remove background') }} | ||||
<Close :size="24" /> | |||||
<Close :size="32" /> | |||||
</button> | </button> | ||||
</div> | </div> | ||||
</template> | </template> | ||||
<script> | <script> | ||||
import { generateFilePath, generateUrl } from '@nextcloud/router' | |||||
import { generateFilePath, generateRemoteUrl, generateUrl } from '@nextcloud/router' | |||||
import { loadState } from '@nextcloud/initial-state' | import { loadState } from '@nextcloud/initial-state' | ||||
import axios from '@nextcloud/axios' | import axios from '@nextcloud/axios' | ||||
import Check from 'vue-material-design-icons/Check.vue' | import Check from 'vue-material-design-icons/Check.vue' | ||||
import debounce from 'debounce' | import debounce from 'debounce' | ||||
import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker' | import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker' | ||||
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip' | 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 backgroundColor = loadState('theming', 'backgroundColor') | ||||
const backgroundImage = loadState('theming', 'backgroundImage') | const backgroundImage = loadState('theming', 'backgroundImage') | ||||
const defaultShippedBackground = loadState('theming', 'defaultShippedBackground') | const defaultShippedBackground = loadState('theming', 'defaultShippedBackground') | ||||
const prefixWithBaseUrl = (url) => generateFilePath('theming', '', 'img/background/') + url | 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 { | export default { | ||||
name: 'BackgroundSettings', | name: 'BackgroundSettings', | ||||
this.update(result.data) | this.update(result.data) | ||||
}, | }, | ||||
async setFile(path) { | |||||
async setFile(path, color = null) { | |||||
this.loading = 'custom' | 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) | this.update(result.data) | ||||
}, | }, | ||||
async pickColor(event) { | async pickColor(event) { | ||||
this.loading = 'color' | this.loading = 'color' | ||||
const color = event?.target?.dataset?.color || this.Theming?.color || '#0082c9' | 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) | this.update(result.data) | ||||
}, | }, | ||||
debouncePickColor: debounce(function() { | debouncePickColor: debounce(function() { | ||||
this.pickColor(...arguments) | this.pickColor(...arguments) | ||||
}, 200), | }, 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) | |||||
}) | |||||
}) | |||||
}, | }, | ||||
}, | }, | ||||
} | } | ||||
background-position: center center; | background-position: center center; | ||||
background-size: cover; | background-size: cover; | ||||
&__filepicker { | |||||
&.background--active { | |||||
color: white; | |||||
background-image: var(--image-background); | |||||
} | |||||
} | |||||
&__default { | &__default { | ||||
background-color: var(--color-primary-default); | background-color: var(--color-primary-default); | ||||
background-image: var(--image-background-default); | background-image: var(--image-background-default); | ||||
background-color: var(--color-primary-default); | background-color: var(--color-primary-default); | ||||
} | } | ||||
// Over a background image | |||||
&__default, | |||||
&__shipped { | |||||
color: white; | |||||
} | |||||
// Text and svg icon dark on bright background | // Text and svg icon dark on bright background | ||||
&[data-color-bright] { | &[data-color-bright] { | ||||
color: black; | color: black; | ||||
margin: 4px; | 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; | |||||
} | } | ||||
} | } | ||||
} | } |
public function testGetLoginBackground() { | public function testGetLoginBackground() { | ||||
$file = $this->createMock(ISimpleFile::class); | $file = $this->createMock(ISimpleFile::class); | ||||
$file->method('getName')->willReturn('app-background.jpg'); | |||||
$file->method('getName')->willReturn('background.png'); | |||||
$file->method('getMTime')->willReturn(42); | $file->method('getMTime')->willReturn(42); | ||||
$this->imageManager->expects($this->once()) | $this->imageManager->expects($this->once()) | ||||
->method('getImage') | ->method('getImage') |
*/ | */ | ||||
namespace OCA\Theming\Tests\Service; | namespace OCA\Theming\Tests\Service; | ||||
use OCA\Theming\AppInfo\Application; | |||||
use OCA\Theming\ImageManager; | use OCA\Theming\ImageManager; | ||||
use OCA\Theming\ITheme; | use OCA\Theming\ITheme; | ||||
use OCA\Theming\Service\BackgroundService; | |||||
use OCA\Theming\Themes\DefaultTheme; | use OCA\Theming\Themes\DefaultTheme; | ||||
use OCA\Theming\ThemingDefaults; | use OCA\Theming\ThemingDefaults; | ||||
use OCA\Theming\Util; | use OCA\Theming\Util; | ||||
->method('getDefaultColorPrimary') | ->method('getDefaultColorPrimary') | ||||
->willReturn('#0082c9'); | ->willReturn('#0082c9'); | ||||
$this->themingDefaults | |||||
->expects($this->any()) | |||||
->method('getBackground') | |||||
->willReturn('/apps/' . Application::APP_ID . '/img/background/' . BackgroundService::DEFAULT_BACKGROUND); | |||||
$this->l10n | $this->l10n | ||||
->expects($this->any()) | ->expects($this->any()) | ||||
->method('t') | ->method('t') |
public function testGetColorPrimaryWithCustomBackground() { | public function testGetColorPrimaryWithCustomBackground() { | ||||
$backgroundIndex = 2; | $backgroundIndex = 2; | ||||
$background = array_values(BackgroundService::SHIPPED_BACKGROUNDS)[$backgroundIndex]; | $background = array_values(BackgroundService::SHIPPED_BACKGROUNDS)[$backgroundIndex]; | ||||
$user = $this->createMock(IUser::class); | $user = $this->createMock(IUser::class); | ||||
$this->userSession->expects($this->any()) | $this->userSession->expects($this->any()) | ||||
->method('getUser') | ->method('getUser') | ||||
$this->config | $this->config | ||||
->expects($this->once()) | ->expects($this->once()) | ||||
->method('getUserValue') | ->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 | $this->config | ||||
->expects($this->exactly(2)) | ->expects($this->exactly(2)) | ||||
->method('getAppValue') | ->method('getAppValue') | ||||
->willReturnMap([ | ->willReturnMap([ | ||||
['theming', 'disable-user-theming', 'no', 'no'], | |||||
['theming', 'color', '', ''], | ['theming', 'color', '', ''], | ||||
['theming', 'disable-user-theming', 'no', 'no'], | |||||
]); | ]); | ||||
$this->assertEquals($background['primary_color'], $this->template->getColorPrimary()); | $this->assertEquals($background['primary_color'], $this->template->getColorPrimary()); | ||||
$this->config | $this->config | ||||
->expects($this->once()) | ->expects($this->once()) | ||||
->method('getUserValue') | ->method('getUserValue') | ||||
->with('user', 'theming', 'background_image', '') | |||||
->with('user', 'theming', 'background_color', '') | |||||
->willReturn('#fff'); | ->willReturn('#fff'); | ||||
$this->config | $this->config | ||||
->expects($this->exactly(2)) | ->expects($this->exactly(2)) | ||||
->method('getAppValue') | ->method('getAppValue') | ||||
->willReturnMap([ | ->willReturnMap([ | ||||
['theming', 'disable-user-theming', 'no', 'no'], | |||||
['theming', 'color', '', ''], | ['theming', 'color', '', ''], | ||||
['theming', 'disable-user-theming', 'no', 'no'], | |||||
]); | ]); | ||||
$this->assertEquals('#fff', $this->template->getColorPrimary()); | $this->assertEquals('#fff', $this->template->getColorPrimary()); | ||||
$this->config | $this->config | ||||
->expects($this->once()) | ->expects($this->once()) | ||||
->method('getUserValue') | ->method('getUserValue') | ||||
->with('user', 'theming', 'background_image', '') | |||||
->with('user', 'theming', 'background_color', '') | |||||
->willReturn('nextcloud'); | ->willReturn('nextcloud'); | ||||
$this->config | $this->config | ||||
->expects($this->exactly(3)) | ->expects($this->exactly(3)) | ||||
->method('getAppValue') | ->method('getAppValue') | ||||
->willReturnMap([ | ->willReturnMap([ | ||||
['theming', 'disable-user-theming', 'no', 'no'], | |||||
['theming', 'color', '', ''], | ['theming', 'color', '', ''], | ||||
['theming', 'disable-user-theming', 'no', 'no'], | |||||
]); | ]); | ||||
$this->assertEquals($this->template->getDefaultColorPrimary(), $this->template->getColorPrimary()); | $this->assertEquals($this->template->getDefaultColorPrimary(), $this->template->getColorPrimary()); | ||||
->method('deleteAppValue') | ->method('deleteAppValue') | ||||
->with('theming', 'color'); | ->with('theming', 'color'); | ||||
$this->config | $this->config | ||||
->expects($this->exactly(3)) | |||||
->expects($this->exactly(2)) | |||||
->method('getAppValue') | ->method('getAppValue') | ||||
->withConsecutive( | ->withConsecutive( | ||||
['theming', 'cachebuster', '0'], | ['theming', 'cachebuster', '0'], | ||||
['theming', 'color', null], | ['theming', 'color', null], | ||||
['theming', 'disable-user-theming', 'no'], | |||||
)->willReturnOnConsecutiveCalls( | )->willReturnOnConsecutiveCalls( | ||||
'15', | '15', | ||||
$this->defaults->getColorPrimary(), | $this->defaults->getColorPrimary(), | ||||
'no', | |||||
); | ); | ||||
$this->config | $this->config | ||||
->expects($this->once()) | ->expects($this->once()) | ||||
$this->imageManager->expects($this->exactly(4)) | $this->imageManager->expects($this->exactly(4)) | ||||
->method('getImageUrl') | ->method('getImageUrl') | ||||
->willReturnMap([ | ->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 = [ | $expected = [ |
height: 100%; | height: 100%; | ||||
position: absolute; | position: absolute; | ||||
background-color: var(--color-background-plain, var(--color-main-background)); | background-color: var(--color-background-plain, var(--color-main-background)); | ||||
background-image: var(--image-background); | |||||
background-size: cover; | |||||
background-position: center; | |||||
} | } | ||||
body { | body { | ||||
background-color: var(--color-background-plain, var(--color-main-background)); | 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-size: cover; | ||||
background-position: center; | background-position: center; | ||||
position: fixed; | position: fixed; |
width: 100%; | width: 100%; | ||||
height: 100%; | height: 100%; | ||||
position: absolute; | 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-color: var(--color-background-plain, var(--color-main-background)); | ||||
background-image: var(--image-background); | |||||
background-size: cover; | |||||
background-position: center; | |||||
} | } | ||||
body { | 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-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-size: cover; | ||||
background-position: center; | background-position: center; | ||||
position: fixed; | position: fixed; |
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'; | 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); | color: var(--color-text); | ||||
text-align: center; | 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; | background-attachment: fixed; | ||||
min-height: 100%; /* fix sticky footer */ | min-height: 100%; /* fix sticky footer */ | ||||
height: auto; | height: auto; |
height: 100%; | height: 100%; | ||||
position: absolute; | position: absolute; | ||||
background-color: var(--color-background-plain, var(--color-main-background)); | background-color: var(--color-background-plain, var(--color-main-background)); | ||||
background-image: var(--image-background); | |||||
background-size: cover; | |||||
background-position: center; | |||||
} | } | ||||
body { | body { | ||||
background-color: var(--color-background-plain, var(--color-main-background)); | 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-size: cover; | ||||
background-position: center; | background-position: center; | ||||
position: fixed; | position: fixed; |
/* 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 | |||||
}) | |||||
}, | |||||
}, | |||||
}) |
/** | |||||
* @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)) | |||||
} |
/** | |||||
* @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') | |||||
}) | |||||
}) |
/** | |||||
* @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=')) | |||||
}) | |||||
}) |
/** | |||||
* @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}`) | |||||
} | |||||
}) | |||||
}) |
/** | |||||
* @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' |
{ | |||||
"extends": "../tsconfig.json", | |||||
"include": ["./**/*.ts"], | |||||
"compilerOptions": { | |||||
"types": ["cypress", "dockerode", "cypress-wait-until"], | |||||
} | |||||
} |
"test:jsunit": "karma start tests/karma.config.js --single-run", | "test:jsunit": "karma start tests/karma.config.js --single-run", | ||||
"sass": "sass --load-path core/css core/css/ apps/*/css", | "sass": "sass --load-path core/css core/css/ apps/*/css", | ||||
"sass:watch": "sass --watch --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": { | "repository": { | ||||
"type": "git", | "type": "git", | ||||
"moment": "^2.29.4", | "moment": "^2.29.4", | ||||
"moment-timezone": "^0.5.38", | "moment-timezone": "^0.5.38", | ||||
"nextcloud-vue-collections": "^0.10.0", | "nextcloud-vue-collections": "^0.10.0", | ||||
"node-vibrant": "^3.1.6", | |||||
"p-limit": "^4.0.0", | "p-limit": "^4.0.0", | ||||
"p-queue": "^7.3.0", | "p-queue": "^7.3.0", | ||||
"path": "^0.12.7", | "path": "^0.12.7", | ||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
"@babel/node": "^7.20.0", | "@babel/node": "^7.20.0", | ||||
"@cypress/browserify-preprocessor": "^3.0.2", | |||||
"@nextcloud/babel-config": "^1.0.0", | "@nextcloud/babel-config": "^1.0.0", | ||||
"@nextcloud/cypress": "^1.0.0-beta.1", | |||||
"@nextcloud/eslint-config": "^8.0.0", | "@nextcloud/eslint-config": "^8.0.0", | ||||
"@nextcloud/stylelint-config": "^2.1.2", | "@nextcloud/stylelint-config": "^2.1.2", | ||||
"@testing-library/jest-dom": "^5.16.4", | "@testing-library/jest-dom": "^5.16.4", | ||||
"@testing-library/user-event": "^14.4.3", | "@testing-library/user-event": "^14.4.3", | ||||
"@testing-library/vue": "^5.8.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/test-utils": "^1.3.0", | ||||
"@vue/tsconfig": "^0.1.3", | |||||
"@vue/vue2-jest": "^29.1.1", | "@vue/vue2-jest": "^29.1.1", | ||||
"babel-jest": "^29.0.3", | "babel-jest": "^29.0.3", | ||||
"babel-loader": "^8.2.5", | "babel-loader": "^8.2.5", | ||||
"babel-loader-exclude-node-modules-except": "^1.2.1", | "babel-loader-exclude-node-modules-except": "^1.2.1", | ||||
"css-loader": "^6.7.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", | "eslint-plugin-es": "^4.1.0", | ||||
"exports-loader": "^4.0.0", | "exports-loader": "^4.0.0", | ||||
"file-loader": "^6.2.0", | "file-loader": "^6.2.0", | ||||
"sass-loader": "^12.6.0", | "sass-loader": "^12.6.0", | ||||
"sinon": "<= 5.0.7", | "sinon": "<= 5.0.7", | ||||
"style-loader": "^3.3.1", | "style-loader": "^3.3.1", | ||||
"ts-node": "^10.9.1", | |||||
"tslib": "^2.4.1", | |||||
"typescript": "^4.9.3", | |||||
"vue-loader": "^15.9.8", | "vue-loader": "^15.9.8", | ||||
"vue-template-compiler": "^2.7.13", | "vue-template-compiler": "^2.7.13", | ||||
"wait-on": "^6.0.1", | |||||
"webpack": "^5.75.0", | "webpack": "^5.75.0", | ||||
"webpack-cli": "^4.9.2", | "webpack-cli": "^4.9.2", | ||||
"webpack-merge": "^5.8.0" | "webpack-merge": "^5.8.0" |
{ | |||||
"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" | |||||
} | |||||
} | |||||
} |
extensions: ['*', '.js', '.vue'], | extensions: ['*', '.js', '.vue'], | ||||
symlinks: true, | symlinks: true, | ||||
fallback: { | fallback: { | ||||
stream: require.resolve('stream-browserify'), | |||||
buffer: require.resolve('buffer'), | buffer: require.resolve('buffer'), | ||||
fs: false, | |||||
stream: require.resolve('stream-browserify'), | |||||
}, | }, | ||||
}, | }, | ||||
} | } |