Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@

namespace OCA\Libresign;

use OCA\Libresign\AppInfo\Application;
use OCA\Libresign\Service\Envelope\EnvelopeService;
use OCA\Libresign\Service\SignatureTextService;
use OCA\Libresign\Service\SignerElementsService;
use OCP\App\IAppManager;
use OCP\Capabilities\IPublicCapability;
use OCP\IAppConfig;

/**
* @psalm-import-type LibresignCapabilities from ResponseDefinitions
Expand All @@ -27,6 +29,7 @@ public function __construct(
protected SignatureTextService $signatureTextService,
protected IAppManager $appManager,
protected EnvelopeService $envelopeService,
protected IAppConfig $appConfig,
) {
}

Expand All @@ -39,6 +42,7 @@ public function getCapabilities(): array {
$capabilities = [
'features' => self::FEATURES,
'config' => [
'show-confetti' => $this->appConfig->getValueBool(Application::APP_ID, 'show_confetti_after_signing', true),
'sign-elements' => [
'is-available' => $this->signerElementsService->isSignElementsAvailable(),
'can-create-signature' => $this->signerElementsService->canCreateSignature(),
Expand Down
1 change: 1 addition & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@
* @psalm-type LibresignCapabilities = array{
* features: list<string>,
* config: array{
* show-confetti: bool,
* sign-elements: array{
* is-available: bool,
* can-create-signature: bool,
Expand Down
4 changes: 4 additions & 0 deletions openapi-administration.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@
"config": {
"type": "object",
"required": [
"show-confetti",
"sign-elements",
"envelope",
"upload"
],
"properties": {
"show-confetti": {
"type": "boolean"
},
"sign-elements": {
"type": "object",
"required": [
Expand Down
4 changes: 4 additions & 0 deletions openapi-full.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@
"config": {
"type": "object",
"required": [
"show-confetti",
"sign-elements",
"envelope",
"upload"
],
"properties": {
"show-confetti": {
"type": "boolean"
},
"sign-elements": {
"type": "object",
"required": [
Expand Down
4 changes: 4 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@
"config": {
"type": "object",
"required": [
"show-confetti",
"sign-elements",
"envelope",
"upload"
],
"properties": {
"show-confetti": {
"type": "boolean"
},
"sign-elements": {
"type": "object",
"required": [
Expand Down
111 changes: 111 additions & 0 deletions src/tests/views/Settings/Confetti.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* SPDX-FileCopyrightText: 2026 LibreSign contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { mount } from '@vue/test-utils'

const loadStateMock = vi.fn()

vi.mock('@nextcloud/initial-state', () => ({
loadState: (...args: unknown[]) => loadStateMock(...args),
}))

vi.mock('@nextcloud/l10n', () => ({
t: vi.fn((_app: string, text: string) => text),
translate: vi.fn((_app: string, text: string) => text),
translatePlural: vi.fn((_app: string, singular: string, plural: string, count: number) => (count === 1 ? singular : plural)),
n: vi.fn((_app: string, singular: string, plural: string, count: number) => (count === 1 ? singular : plural)),
isRTL: vi.fn(() => false),
getLanguage: vi.fn(() => 'en'),
getLocale: vi.fn(() => 'en'),
}))

let Confetti: unknown

beforeAll(async () => {
;({ default: Confetti } = await import('../../../views/Settings/Confetti.vue'))
})

describe('Confetti', () => {
beforeEach(() => {
loadStateMock.mockReset()
})

it('defaults to true when state is not set', async () => {
loadStateMock.mockImplementation((_app: string, key: string, fallback: unknown) => fallback)

const wrapper = mount(Confetti as never, {
global: {
stubs: {
NcSettingsSection: { template: '<div><slot /></div>' },
NcCheckboxRadioSwitch: { template: '<div><slot /></div>' },
},
},
})

expect(wrapper.vm.showConfetti).toBe(true)
})

it('reads show_confetti_after_signing from initial state', async () => {
loadStateMock.mockImplementation((_app: string, key: string, fallback: unknown) => {
if (key === 'show_confetti_after_signing') return true
return fallback
})

const wrapper = mount(Confetti as never, {
global: {
stubs: {
NcSettingsSection: { template: '<div><slot /></div>' },
NcCheckboxRadioSwitch: { template: '<div><slot /></div>' },
},
},
})

expect(wrapper.vm.showConfetti).toBe(true)
})

it('calls OCP.AppConfig.setValue with "1" when enabled', async () => {
loadStateMock.mockImplementation((_app: string, key: string, fallback: unknown) => fallback)
const setValueMock = vi.fn()
vi.stubGlobal('OCP', { AppConfig: { setValue: setValueMock } })

const wrapper = mount(Confetti as never, {
global: {
stubs: {
NcSettingsSection: { template: '<div><slot /></div>' },
NcCheckboxRadioSwitch: { template: '<div><slot /></div>' },
},
},
})

await wrapper.setData({ showConfetti: true })
wrapper.vm.saveShowConfetti()

expect(setValueMock).toHaveBeenCalledWith('libresign', 'show_confetti_after_signing', '1')
})

it('calls OCP.AppConfig.setValue with "0" when disabled', async () => {
loadStateMock.mockImplementation((_app: string, key: string, fallback: unknown) => {
if (key === 'show_confetti_after_signing') return true
return fallback
})
const setValueMock = vi.fn()
vi.stubGlobal('OCP', { AppConfig: { setValue: setValueMock } })

const wrapper = mount(Confetti as never, {
global: {
stubs: {
NcSettingsSection: { template: '<div><slot /></div>' },
NcCheckboxRadioSwitch: { template: '<div><slot /></div>' },
},
},
})

await wrapper.setData({ showConfetti: false })
wrapper.vm.saveShowConfetti()

expect(setValueMock).toHaveBeenCalledWith('libresign', 'show_confetti_after_signing', '0')
})
})
25 changes: 25 additions & 0 deletions src/tests/views/Validation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { afterEach, describe, expect, it, beforeEach, vi } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import axios from '@nextcloud/axios'
import { getCapabilities } from '@nextcloud/capabilities'
import JSConfetti from 'js-confetti'
import Validation from '../../views/Validation.vue'

Expand Down Expand Up @@ -104,6 +105,17 @@ const mockRouter = {
push: vi.fn(),
}

// Mock capabilities - show-confetti enabled by default so existing tests pass
vi.mock('@nextcloud/capabilities', () => ({
getCapabilities: vi.fn(() => ({
libresign: {
config: {
'show-confetti': true,
},
},
})),
}))

// Mock initial state
vi.mock('@nextcloud/initial-state', () => ({
loadState: vi.fn((app, key, defaultValue) => defaultValue),
Expand Down Expand Up @@ -654,6 +666,19 @@ describe('Validation.vue - Business Logic', () => {
wrapper.vm.handleValidationSuccess({ status: SIGNED_STATUS, signers: [] })
expect(mockAddConfetti).not.toHaveBeenCalled()
})

it('does not fire confetti when show-confetti capability is disabled', async () => {
vi.mocked(getCapabilities).mockReturnValueOnce({
libresign: {
config: {
'show-confetti': false,
},
},
} as ReturnType<typeof getCapabilities>)
vi.spyOn(wrapper.vm, 'isAfterSigned', 'get').mockReturnValue(true)
wrapper.vm.handleValidationSuccess({ status: SIGNED_STATUS, signers: [] })
expect(mockAddConfetti).not.toHaveBeenCalled()
})
})

describe('handleSigningComplete method', () => {
Expand Down
1 change: 1 addition & 0 deletions src/types/openapi/openapi-administration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ export type components = {
Capabilities: {
features: string[];
config: {
"show-confetti": boolean;
"sign-elements": {
"is-available": boolean;
"can-create-signature": boolean;
Expand Down
1 change: 1 addition & 0 deletions src/types/openapi/openapi-full.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,7 @@ export type components = {
Capabilities: {
features: string[];
config: {
"show-confetti": boolean;
"sign-elements": {
"is-available": boolean;
"can-create-signature": boolean;
Expand Down
1 change: 1 addition & 0 deletions src/types/openapi/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,7 @@ export type components = {
Capabilities: {
features: string[];
config: {
"show-confetti": boolean;
"sign-elements": {
"is-available": boolean;
"can-create-signature": boolean;
Expand Down
41 changes: 41 additions & 0 deletions src/views/Settings/Confetti.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!--
- SPDX-FileCopyrightText: 2026 LibreCode coop and LibreCode contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcSettingsSection
:name="t('libresign', 'Confetti animation')"
:description="t('libresign', 'Control whether a confetti animation is shown after a document is signed.')">
<NcCheckboxRadioSwitch type="switch"
v-model="showConfetti"
@update:modelValue="saveShowConfetti">
{{ t('libresign', 'Show confetti animation after signing') }}
</NcCheckboxRadioSwitch>
</NcSettingsSection>
</template>
<script>
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'

import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'

export default {
name: 'ConfettiSettings',
components: {
NcSettingsSection,
NcCheckboxRadioSwitch,
},
data() {
return {
showConfetti: loadState('libresign', 'show_confetti_after_signing', true) === true,
}
},
methods: {
t,
saveShowConfetti() {
OCP.AppConfig.setValue('libresign', 'show_confetti_after_signing', this.showConfetti ? '1' : '0')
},
},
}
</script>
7 changes: 5 additions & 2 deletions src/views/Validation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import {
} from '@mdi/js'
import JSConfetti from 'js-confetti'
import axios from '@nextcloud/axios'
import { getCapabilities } from '@nextcloud/capabilities'
import { formatFileSize } from '@nextcloud/files'
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
Expand Down Expand Up @@ -614,8 +615,10 @@ export default {
&& this.document.files.every(file => isSignedStatus(file.status))
const signerCompleted = this.isCurrentSignerSigned()
if ((isSignedDoc || allFilesSigned || signerCompleted) && (this.isAfterSigned || this.shouldFireAsyncConfetti)) {
const jsConfetti = new JSConfetti()
jsConfetti.addConfetti()
if (getCapabilities()?.libresign?.config?.['show-confetti'] === true) {
const jsConfetti = new JSConfetti()
jsConfetti.addConfetti()
}
this.shouldFireAsyncConfetti = false
}
},
Expand Down
21 changes: 21 additions & 0 deletions tests/php/Unit/CapabilitiesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

use OCA\Libresign\AppInfo\Application;
use OCA\Libresign\Capabilities;
use OCA\Libresign\Service\Envelope\EnvelopeService;
use OCA\Libresign\Service\SignatureTextService;
use OCA\Libresign\Service\SignerElementsService;
use OCP\App\IAppManager;
use OCP\IAppConfig;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;

Expand All @@ -23,12 +25,14 @@ final class CapabilitiesTest extends \OCA\Libresign\Tests\Unit\TestCase {
private SignatureTextService&MockObject $signatureTextService;
private IAppManager&MockObject $appManager;
private EnvelopeService&MockObject $envelopeService;
private IAppConfig&MockObject $appConfig;

public function setUp(): void {
$this->signerElementsService = $this->createMock(SignerElementsService::class);
$this->signatureTextService = $this->createMock(SignatureTextService::class);
$this->appManager = $this->createMock(IAppManager::class);
$this->envelopeService = $this->createMock(EnvelopeService::class);
$this->appConfig = $this->createMock(IAppConfig::class);
}


Expand All @@ -38,6 +42,7 @@ private function getClass(): Capabilities {
$this->signatureTextService,
$this->appManager,
$this->envelopeService,
$this->appConfig,
);
return $this->capabilities;
}
Expand All @@ -55,4 +60,20 @@ public static function providerSignElementsIsAvailable(): array {
[false, false],
];
}

#[DataProvider('providerShowConfetti')]
public function testShowConfetti(bool $configValue, bool $expected): void {
$this->appConfig->method('getValueBool')
->with(Application::APP_ID, 'show_confetti_after_signing', true)
->willReturn($configValue);
$capabilities = $this->getClass()->getCapabilities();
$this->assertEquals($expected, $capabilities['libresign']['config']['show-confetti']);
}

public static function providerShowConfetti(): array {
return [
[true, true],
[false, false],
];
}
}
Loading