Browse Source
refactor(federatedfilesharing): Replace deprecated functions
refactor(federatedfilesharing): Replace deprecated functions
* Replace deprecated OC dialogs methods * Replace deprecated global jQuery with axios * Replace deprecated jQuery event with event bus * Add component + unit tests for new dialog Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>pull/48725/head
committed by
skjnldsv
7 changed files with 370 additions and 345 deletions
-
1__tests__/setup-testing-library.js
-
123apps/federatedfilesharing/src/components/RemoteShareDialog.cy.ts
-
67apps/federatedfilesharing/src/components/RemoteShareDialog.vue
-
182apps/federatedfilesharing/src/external.js
-
65apps/federatedfilesharing/src/services/dialogService.spec.ts
-
36apps/federatedfilesharing/src/services/dialogService.ts
-
241apps/federatedfilesharing/tests/js/externalSpec.js
@ -0,0 +1,123 @@ |
|||
/** |
|||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors |
|||
* SPDX-License-Identifier: AGPL-3.0-or-later |
|||
*/ |
|||
|
|||
import RemoteShareDialog from './RemoteShareDialog.vue' |
|||
|
|||
describe('RemoteShareDialog', () => { |
|||
it('can be mounted', () => { |
|||
cy.mount(RemoteShareDialog, { |
|||
propsData: { |
|||
owner: 'user123', |
|||
name: 'my-photos', |
|||
remote: 'nextcloud.local', |
|||
passwordRequired: false, |
|||
}, |
|||
}) |
|||
|
|||
cy.findByRole('dialog') |
|||
.should('be.visible') |
|||
.and('contain.text', 'user123@nextcloud.local') |
|||
.and('contain.text', 'my-photos') |
|||
cy.findByRole('button', { name: 'Cancel' }) |
|||
.should('be.visible') |
|||
cy.findByRole('button', { name: /add remote share/i }) |
|||
.should('be.visible') |
|||
}) |
|||
|
|||
it('does not show password input if not enabled', () => { |
|||
cy.mount(RemoteShareDialog, { |
|||
propsData: { |
|||
owner: 'user123', |
|||
name: 'my-photos', |
|||
remote: 'nextcloud.local', |
|||
passwordRequired: false, |
|||
}, |
|||
}) |
|||
|
|||
cy.findByRole('dialog') |
|||
.should('be.visible') |
|||
.find('input[type="password"]') |
|||
.should('not.exist') |
|||
}) |
|||
|
|||
it('emits true when accepted', () => { |
|||
const onClose = cy.spy().as('onClose') |
|||
|
|||
cy.mount(RemoteShareDialog, { |
|||
listeners: { |
|||
close: onClose, |
|||
}, |
|||
propsData: { |
|||
owner: 'user123', |
|||
name: 'my-photos', |
|||
remote: 'nextcloud.local', |
|||
passwordRequired: false, |
|||
}, |
|||
}) |
|||
|
|||
cy.findByRole('button', { name: 'Cancel' }).click() |
|||
cy.get('@onClose') |
|||
.should('have.been.calledWith', false) |
|||
}) |
|||
|
|||
it('show password input if needed', () => { |
|||
cy.mount(RemoteShareDialog, { |
|||
propsData: { |
|||
owner: 'admin', |
|||
name: 'secret-data', |
|||
remote: 'nextcloud.local', |
|||
passwordRequired: true, |
|||
}, |
|||
}) |
|||
|
|||
cy.findByRole('dialog') |
|||
.should('be.visible') |
|||
.find('input[type="password"]') |
|||
.should('be.visible') |
|||
}) |
|||
|
|||
it('emits the submitted password', () => { |
|||
const onClose = cy.spy().as('onClose') |
|||
|
|||
cy.mount(RemoteShareDialog, { |
|||
listeners: { |
|||
close: onClose, |
|||
}, |
|||
propsData: { |
|||
owner: 'admin', |
|||
name: 'secret-data', |
|||
remote: 'nextcloud.local', |
|||
passwordRequired: true, |
|||
}, |
|||
}) |
|||
|
|||
cy.get('input[type="password"]') |
|||
.type('my password{enter}') |
|||
cy.get('@onClose') |
|||
.should('have.been.calledWith', true, 'my password') |
|||
}) |
|||
|
|||
it('emits no password if cancelled', () => { |
|||
const onClose = cy.spy().as('onClose') |
|||
|
|||
cy.mount(RemoteShareDialog, { |
|||
listeners: { |
|||
close: onClose, |
|||
}, |
|||
propsData: { |
|||
owner: 'admin', |
|||
name: 'secret-data', |
|||
remote: 'nextcloud.local', |
|||
passwordRequired: true, |
|||
}, |
|||
}) |
|||
|
|||
cy.get('input[type="password"]') |
|||
.type('my password') |
|||
cy.findByRole('button', { name: 'Cancel' }).click() |
|||
cy.get('@onClose') |
|||
.should('have.been.calledWith', false) |
|||
}) |
|||
}) |
@ -0,0 +1,67 @@ |
|||
<!-- |
|||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors |
|||
- SPDX-License-Identifier: AGPL-3.0-or-later |
|||
--> |
|||
<script setup lang="ts"> |
|||
import { t } from '@nextcloud/l10n' |
|||
import { computed, ref } from 'vue' |
|||
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' |
|||
import NcPasswordField from '@nextcloud/vue/dist/Components/NcPasswordField.js' |
|||
|
|||
const props = defineProps<{ |
|||
/** Name of the share */ |
|||
name: string |
|||
/** Display name of the owner */ |
|||
owner: string |
|||
/** The remote instance name */ |
|||
remote: string |
|||
/** True if the user should enter a password */ |
|||
passwordRequired: boolean |
|||
}>() |
|||
|
|||
const emit = defineEmits<{ |
|||
(e: 'close', state: boolean, password?: string): void |
|||
}>() |
|||
|
|||
const password = ref('') |
|||
|
|||
/** |
|||
* The dialog buttons |
|||
*/ |
|||
const buttons = computed(() => [ |
|||
{ |
|||
label: t('federatedfilesharing', 'Cancel'), |
|||
callback: () => emit('close', false), |
|||
}, |
|||
{ |
|||
label: t('federatedfilesharing', 'Add remote share'), |
|||
nativeType: props.passwordRequired ? 'submit' : undefined, |
|||
type: 'primary', |
|||
callback: () => emit('close', true, password.value), |
|||
}, |
|||
]) |
|||
</script> |
|||
|
|||
<template> |
|||
<NcDialog :buttons="buttons" |
|||
:is-form="passwordRequired" |
|||
:name="t('federatedfilesharing', 'Remote share')" |
|||
@submit="emit('close', true, password)"> |
|||
<p> |
|||
{{ t('federatedfilesharing', 'Do you want to add the remote share {name} from {owner}@{remote}?', { name, owner, remote }) }} |
|||
</p> |
|||
<NcPasswordField v-if="passwordRequired" |
|||
class="remote-share-dialog__password" |
|||
:label="t('federatedfilesharing', 'Remote share password')" |
|||
:value.sync="password" /> |
|||
</NcDialog> |
|||
</template> |
|||
|
|||
<style scoped lang="scss"> |
|||
.remote-share-dialog { |
|||
|
|||
&__password { |
|||
margin-block: 1em .5em; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,65 @@ |
|||
/** |
|||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors |
|||
* SPDX-License-Identifier: AGPL-3.0-or-later |
|||
*/ |
|||
|
|||
import { describe, expect, it } from 'vitest' |
|||
import { showRemoteShareDialog } from './dialogService' |
|||
import { nextTick } from 'vue' |
|||
|
|||
describe('federatedfilesharing: dialog service', () => { |
|||
it('mounts dialog', async () => { |
|||
showRemoteShareDialog('share-name', 'user123', 'example.com') |
|||
await nextTick() |
|||
expect(document.querySelector('[role="dialog"]')).not.toBeNull() |
|||
expect(document.querySelector('[role="dialog"]')!.textContent).to.contain('share-name') |
|||
expect(document.querySelector('[role="dialog"]')!.textContent).to.contain('user123@example.com') |
|||
expect(document.querySelector('[role="dialog"] input[type="password"]')).toBeNull() |
|||
}) |
|||
|
|||
it('shows password input', async () => { |
|||
showRemoteShareDialog('share-name', 'user123', 'example.com', true) |
|||
await nextTick() |
|||
expect(document.querySelector('[role="dialog"]')).not.toBeNull() |
|||
expect(document.querySelector('[role="dialog"] input[type="password"]')).not.toBeNull() |
|||
}) |
|||
|
|||
it('resolves if accepted', async () => { |
|||
const promise = showRemoteShareDialog('share-name', 'user123', 'example.com') |
|||
await nextTick() |
|||
|
|||
for (const button of document.querySelectorAll('button').values()) { |
|||
if (button.textContent?.match(/add remote share/i)) { |
|||
button.click() |
|||
} |
|||
} |
|||
|
|||
expect(await promise).toBe(undefined) |
|||
}) |
|||
|
|||
it('resolves password if accepted', async () => { |
|||
const promise = showRemoteShareDialog('share-name', 'user123', 'example.com', true) |
|||
await nextTick() |
|||
|
|||
for (const button of document.querySelectorAll('button').values()) { |
|||
if (button.textContent?.match(/add remote share/i)) { |
|||
button.click() |
|||
} |
|||
} |
|||
|
|||
expect(await promise).toBe('') |
|||
}) |
|||
|
|||
it('rejects if cancelled', async () => { |
|||
const promise = showRemoteShareDialog('share-name', 'user123', 'example.com') |
|||
await nextTick() |
|||
|
|||
for (const button of document.querySelectorAll('button').values()) { |
|||
if (button.textContent?.match(/cancel/i)) { |
|||
button.click() |
|||
} |
|||
} |
|||
|
|||
expect(async () => await promise).rejects.toThrow() |
|||
}) |
|||
}) |
@ -0,0 +1,36 @@ |
|||
/** |
|||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors |
|||
* SPDX-License-Identifier: AGPL-3.0-or-later |
|||
*/ |
|||
|
|||
import { spawnDialog } from '@nextcloud/dialogs' |
|||
import RemoteShareDialog from '../components/RemoteShareDialog.vue' |
|||
|
|||
/** |
|||
* Open a dialog to ask the user whether to add a remote share. |
|||
* |
|||
* @param name The name of the share |
|||
* @param owner The owner of the share |
|||
* @param remote The remote address |
|||
* @param passwordRequired True if the share is password protected |
|||
*/ |
|||
export function showRemoteShareDialog( |
|||
name: string, |
|||
owner: string, |
|||
remote: string, |
|||
passwordRequired = false, |
|||
): Promise<string|void> { |
|||
const { promise, reject, resolve } = Promise.withResolvers<string|void>() |
|||
|
|||
spawnDialog(RemoteShareDialog, { name, owner, remote, passwordRequired }, (status, password) => { |
|||
if (passwordRequired && status) { |
|||
resolve(password as string) |
|||
} else if (status) { |
|||
resolve(undefined) |
|||
} else { |
|||
reject() |
|||
} |
|||
}) |
|||
|
|||
return promise |
|||
} |
@ -1,241 +0,0 @@ |
|||
/** |
|||
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors |
|||
* SPDX-FileCopyrightText: 2015 ownCloud, Inc. |
|||
* SPDX-License-Identifier: AGPL-3.0-or-later |
|||
*/ |
|||
|
|||
describe('OCA.Sharing external tests', function() { |
|||
var plugin; |
|||
var urlQueryStub; |
|||
var promptDialogStub; |
|||
var confirmDialogStub; |
|||
|
|||
function dummyShowDialog() { |
|||
var deferred = $.Deferred(); |
|||
deferred.resolve(); |
|||
return deferred.promise(); |
|||
} |
|||
|
|||
beforeEach(function() { |
|||
plugin = OCA.Sharing.ExternalShareDialogPlugin; |
|||
urlQueryStub = sinon.stub(OC.Util.History, 'parseUrlQuery'); |
|||
|
|||
confirmDialogStub = sinon.stub(OC.dialogs, 'confirm').callsFake(dummyShowDialog); |
|||
promptDialogStub = sinon.stub(OC.dialogs, 'prompt').callsFake(dummyShowDialog); |
|||
|
|||
plugin.filesApp = { |
|||
fileList: { |
|||
reload: sinon.stub() |
|||
} |
|||
}; |
|||
}); |
|||
afterEach(function() { |
|||
urlQueryStub.restore(); |
|||
confirmDialogStub.restore(); |
|||
promptDialogStub.restore(); |
|||
plugin = null; |
|||
}); |
|||
describe('confirmation dialog from URL', function() { |
|||
var testShare; |
|||
|
|||
/** |
|||
* Checks that the server call's query matches what is |
|||
* expected. |
|||
* |
|||
* @param {Object} expectedQuery expected query params |
|||
*/ |
|||
function checkRequest(expectedQuery) { |
|||
var request = fakeServer.requests[0]; |
|||
var query = OC.parseQueryString(request.requestBody); |
|||
expect(request.method).toEqual('POST'); |
|||
expect(query).toEqual(expectedQuery); |
|||
|
|||
request.respond( |
|||
200, |
|||
{'Content-Type': 'application/json'}, |
|||
JSON.stringify({status: 'success'}) |
|||
); |
|||
expect(plugin.filesApp.fileList.reload.calledOnce).toEqual(true); |
|||
} |
|||
|
|||
beforeEach(function() { |
|||
testShare = { |
|||
remote: 'http://example.com/owncloud', |
|||
token: 'abcdefg', |
|||
owner: 'theowner', |
|||
ownerDisplayName: 'The Generous Owner', |
|||
name: 'the share name' |
|||
}; |
|||
}); |
|||
it('does nothing when no share was passed in URL', function() { |
|||
urlQueryStub.returns({}); |
|||
plugin.processIncomingShareFromUrl(); |
|||
expect(promptDialogStub.notCalled).toEqual(true); |
|||
expect(confirmDialogStub.notCalled).toEqual(true); |
|||
expect(fakeServer.requests.length).toEqual(0); |
|||
}); |
|||
it('sends share info to server on confirm', function() { |
|||
urlQueryStub.returns(testShare); |
|||
plugin.processIncomingShareFromUrl(); |
|||
expect(promptDialogStub.notCalled).toEqual(true); |
|||
expect(confirmDialogStub.calledOnce).toEqual(true); |
|||
confirmDialogStub.getCall(0).args[2](true); |
|||
expect(fakeServer.requests.length).toEqual(1); |
|||
checkRequest({ |
|||
remote: 'http://example.com/owncloud', |
|||
token: 'abcdefg', |
|||
owner: 'theowner', |
|||
ownerDisplayName: 'The Generous Owner', |
|||
name: 'the share name', |
|||
password: '' |
|||
}); |
|||
}); |
|||
it('sends share info with password to server on confirm', function() { |
|||
testShare = _.extend(testShare, {protected: 1}); |
|||
urlQueryStub.returns(testShare); |
|||
plugin.processIncomingShareFromUrl(); |
|||
expect(promptDialogStub.calledOnce).toEqual(true); |
|||
expect(confirmDialogStub.notCalled).toEqual(true); |
|||
promptDialogStub.getCall(0).args[2](true, 'thepassword'); |
|||
expect(fakeServer.requests.length).toEqual(1); |
|||
checkRequest({ |
|||
remote: 'http://example.com/owncloud', |
|||
token: 'abcdefg', |
|||
owner: 'theowner', |
|||
ownerDisplayName: 'The Generous Owner', |
|||
name: 'the share name', |
|||
password: 'thepassword' |
|||
}); |
|||
}); |
|||
it('does not send share info on cancel', function() { |
|||
urlQueryStub.returns(testShare); |
|||
plugin.processIncomingShareFromUrl(); |
|||
expect(promptDialogStub.notCalled).toEqual(true); |
|||
expect(confirmDialogStub.calledOnce).toEqual(true); |
|||
confirmDialogStub.getCall(0).args[2](false); |
|||
expect(fakeServer.requests.length).toEqual(0); |
|||
}); |
|||
}); |
|||
describe('show dialog for each share to confirm', function() { |
|||
var testShare; |
|||
|
|||
/** |
|||
* Call processSharesToConfirm() and make the fake server |
|||
* return the passed response. |
|||
* |
|||
* @param {Array} response list of shares to process |
|||
*/ |
|||
function processShares(response) { |
|||
plugin.processSharesToConfirm(); |
|||
|
|||
expect(fakeServer.requests.length).toEqual(1); |
|||
|
|||
var req = fakeServer.requests[0]; |
|||
expect(req.method).toEqual('GET'); |
|||
expect(req.url).toEqual(OC.getRootPath() + '/index.php/apps/files_sharing/api/externalShares'); |
|||
|
|||
req.respond( |
|||
200, |
|||
{'Content-Type': 'application/json'}, |
|||
JSON.stringify(response) |
|||
); |
|||
} |
|||
|
|||
beforeEach(function() { |
|||
testShare = { |
|||
id: 123, |
|||
remote: 'http://example.com/owncloud', |
|||
token: 'abcdefg', |
|||
owner: 'theowner', |
|||
ownerDisplayName: 'The Generous Owner', |
|||
name: 'the share name' |
|||
}; |
|||
}); |
|||
|
|||
it('does not show any dialog if no shares to confirm', function() { |
|||
processShares([]); |
|||
expect(confirmDialogStub.notCalled).toEqual(true); |
|||
expect(promptDialogStub.notCalled).toEqual(true); |
|||
}); |
|||
it('sends accept info to server on confirm', function() { |
|||
processShares([testShare]); |
|||
|
|||
expect(promptDialogStub.notCalled).toEqual(true); |
|||
expect(confirmDialogStub.calledOnce).toEqual(true); |
|||
|
|||
confirmDialogStub.getCall(0).args[2](true); |
|||
|
|||
expect(fakeServer.requests.length).toEqual(2); |
|||
|
|||
var request = fakeServer.requests[1]; |
|||
var query = OC.parseQueryString(request.requestBody); |
|||
expect(request.method).toEqual('POST'); |
|||
expect(query).toEqual({id: '123'}); |
|||
expect(request.url).toEqual( |
|||
OC.getRootPath() + '/index.php/apps/files_sharing/api/externalShares' |
|||
); |
|||
|
|||
expect(plugin.filesApp.fileList.reload.notCalled).toEqual(true); |
|||
request.respond( |
|||
200, |
|||
{'Content-Type': 'application/json'}, |
|||
JSON.stringify({status: 'success'}) |
|||
); |
|||
expect(plugin.filesApp.fileList.reload.calledOnce).toEqual(true); |
|||
}); |
|||
it('sends delete info to server on cancel', function() { |
|||
processShares([testShare]); |
|||
|
|||
expect(promptDialogStub.notCalled).toEqual(true); |
|||
expect(confirmDialogStub.calledOnce).toEqual(true); |
|||
|
|||
confirmDialogStub.getCall(0).args[2](false); |
|||
|
|||
expect(fakeServer.requests.length).toEqual(2); |
|||
|
|||
var request = fakeServer.requests[1]; |
|||
expect(request.method).toEqual('DELETE'); |
|||
expect(request.url).toEqual( |
|||
OC.getRootPath() + '/index.php/apps/files_sharing/api/externalShares/123' |
|||
); |
|||
|
|||
expect(plugin.filesApp.fileList.reload.notCalled).toEqual(true); |
|||
request.respond( |
|||
200, |
|||
{'Content-Type': 'application/json'}, |
|||
JSON.stringify({status: 'success'}) |
|||
); |
|||
expect(plugin.filesApp.fileList.reload.notCalled).toEqual(true); |
|||
}); |
|||
xit('shows another dialog when multiple shares need to be accepted', function() { |
|||
// TODO: enable this test when fixing multiple dialogs issue / confirm loop
|
|||
var testShare2 = _.extend({}, testShare); |
|||
testShare2.id = 256; |
|||
processShares([testShare, testShare2]); |
|||
|
|||
// confirm first one
|
|||
expect(confirmDialogStub.calledOnce).toEqual(true); |
|||
confirmDialogStub.getCall(0).args[2](true); |
|||
|
|||
// next dialog not shown yet
|
|||
expect(confirmDialogStub.calledOnce); |
|||
|
|||
// respond to the first accept request
|
|||
fakeServer.requests[1].respond( |
|||
200, |
|||
{'Content-Type': 'application/json'}, |
|||
JSON.stringify({status: 'success'}) |
|||
); |
|||
|
|||
// don't reload yet, there are other shares to confirm
|
|||
expect(plugin.filesApp.fileList.reload.notCalled).toEqual(true); |
|||
|
|||
// cancel second share
|
|||
expect(confirmDialogStub.calledTwice).toEqual(true); |
|||
confirmDialogStub.getCall(1).args[2](true); |
|||
|
|||
// reload only called at the very end
|
|||
expect(plugin.filesApp.fileList.reload.calledOnce).toEqual(true); |
|||
}); |
|||
}); |
|||
}); |
Write
Preview
Loading…
Cancel
Save
Reference in new issue