* 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
parent
9aec7b9b85
commit
55595f61df
@ -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); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
Loading…
Reference in new issue