feat(systemtags): add cypress tests and fix a few logic issues

Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
This commit is contained in:
skjnldsv 2024-10-24 15:35:19 +02:00
parent db546e1f55
commit d51cf4536c
11 changed files with 414 additions and 18 deletions

View file

@ -21,7 +21,6 @@ use OCP\Util;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Conflict;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\PreconditionFailed;
use Sabre\DAV\Exception\UnsupportedMediaType;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
@ -218,8 +217,8 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
$propFind->setPath(str_replace('systemtags-assigned/', 'systemtags/', $propFind->getPath()));
}
$propFind->handle(FilesPlugin::GETETAG_PROPERTYNAME, function () use ($node): string|null {
return $node->getSystemTag()->getETag();
$propFind->handle(FilesPlugin::GETETAG_PROPERTYNAME, function () use ($node): string {
return '"' . ($node->getSystemTag()->getETag() ?? '') . '"';
});
$propFind->handle(self::ID_PROPERTYNAME, function () use ($node) {
@ -379,7 +378,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
if (isset($props[self::OBJECTIDS_PROPERTYNAME])) {
$propValue = $props[self::OBJECTIDS_PROPERTYNAME];
if (!($propValue instanceof SystemTagsObjectList) || count($propValue?->getObjects() ?: []) === 0) {
if (!($propValue instanceof SystemTagsObjectList) || count($propValue->getObjects()) === 0) {
throw new BadRequest('Invalid object-ids property');
}

View file

@ -9,6 +9,7 @@
<NcCheckboxRadioSwitch v-else
:aria-label="ariaLabel"
:checked="isSelected"
data-cy-files-list-row-checkbox
@update:checked="onSelectionChange" />
</td>
</template>

View file

@ -6,7 +6,7 @@
<tr class="files-list__row-head">
<th class="files-list__column files-list__row-checkbox"
@keyup.esc.exact="resetSelection">
<NcCheckboxRadioSwitch v-bind="selectAllBind" @update:checked="onToggleAll" />
<NcCheckboxRadioSwitch v-bind="selectAllBind" data-cy-files-list-selection-checkbox @update:checked="onToggleAll" />
</th>
<!-- Columns display -->

View file

@ -3,7 +3,7 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="files-list__column files-list__row-actions-batch">
<div class="files-list__column files-list__row-actions-batch" data-cy-files-list-selection-actions>
<NcActions ref="actionsMenu"
container="#app-content-vue"
:disabled="!!loading || areSomeNodesLoading"
@ -15,6 +15,7 @@
:key="action.id"
:aria-label="action.displayName(nodes, currentView) + ' ' + t('files', '(selected)') /** TRANSLATORS: Selected like 'selected files and folders' */"
:class="'files-list__row-actions-batch-' + action.id"
:data-cy-files-list-selection-action="action.id"
@click="onActionClick(action)">
<template #icon>
<NcLoadingIcon v-if="loading === action.id" :size="18" />

View file

@ -23,22 +23,28 @@
<!-- Search or create input -->
<form class="systemtags-picker__create" @submit.stop.prevent="onNewTag">
<NcTextField :value.sync="input"
:label="t('systemtags', 'Search or create tag')">
:label="t('systemtags', 'Search or create tag')"
data-cy-systemtags-picker-input>
<TagIcon :size="20" />
</NcTextField>
<NcButton :disabled="status === Status.CREATING_TAG" native-type="submit">
<NcButton :disabled="status === Status.CREATING_TAG"
native-type="submit"
data-cy-systemtags-picker-input-submit>
{{ t('systemtags', 'Create tag') }}
</NcButton>
</form>
<!-- Tags list -->
<div v-if="filteredTags.length > 0" class="systemtags-picker__tags">
<div v-if="filteredTags.length > 0"
class="systemtags-picker__tags"
data-cy-systemtags-picker-tags>
<NcCheckboxRadioSwitch v-for="tag in filteredTags"
:key="tag.id"
:label="tag.displayName"
:checked="isChecked(tag)"
:indeterminate="isIndeterminate(tag)"
:disabled="!tag.canAssign"
:data-cy-systemtags-picker-tag="tag.id"
@update:checked="onCheckUpdate(tag, $event)">
{{ formatTagName(tag) }}
</NcCheckboxRadioSwitch>
@ -61,10 +67,15 @@
</template>
<template #actions>
<NcButton :disabled="status !== Status.BASE" type="tertiary" @click="onCancel">
<NcButton :disabled="status !== Status.BASE"
type="tertiary"
data-cy-systemtags-picker-button-cancel
@click="onCancel">
{{ t('systemtags', 'Cancel') }}
</NcButton>
<NcButton :disabled="!hasChanges || status !== Status.BASE" @click="onSubmit">
<NcButton :disabled="!hasChanges || status !== Status.BASE"
data-cy-systemtags-picker-button-submit
@click="onSubmit">
{{ t('systemtags', 'Apply changes') }}
</NcButton>
</template>
@ -270,11 +281,11 @@ export default defineComponent({
},
formatTagName(tag: TagWithId): string {
if (tag.userVisible) {
if (!tag.userVisible) {
return t('systemtags', '{displayName} (hidden)', { displayName: tag.displayName })
}
if (tag.userAssignable) {
if (!tag.userAssignable) {
return t('systemtags', '{displayName} (restricted)', { displayName: tag.displayName })
}
@ -317,6 +328,9 @@ export default defineComponent({
const tag = await fetchTag(id)
this.tags.push(tag)
this.input = ''
// Check the newly created tag
this.onCheckUpdate(tag, true)
} catch (error) {
showError((error as Error)?.message || t('systemtags', 'Failed to create tag'))
} finally {

View file

@ -1,3 +1,7 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Node } from '@nextcloud/files'
declare module '@nextcloud/event-bus' {

View file

@ -19,7 +19,7 @@ export const action = new FileAction({
// If the app is disabled, the action is not available anyway
enabled(nodes) {
if (nodes.length > 0) {
if (nodes.length === 0) {
return false
}

View file

@ -14,11 +14,15 @@ export const getActionButtonForFile = (filename: string) => getActionsForFile(fi
export const triggerActionForFileId = (fileid: number, actionId: string) => {
getActionButtonForFileId(fileid).click()
cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).should('exist').click()
// Getting the last button to avoid the one from popup fading out
cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).last()
.should('exist').click()
}
export const triggerActionForFile = (filename: string, actionId: string) => {
getActionButtonForFile(filename).click()
cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).should('exist').click()
// Getting the last button to avoid the one from popup fading out
cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).last()
.should('exist').click()
}
export const triggerInlineActionForFileId = (fileid: number, actionId: string) => {
@ -28,6 +32,25 @@ export const triggerInlineActionForFile = (filename: string, actionId: string) =
getActionsForFile(filename).get(`button[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`).should('exist').click()
}
export const selectAllFiles = () => {
cy.get('[data-cy-files-list-selection-checkbox]').findByRole('checkbox').click({ force: true })
}
export const selectRowForFile = (filename: string) => {
getRowForFile(filename)
.find('[data-cy-files-list-row-checkbox]')
.findByRole('checkbox')
.click({ force: true })
.should('be.checked')
cy.get('[data-cy-files-list-selection-checkbox]').findByRole('checkbox').should('satisfy', (elements) => {
return elements.length === 1 && (elements[0].checked === true || elements[0].indeterminate === true)
})
}
export const triggerSelectionAction = (actionId: string) => {
cy.get(`button[data-cy-files-list-selection-action="${CSS.escape(actionId)}"]`).should('exist').click()
}
export const moveFile = (fileName: string, dirPath: string) => {
getRowForFile(fileName).should('be.visible')
triggerActionForFile(fileName, 'move-copy')

View file

@ -0,0 +1,354 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import { randomBytes } from 'crypto'
import { getRowForFile, selectAllFiles, selectRowForFile, triggerSelectionAction } from '../files/FilesUtils'
import { createShare } from '../files_sharing/FilesSharingUtils'
let tags = {} as Record<string, number>
const files = [
'file1.txt',
'file2.txt',
'file3.txt',
'file4.txt',
'file5.txt',
]
function resetTags() {
tags = {}
for (const tag in [0, 1, 2, 3, 4]) {
tags[randomBytes(8).toString('base64').slice(0, 6)] = 0
}
// delete any existing tags
cy.runOccCommand('tag:list --output=json').then((output) => {
Object.keys(JSON.parse(output.stdout)).forEach((id) => {
cy.runOccCommand(`tag:delete ${id}`)
})
})
// create tags
Object.keys(tags).forEach((tag) => {
cy.runOccCommand(`tag:add ${tag} public --output=json`).then((output) => {
tags[tag] = JSON.parse(output.stdout).id as number
})
})
cy.log('Using tags', tags)
}
function expectInlineTagForFile(file: string, tags: string[]) {
getRowForFile(file)
.find('[data-systemtags-fileid]')
.findAllByRole('listitem')
.should('have.length', tags.length)
.each(tag => {
expect(tag.text()).to.be.oneOf(tags)
})
}
function triggerTagManagementDialogAction() {
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/').as('getTagsList')
triggerSelectionAction('systemtags:bulk')
cy.wait('@getTagsList')
cy.get('[data-cy-systemtags-picker]').should('be.visible')
}
describe('Systemtags: Files bulk action', { testIsolation: false }, () => {
let snapshot: string
let user1: User
let user2: User
before(() => {
cy.createRandomUser().then((_user1) => {
user1 = _user1
cy.createRandomUser().then((_user2) => {
user2 = _user2
})
files.forEach((file) => {
cy.uploadContent(user1, new Blob([]), 'text/plain', '/' + file)
})
})
resetTags()
})
it('Can assign tag to selection', () => {
cy.login(user1)
cy.visit('/apps/files')
files.forEach((file) => {
getRowForFile(file).should('be.visible')
})
selectRowForFile('file2.txt')
selectRowForFile('file4.txt')
triggerTagManagementDialogAction()
cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5)
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
const tag = Object.keys(tags)[3]
cy.get(`[data-cy-systemtags-picker-tag=${tags[tag]}]`).should('be.visible')
.findByRole('checkbox').click({ force: true })
cy.get('[data-cy-systemtags-picker-button-submit]').click()
cy.wait('@getTagData')
cy.wait('@assignTagData')
cy.get('[data-cy-systemtags-picker]').should('not.exist')
expectInlineTagForFile('file2.txt', [tag])
expectInlineTagForFile('file4.txt', [tag])
})
it('Can assign multiple tags to selection', () => {
cy.login(user1)
cy.visit('/apps/files')
files.forEach((file) => {
getRowForFile(file).should('be.visible')
})
selectAllFiles()
triggerTagManagementDialogAction()
cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5)
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
const prevTag = Object.keys(tags)[3]
const tag1 = Object.keys(tags)[1]
const tag2 = Object.keys(tags)[2]
cy.get(`[data-cy-systemtags-picker-tag=${tags[tag1]}]`).should('be.visible')
.findByRole('checkbox').click({ force: true })
cy.get(`[data-cy-systemtags-picker-tag=${tags[tag2]}]`).should('be.visible')
.findByRole('checkbox').click({ force: true })
cy.get('[data-cy-systemtags-picker-button-submit]').click()
cy.wait('@getTagData')
cy.wait('@assignTagData')
cy.get('@getTagData.all').should('have.length', 2)
cy.get('@assignTagData.all').should('have.length', 2)
cy.get('[data-cy-systemtags-picker]').should('not.exist')
expectInlineTagForFile('file1.txt', [tag1, tag2])
expectInlineTagForFile('file2.txt', [prevTag, tag1, tag2])
expectInlineTagForFile('file3.txt', [tag1, tag2])
expectInlineTagForFile('file4.txt', [prevTag, tag1, tag2])
expectInlineTagForFile('file5.txt', [tag1, tag2])
})
it('Can remove tag from selection', () => {
cy.login(user1)
cy.visit('/apps/files')
files.forEach((file) => {
getRowForFile(file).should('be.visible')
})
selectRowForFile('file1.txt')
selectRowForFile('file3.txt')
selectRowForFile('file4.txt')
triggerTagManagementDialogAction()
cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5)
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
const firstTag = Object.keys(tags)[3]
const tag1 = Object.keys(tags)[1]
const tag2 = Object.keys(tags)[2]
cy.get(`[data-cy-systemtags-picker-tag=${tags[tag2]}]`).should('be.visible')
.findByRole('checkbox').click({ force: true })
cy.get('[data-cy-systemtags-picker-button-submit]').click()
cy.wait('@getTagData')
cy.wait('@assignTagData')
cy.get('[data-cy-systemtags-picker]').should('not.exist')
expectInlineTagForFile('file1.txt', [tag1])
expectInlineTagForFile('file2.txt', [firstTag, tag1, tag2])
expectInlineTagForFile('file3.txt', [tag1])
expectInlineTagForFile('file4.txt', [firstTag, tag1])
expectInlineTagForFile('file5.txt', [tag1, tag2])
})
it('Can remove multiple tags from selection', () => {
cy.login(user1)
cy.visit('/apps/files')
files.forEach((file) => {
getRowForFile(file).should('be.visible')
})
selectAllFiles()
triggerTagManagementDialogAction()
cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5)
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
cy.get('[data-cy-systemtags-picker-tag] input:indeterminate').should('exist')
.click({ force: true, multiple: true })
// indeterminate became checked
cy.get('[data-cy-systemtags-picker-tag] input:checked').should('exist')
.click({ force: true, multiple: true })
// now all are unchecked
cy.get('[data-cy-systemtags-picker-button-submit]').click()
cy.wait('@getTagData')
cy.wait('@assignTagData')
cy.get('@getTagData.all').should('have.length', 3)
cy.get('@assignTagData.all').should('have.length', 3)
cy.get('[data-cy-systemtags-picker]').should('not.exist')
expectInlineTagForFile('file1.txt', [])
expectInlineTagForFile('file2.txt', [])
expectInlineTagForFile('file3.txt', [])
expectInlineTagForFile('file4.txt', [])
expectInlineTagForFile('file5.txt', [])
})
it('Can assign and remove multiple tags as a secondary user', () => {
// Create new users
cy.createRandomUser().then((_user1) => {
user1 = _user1
cy.createRandomUser().then((_user2) => {
user2 = _user2
})
files.forEach((file) => {
cy.uploadContent(user1, new Blob([]), 'text/plain', '/' + file)
})
})
cy.login(user1)
cy.visit('/apps/files')
files.forEach((file) => {
getRowForFile(file).should('be.visible')
})
selectAllFiles()
triggerTagManagementDialogAction()
cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5)
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData1')
cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData1')
const tag1 = Object.keys(tags)[0]
const tag2 = Object.keys(tags)[3]
cy.get(`[data-cy-systemtags-picker-tag=${tags[tag1]}]`).should('be.visible')
.findByRole('checkbox').click({ force: true })
cy.get(`[data-cy-systemtags-picker-tag=${tags[tag2]}]`).should('be.visible')
.findByRole('checkbox').click({ force: true })
cy.get('[data-cy-systemtags-picker-button-submit]').click()
cy.wait('@getTagData1')
cy.wait('@assignTagData1')
cy.get('@getTagData1.all').should('have.length', 2)
cy.get('@assignTagData1.all').should('have.length', 2)
cy.get('[data-cy-systemtags-picker]').should('not.exist')
expectInlineTagForFile('file1.txt', [tag1, tag2])
expectInlineTagForFile('file2.txt', [tag1, tag2])
expectInlineTagForFile('file3.txt', [tag1, tag2])
expectInlineTagForFile('file4.txt', [tag1, tag2])
expectInlineTagForFile('file5.txt', [tag1, tag2])
createShare('file1.txt', user2.userId)
createShare('file3.txt', user2.userId)
cy.login(user2)
cy.visit('/apps/files')
getRowForFile('file1.txt').should('be.visible')
getRowForFile('file3.txt').should('be.visible')
expectInlineTagForFile('file1.txt', [tag1, tag2])
expectInlineTagForFile('file3.txt', [tag1, tag2])
selectRowForFile('file1.txt')
selectRowForFile('file3.txt')
triggerTagManagementDialogAction()
cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5)
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData2')
cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData2')
cy.get(`[data-cy-systemtags-picker-tag=${tags[tag1]}]`).should('be.visible')
.findByRole('checkbox').click({ force: true })
cy.get(`[data-cy-systemtags-picker-tag=${tags[tag2]}]`).should('be.visible')
.findByRole('checkbox').click({ force: true })
cy.get('[data-cy-systemtags-picker-button-submit]').click()
cy.wait('@getTagData2')
cy.wait('@assignTagData2')
cy.get('@getTagData2.all').should('have.length', 2)
cy.get('@assignTagData2.all').should('have.length', 2)
cy.get('[data-cy-systemtags-picker]').should('not.exist')
expectInlineTagForFile('file1.txt', [])
expectInlineTagForFile('file3.txt', [])
cy.login(user1)
cy.visit('/apps/files')
expectInlineTagForFile('file1.txt', [])
expectInlineTagForFile('file3.txt', [])
})
it('Can create tag and assign files to it', () => {
cy.createRandomUser().then((user1) => {
files.forEach((file) => {
cy.uploadContent(user1, new Blob([]), 'text/plain', '/' + file)
})
cy.login(user1)
cy.visit('/apps/files')
files.forEach((file) => {
getRowForFile(file).should('be.visible')
})
selectAllFiles()
triggerTagManagementDialogAction()
cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 5)
cy.intercept('POST', '/remote.php/dav/systemtags').as('createTag')
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
const newTag = randomBytes(8).toString('base64').slice(0, 6)
cy.get('[data-cy-systemtags-picker-input]').type(newTag)
cy.get('[data-cy-systemtags-picker-input-submit]').click()
cy.wait('@createTag')
cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 6)
// Verify the new tag is selected by default
cy.get('[data-cy-systemtags-picker-tag]').contains(newTag)
.parents('[data-cy-systemtags-picker-tag]')
.findByRole('checkbox', { hidden: true }).should('be.checked')
// Apply changes
cy.get('[data-cy-systemtags-picker-button-submit]').click()
cy.wait('@getTagData')
cy.wait('@assignTagData')
cy.get('@getTagData.all').should('have.length', 1)
cy.get('@assignTagData.all').should('have.length', 1)
cy.get('[data-cy-systemtags-picker]').should('not.exist')
expectInlineTagForFile('file1.txt', [newTag])
expectInlineTagForFile('file2.txt', [newTag])
expectInlineTagForFile('file3.txt', [newTag])
expectInlineTagForFile('file4.txt', [newTag])
expectInlineTagForFile('file5.txt', [newTag])
})
})
})

View file

@ -204,7 +204,7 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper {
/**
* Update the etag for the given tags.
*
* @param int[] $tagIds
* @param string[] $tagIds
*/
private function updateEtagForTags(array $tagIds): void {
// Update etag after assigning tags

View file

@ -129,7 +129,7 @@ interface ISystemTagObjectMapper {
* @param string $tagId tag id
* @param string $objectType object type
* @param string[] $objectIds list of object ids
*
*
* @throws TagNotFoundException if the tag does not exist
* @since 31.0.0
*/