* 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