mirror of
https://github.com/nextcloud/server.git
synced 2026-06-11 09:42:09 -04:00
test(systemtags): migrate from Cypress to PlayWright
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
2b7415ef4d
commit
ab96b85ac6
16 changed files with 576 additions and 647 deletions
4
.github/workflows/cypress.yml
vendored
4
.github/workflows/cypress.yml
vendored
|
|
@ -105,10 +105,10 @@ jobs:
|
|||
matrix:
|
||||
# Run multiple copies of the current job in parallel
|
||||
# Please increase the number or runners as your tests suite grows (0 based index for e2e tests)
|
||||
containers: ['setup', '0', '1', '2', '3', '4', '5', '6', '7']
|
||||
containers: ['setup', '0', '1', '2', '3', '4', '5', '6']
|
||||
# Hack as strategy.job-total includes the "setup" and GitHub does not allow math expressions
|
||||
# Always align this number with the total of e2e runners (max. index + 1)
|
||||
total-containers: [8]
|
||||
total-containers: [7]
|
||||
|
||||
services:
|
||||
mysql:
|
||||
|
|
|
|||
|
|
@ -1,468 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
|
||||
import { randomBytes } from 'crypto'
|
||||
import { getRowForFile, selectAllFiles, selectRowForFile, triggerSelectionAction } from '../files/FilesUtils.ts'
|
||||
import { createShare } from '../files_sharing/FilesSharingUtils.ts'
|
||||
|
||||
let tags = {} as Record<string, number>
|
||||
const files = [
|
||||
'file1.txt',
|
||||
'file2.txt',
|
||||
'file3.txt',
|
||||
'file4.txt',
|
||||
'file5.txt',
|
||||
]
|
||||
|
||||
describe('Systemtags: Files bulk action', { testIsolation: false }, () => {
|
||||
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()
|
||||
})
|
||||
|
||||
after(() => {
|
||||
resetTags()
|
||||
cy.runOccCommand('config:app:set systemtags restrict_creation_to_admin --value 0')
|
||||
})
|
||||
|
||||
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.get('[data-cy-systemtags-picker-tag-color]').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.get('[data-cy-systemtags-picker-tag-color]').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-tag]').should('have.length', 0)
|
||||
cy.get('[data-cy-systemtags-picker-button-create]').should('be.visible')
|
||||
cy.get('[data-cy-systemtags-picker-button-create]').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])
|
||||
})
|
||||
})
|
||||
|
||||
it('Cannot create tag if restriction is in place', () => {
|
||||
let tagId: string
|
||||
|
||||
cy.runOccCommand('config:app:set systemtags restrict_creation_to_admin --value 1')
|
||||
cy.runOccCommand('tag:add testTag public --output json').then(({ stdout }) => {
|
||||
const tag = JSON.parse(stdout)
|
||||
tagId = tag.id
|
||||
})
|
||||
|
||||
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.findByRole('textbox', { name: 'Search or create tag' }).should('not.exist')
|
||||
cy.findByRole('textbox', { name: 'Search tag' }).should('be.visible')
|
||||
|
||||
cy.get('[data-cy-systemtags-picker-input]').type('testTag')
|
||||
|
||||
cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 1)
|
||||
cy.get('[data-cy-systemtags-picker-button-create]').should('not.exist')
|
||||
cy.get('[data-cy-systemtags-picker-tag-color]').should('not.exist')
|
||||
|
||||
// Assign the tag
|
||||
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="${tagId}"]`).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')
|
||||
|
||||
// Finally, reset the restriction
|
||||
cy.runOccCommand('config:app:set systemtags restrict_creation_to_admin --value 0')
|
||||
})
|
||||
})
|
||||
|
||||
it('Can search for tags with insensitive case', () => {
|
||||
let tagId: string
|
||||
resetTags()
|
||||
|
||||
cy.runOccCommand('tag:add TESTTAG public --output json').then(({ stdout }) => {
|
||||
const tag = JSON.parse(stdout)
|
||||
tagId = tag.id
|
||||
})
|
||||
|
||||
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.findByRole('textbox', { name: 'Search or create tag' }).should('be.visible')
|
||||
cy.findByRole('textbox', { name: 'Search tag' }).should('not.exist')
|
||||
|
||||
cy.get('[data-cy-systemtags-picker-input]').type('testtag')
|
||||
|
||||
cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 1)
|
||||
cy.get(`[data-cy-systemtags-picker-tag="${tagId}"]`).should('be.visible')
|
||||
.findByRole('checkbox').should('not.be.checked')
|
||||
|
||||
// Assign the tag
|
||||
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="${tagId}"]`).should('be.visible')
|
||||
.findByRole('checkbox').click({ force: true })
|
||||
cy.get('[data-cy-systemtags-picker-button-submit]').click()
|
||||
|
||||
cy.wait('@getTagData')
|
||||
cy.wait('@assignTagData')
|
||||
|
||||
expectInlineTagForFile('file1.txt', ['TESTTAG'])
|
||||
expectInlineTagForFile('file2.txt', ['TESTTAG'])
|
||||
expectInlineTagForFile('file3.txt', ['TESTTAG'])
|
||||
expectInlineTagForFile('file4.txt', ['TESTTAG'])
|
||||
expectInlineTagForFile('file5.txt', ['TESTTAG'])
|
||||
|
||||
cy.get('[data-cy-systemtags-picker]').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function resetTags() {
|
||||
tags = {}
|
||||
for (let i = 0; i < 5; i++) {
|
||||
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')
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
|
||||
import { randomBytes } from 'crypto'
|
||||
import { getRowForFile } from '../files/FilesUtils.ts'
|
||||
import { addTagToFile } from './utils.ts'
|
||||
|
||||
describe('Systemtags: Files integration', { testIsolation: true }, () => {
|
||||
let user: User
|
||||
|
||||
beforeEach(() => cy.createRandomUser().then(($user) => {
|
||||
user = $user
|
||||
|
||||
cy.mkdir(user, '/folder')
|
||||
cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt')
|
||||
cy.login(user)
|
||||
cy.visit('/apps/files')
|
||||
}))
|
||||
|
||||
it('See first assigned tag in the file list', () => {
|
||||
const tag = randomBytes(8).toString('base64')
|
||||
addTagToFile('file.txt', tag)
|
||||
cy.reload()
|
||||
|
||||
getRowForFile('file.txt')
|
||||
.findByRole('list', { name: /collaborative tags/i })
|
||||
.findByRole('listitem')
|
||||
.should('be.visible')
|
||||
.and('contain.text', tag)
|
||||
})
|
||||
|
||||
it('See two assigned tags are also shown in the file list', () => {
|
||||
const tag1 = randomBytes(5).toString('base64')
|
||||
const tag2 = randomBytes(5).toString('base64')
|
||||
addTagToFile('file.txt', tag1)
|
||||
addTagToFile('file.txt', tag2)
|
||||
cy.reload()
|
||||
|
||||
getRowForFile('file.txt')
|
||||
.findByRole('list', { name: /collaborative tags/i })
|
||||
.children()
|
||||
.should('have.length', 2)
|
||||
.should('contain.text', tag1)
|
||||
.should('contain.text', tag2)
|
||||
})
|
||||
|
||||
it('See three assigned tags result in overflow entry', () => {
|
||||
const tag1 = randomBytes(4).toString('base64')
|
||||
const tag2 = randomBytes(4).toString('base64')
|
||||
const tag3 = randomBytes(4).toString('base64')
|
||||
addTagToFile('file.txt', tag1)
|
||||
addTagToFile('file.txt', tag2)
|
||||
addTagToFile('file.txt', tag3)
|
||||
cy.reload()
|
||||
|
||||
getRowForFile('file.txt')
|
||||
.findByRole('list', { name: /collaborative tags/i })
|
||||
.children()
|
||||
.then(($children) => {
|
||||
expect($children.length).to.eq(4)
|
||||
expect($children.get(0)).be.visible
|
||||
expect($children.get(1)).be.visible
|
||||
// not visible - just for accessibility
|
||||
expect($children.get(2)).not.be.visible
|
||||
expect($children.get(3)).not.be.visible
|
||||
// Text content
|
||||
expect($children.get(1)).contain.text('+2')
|
||||
// Remove the '+x' element
|
||||
const elements = [$children.get(0), ...$children.get().slice(2)]
|
||||
.map((el) => el.innerText.trim())
|
||||
expect(elements).to.have.members([tag1, tag2, tag3])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
|
||||
import { randomBytes } from 'crypto'
|
||||
import { getRowForFile, triggerActionForFile } from '../files/FilesUtils.ts'
|
||||
import { createNewTagInDialog } from './utils.ts'
|
||||
|
||||
describe('Systemtags: Files sidebar integration', { testIsolation: true }, () => {
|
||||
let user: User
|
||||
|
||||
beforeEach(() => cy.createRandomUser().then(($user) => {
|
||||
user = $user
|
||||
|
||||
cy.mkdir(user, '/folder')
|
||||
cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt')
|
||||
cy.login(user)
|
||||
}))
|
||||
|
||||
it('Can assign tags using the sidebar', () => {
|
||||
const tag = randomBytes(8).toString('base64')
|
||||
cy.visit('/apps/files')
|
||||
|
||||
getRowForFile('file.txt').should('be.visible')
|
||||
triggerActionForFile('file.txt', 'details')
|
||||
|
||||
cy.get('[data-cy-sidebar]')
|
||||
.should('be.visible')
|
||||
.findByRole('button', { name: 'Actions' })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
cy.findByRole('menuitem', { name: 'Add tags' })
|
||||
.click()
|
||||
|
||||
createNewTagInDialog(tag)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
|
||||
import { randomBytes } from 'crypto'
|
||||
import { getRowForFile } from '../files/FilesUtils.ts'
|
||||
import { addTagToFile } from './utils.ts'
|
||||
|
||||
describe('Systemtags: Files view', { testIsolation: true }, () => {
|
||||
let user: User
|
||||
|
||||
beforeEach(() => cy.createRandomUser().then(($user) => {
|
||||
user = $user
|
||||
|
||||
cy.mkdir(user, '/folder')
|
||||
cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt')
|
||||
cy.login(user)
|
||||
cy.visit('/apps/files')
|
||||
}))
|
||||
|
||||
it('See first assigned tag in the file list', () => {
|
||||
const tag = randomBytes(8).toString('base64')
|
||||
addTagToFile('folder', tag)
|
||||
|
||||
// open the tags view
|
||||
cy.visit('/apps/files/tags').then(() => {
|
||||
// see the tag
|
||||
getRowForFile('folder').should('not.exist')
|
||||
getRowForFile('file.txt').should('not.exist')
|
||||
cy.findByRole('cell', { name: tag })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
// see that the tag has its content
|
||||
getRowForFile('folder').should('be.visible')
|
||||
getRowForFile('file.txt').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { runOcc } from '@nextcloud/e2e-test-server'
|
||||
import { expect } from '@playwright/test'
|
||||
import { test } from '../../support/fixtures/systemtags-files-page.ts'
|
||||
import { uploadContent } from '../../support/utils/dav.ts'
|
||||
import { clearTags, createTag } from '../../support/utils/systemtags.ts'
|
||||
|
||||
test.beforeAll(async () => await runOcc(['config:app:set', 'systemtags', 'restrict_creation_to_admin', '--value', '1']))
|
||||
test.afterAll(async () => await runOcc(['config:app:delete', 'systemtags', 'restrict_creation_to_admin']))
|
||||
test.afterAll(async () => await clearTags())
|
||||
|
||||
test.beforeEach(async ({ filesListPage, page, user }) => {
|
||||
await uploadContent(page.request, user, Buffer.alloc(0), 'text/plain', '/file1.txt')
|
||||
await uploadContent(page.request, user, Buffer.alloc(0), 'text/plain', '/file2.txt')
|
||||
await filesListPage.open()
|
||||
})
|
||||
|
||||
test('Cannot create tag if restriction is in place', async ({ filesListPage }) => {
|
||||
const tag = crypto.randomUUID()
|
||||
await createTag(tag, 'public')
|
||||
|
||||
await filesListPage.expectInlineTagsForFile('file1.txt', [])
|
||||
await filesListPage.selectAll()
|
||||
const picker = await filesListPage.openTagPickerForSelection()
|
||||
|
||||
// When restricted, the input label changes and create/color buttons are absent
|
||||
await expect(picker.getByLabel('Search or create tag')).toHaveCount(0)
|
||||
await expect(picker.getByLabel('Search tag')).toBeVisible()
|
||||
|
||||
await picker.getByLabel('Search tag').fill(crypto.randomUUID())
|
||||
await expect(picker.getByRole('button', { name: /Create new tag/i })).toHaveCount(0)
|
||||
|
||||
await picker.getByLabel('Search tag').clear()
|
||||
await picker.getByLabel('Search tag').fill(tag)
|
||||
|
||||
await expect(picker.getByRole('checkbox')).toHaveCount(1)
|
||||
await expect(picker.getByRole('button', { name: /Create new tag/i })).toHaveCount(0)
|
||||
await expect(picker.getByRole('button', { name: 'Change tag color' })).toHaveCount(0)
|
||||
|
||||
// Can still assign the existing admin-created tag
|
||||
await picker.getByRole('checkbox', { name: tag }).click({ force: true })
|
||||
await filesListPage.applyTagPicker()
|
||||
await filesListPage.expectInlineTagsForFile('file1.txt', [tag])
|
||||
})
|
||||
|
|
@ -6,19 +6,17 @@
|
|||
import { expect } from '@playwright/test'
|
||||
import { runOcc } from '@nextcloud/e2e-test-server/docker'
|
||||
import { test } from '../../support/fixtures/admin-session.ts'
|
||||
import { createTag, deleteTag, listTags } from '../../support/utils/systemtags.ts'
|
||||
|
||||
const tagName = 'foo'
|
||||
const updatedTagName = 'bar'
|
||||
|
||||
test.describe('System tags admin settings', () => {
|
||||
// Tests are sequential: update depends on create, delete depends on update
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
test.beforeAll(async () => {
|
||||
// Delete all existing tags so each test run starts from a clean state
|
||||
const output = await runOcc(['tag:list', '--output=json'])
|
||||
const tags = JSON.parse(output) as Record<string, unknown>
|
||||
await Promise.all(Object.keys(tags).map((id) => runOcc(['tag:delete', id]).catch(() => {})))
|
||||
test.beforeEach(async () => {
|
||||
const tags = await listTags()
|
||||
for (const tag of tags) {
|
||||
await deleteTag(tag.id)
|
||||
}
|
||||
})
|
||||
|
||||
test('Can create a tag', async ({ page }) => {
|
||||
|
|
@ -44,6 +42,8 @@ test.describe('System tags admin settings', () => {
|
|||
})
|
||||
|
||||
test('Can update a tag', async ({ page }) => {
|
||||
const tag = await createTag(tagName)
|
||||
|
||||
await page.goto('settings/admin/server')
|
||||
await page.getByRole('heading', { name: 'Collaborative tags' }).scrollIntoViewIfNeeded()
|
||||
|
||||
|
|
@ -69,24 +69,25 @@ test.describe('System tags admin settings', () => {
|
|||
await page.getByRole('button', { name: 'Update' }).click()
|
||||
expect((await updateResponse).status()).toBe(207)
|
||||
|
||||
await page.getByRole('combobox', { name: 'Search for a tag to edit' }).click()
|
||||
// NcEllipsisedOption splits names ≥ 10 chars across two spans, breaking the accessible name.
|
||||
// "bar (invisible)" (15 chars) splits at position 8 → accessible name "bar (inv isible)".
|
||||
// Use filter({ hasText }) to match on text content instead of the exact accessible name.
|
||||
await page.getByRole('combobox', { name: 'Search for a tag to edit' }).click()
|
||||
await expect(page.getByRole('option').filter({ hasText: updatedTagName })).toBeVisible()
|
||||
})
|
||||
|
||||
test('Can delete a tag', async ({ page }) => {
|
||||
await createTag(tagName)
|
||||
|
||||
await page.goto('settings/admin/server')
|
||||
await page.getByRole('heading', { name: 'Collaborative tags' }).scrollIntoViewIfNeeded()
|
||||
|
||||
// Select the invisible tag to delete
|
||||
await page.getByRole('combobox', { name: 'Search for a tag to edit' }).click()
|
||||
await page.getByRole('option').filter({ hasText: updatedTagName }).click()
|
||||
await page.getByRole('option').filter({ hasText: tagName }).click()
|
||||
|
||||
// Verify the form reflects the selected tag
|
||||
await expect(page.getByLabel('Tag name')).toHaveValue(updatedTagName)
|
||||
await expect(page.locator('.system-tag-form__group:has(#system-tag-level) .vs__selected')).toContainText('Invisible')
|
||||
await expect(page.getByLabel('Tag name')).toHaveValue(tagName)
|
||||
|
||||
const deleteResponse = page.waitForResponse(
|
||||
(r) => r.url().includes('/remote.php/dav/systemtags/') && r.request().method() === 'DELETE',
|
||||
|
|
@ -96,6 +97,6 @@ test.describe('System tags admin settings', () => {
|
|||
|
||||
// Verify the tag is gone from the dropdown
|
||||
await page.getByRole('combobox', { name: 'Search for a tag to edit' }).click()
|
||||
await expect(page.getByRole('option').filter({ hasText: updatedTagName })).not.toBeVisible()
|
||||
await expect(page.getByRole('option').filter({ hasText: tagName })).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
153
tests/playwright/e2e/systemtags/files-bulk-action.spec.ts
Normal file
153
tests/playwright/e2e/systemtags/files-bulk-action.spec.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { test as baseTest } from '../../support/fixtures/systemtags-files-page.ts'
|
||||
import { uploadContent } from '../../support/utils/dav.ts'
|
||||
import { assignTagsToFile, clearTags, createTag } from '../../support/utils/systemtags.ts'
|
||||
|
||||
// Extends the base fixture with per-test file IDs so tests in parallel each get
|
||||
// their own isolated file IDs rather than sharing module-level mutable state.
|
||||
const test = baseTest.extend<{ fileIds: [string, string] }>({
|
||||
fileIds: [async ({ page, user, filesListPage }, use) => {
|
||||
const fileId1 = await uploadContent(page.request, user, Buffer.alloc(0), 'text/plain', '/file1.txt')
|
||||
const fileId2 = await uploadContent(page.request, user, Buffer.alloc(0), 'text/plain', '/file2.txt')
|
||||
await filesListPage.open()
|
||||
await use([fileId1, fileId2])
|
||||
}, { auto: true }],
|
||||
})
|
||||
|
||||
test.describe('Systemtags: Files bulk action', () => {
|
||||
test.afterAll(async () => await clearTags())
|
||||
test.beforeEach(async ({ fileIds }) => {
|
||||
console.debug('Created files with IDs', fileIds)
|
||||
})
|
||||
|
||||
test('Can assign tag to selection', async ({ filesListPage, fileIds }) => {
|
||||
const tag = crypto.randomUUID()
|
||||
|
||||
await filesListPage.expectInlineTagsForFile('file1.txt', [])
|
||||
await filesListPage.expectInlineTagsForFile('file2.txt', [])
|
||||
|
||||
await filesListPage.selectRowForFile('file1.txt')
|
||||
await filesListPage.selectRowForFile('file2.txt')
|
||||
|
||||
await filesListPage.openTagPickerForSelection()
|
||||
await filesListPage.createNewTagInPicker(tag)
|
||||
await filesListPage.applyTagPicker()
|
||||
|
||||
await filesListPage.expectInlineTagsForFile('file1.txt', [tag])
|
||||
await filesListPage.expectInlineTagsForFile('file2.txt', [tag])
|
||||
})
|
||||
|
||||
test('Can assign multiple tags to selection', async ({ filesListPage }) => {
|
||||
const tag1 = crypto.randomUUID()
|
||||
const tag2 = crypto.randomUUID()
|
||||
|
||||
await filesListPage.expectInlineTagsForFile('file1.txt', [])
|
||||
await filesListPage.expectInlineTagsForFile('file2.txt', [])
|
||||
|
||||
await filesListPage.selectRowForFile('file1.txt')
|
||||
await filesListPage.selectRowForFile('file2.txt')
|
||||
|
||||
await filesListPage.openTagPickerForSelection()
|
||||
await filesListPage.createNewTagInPicker(tag1)
|
||||
await filesListPage.createNewTagInPicker(tag2)
|
||||
await filesListPage.applyTagPicker()
|
||||
|
||||
await filesListPage.expectInlineTagsForFile('file1.txt', [tag1, tag2])
|
||||
await filesListPage.expectInlineTagsForFile('file2.txt', [tag1, tag2])
|
||||
})
|
||||
|
||||
test('Can remove tag from selection', async ({ filesListPage, page, fileIds }) => {
|
||||
const tag1 = crypto.randomUUID()
|
||||
const tag2 = crypto.randomUUID()
|
||||
await assignTagsToFile(fileIds[0], [tag1, tag2])
|
||||
await assignTagsToFile(fileIds[1], [tag1, tag2])
|
||||
await page.reload()
|
||||
|
||||
await filesListPage.expectInlineTagsForFile('file1.txt', [tag1, tag2])
|
||||
await filesListPage.expectInlineTagsForFile('file2.txt', [tag1, tag2])
|
||||
|
||||
await filesListPage.selectRowForFile('file1.txt')
|
||||
await filesListPage.selectRowForFile('file2.txt')
|
||||
|
||||
await filesListPage.openTagPickerForSelection()
|
||||
await filesListPage.getTagPicker().getByRole('checkbox', { name: tag2 })
|
||||
.uncheck({ force: true })
|
||||
await filesListPage.applyTagPicker()
|
||||
|
||||
await filesListPage.expectInlineTagsForFile('file1.txt', [tag1])
|
||||
await filesListPage.expectInlineTagsForFile('file2.txt', [tag1])
|
||||
})
|
||||
|
||||
test('Can remove multiple tags from selection', async ({ filesListPage, page, fileIds }) => {
|
||||
const tag1 = crypto.randomUUID()
|
||||
const tag2 = crypto.randomUUID()
|
||||
const tag3 = crypto.randomUUID()
|
||||
await assignTagsToFile(fileIds[0], [tag1, tag2, tag3])
|
||||
await assignTagsToFile(fileIds[1], [tag1, tag2, tag3])
|
||||
await page.reload()
|
||||
|
||||
await filesListPage.expectInlineTagsForFile('file1.txt', [tag1, tag2, tag3])
|
||||
await filesListPage.expectInlineTagsForFile('file2.txt', [tag1, tag2, tag3])
|
||||
|
||||
await filesListPage.selectRowForFile('file1.txt')
|
||||
await filesListPage.selectRowForFile('file2.txt')
|
||||
|
||||
await filesListPage.openTagPickerForSelection()
|
||||
await filesListPage.getTagPicker().getByRole('checkbox', { name: tag2 })
|
||||
.uncheck({ force: true })
|
||||
await filesListPage.getTagPicker().getByRole('checkbox', { name: tag3 })
|
||||
.uncheck({ force: true })
|
||||
await filesListPage.applyTagPicker()
|
||||
|
||||
await filesListPage.expectInlineTagsForFile('file1.txt', [tag1])
|
||||
await filesListPage.expectInlineTagsForFile('file2.txt', [tag1])
|
||||
})
|
||||
|
||||
test('Can assign and remove multiple tags', async ({ filesListPage, page, fileIds }) => {
|
||||
const tag1 = crypto.randomUUID()
|
||||
const tag2 = crypto.randomUUID()
|
||||
const tag3 = crypto.randomUUID()
|
||||
await assignTagsToFile(fileIds[0], [tag1, tag2])
|
||||
await assignTagsToFile(fileIds[1], [tag1, tag2])
|
||||
await page.reload()
|
||||
|
||||
await filesListPage.expectInlineTagsForFile('file1.txt', [tag1, tag2])
|
||||
await filesListPage.expectInlineTagsForFile('file2.txt', [tag1, tag2])
|
||||
|
||||
await filesListPage.selectRowForFile('file1.txt')
|
||||
await filesListPage.selectRowForFile('file2.txt')
|
||||
|
||||
await filesListPage.openTagPickerForSelection()
|
||||
await filesListPage.getTagPicker().getByRole('checkbox', { name: tag2 })
|
||||
.scrollIntoViewIfNeeded()
|
||||
await filesListPage.getTagPicker().getByRole('checkbox', { name: tag2 })
|
||||
.uncheck({ force: true })
|
||||
await filesListPage.createNewTagInPicker(tag3)
|
||||
await filesListPage.applyTagPicker()
|
||||
|
||||
await filesListPage.expectInlineTagsForFile('file1.txt', [tag1, tag3])
|
||||
await filesListPage.expectInlineTagsForFile('file2.txt', [tag1, tag3])
|
||||
})
|
||||
|
||||
test('Can search for tags with insensitive case', async ({ filesListPage }) => {
|
||||
const tag = crypto.randomUUID().toLowerCase()
|
||||
await createTag(tag, 'public')
|
||||
|
||||
await filesListPage.expectInlineTagsForFile('file1.txt', [])
|
||||
await filesListPage.expectInlineTagsForFile('file2.txt', [])
|
||||
|
||||
await filesListPage.selectRowForFile('file1.txt')
|
||||
await filesListPage.selectRowForFile('file2.txt')
|
||||
|
||||
await filesListPage.openTagPickerForSelection()
|
||||
await filesListPage.selectTagInPicker(tag.toUpperCase())
|
||||
await filesListPage.applyTagPicker()
|
||||
|
||||
await filesListPage.expectInlineTagsForFile('file1.txt', [tag])
|
||||
await filesListPage.expectInlineTagsForFile('file2.txt', [tag])
|
||||
})
|
||||
})
|
||||
74
tests/playwright/e2e/systemtags/files-inline-action.spec.ts
Normal file
74
tests/playwright/e2e/systemtags/files-inline-action.spec.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { test, expect } from '../../support/fixtures/systemtags-files-page.ts'
|
||||
import { uploadContent } from '../../support/utils/dav.ts'
|
||||
import { clearTags } from '../../support/utils/systemtags.ts'
|
||||
|
||||
test.describe('Systemtags: Files integration', () => {
|
||||
test.afterAll(async () => await clearTags())
|
||||
|
||||
test.beforeEach(async ({ page, user, filesListPage }) => {
|
||||
await uploadContent(page.request, user, Buffer.alloc(0), 'text/plain', '/file.txt')
|
||||
await filesListPage.open()
|
||||
})
|
||||
|
||||
test('See first assigned tag in the file list', async ({ page, filesListPage }) => {
|
||||
const tag = crypto.randomUUID()
|
||||
|
||||
await filesListPage.openTagPickerForFile('file.txt')
|
||||
await filesListPage.createNewTagInPicker(tag)
|
||||
await filesListPage.applyTagPicker()
|
||||
await page.reload()
|
||||
|
||||
const tagList = filesListPage.getInlineTagsForFile('file.txt')
|
||||
await expect(tagList.getByRole('listitem')).toHaveCount(1)
|
||||
await expect(tagList.getByRole('listitem')).toBeVisible()
|
||||
await expect(tagList.getByRole('listitem')).toContainText(tag)
|
||||
})
|
||||
|
||||
test('See two assigned tags are also shown in the file list', async ({ page, filesListPage }) => {
|
||||
const tag1 = crypto.randomUUID()
|
||||
const tag2 = crypto.randomUUID()
|
||||
|
||||
await filesListPage.openTagPickerForFile('file.txt')
|
||||
await filesListPage.createNewTagInPicker(tag1)
|
||||
await filesListPage.createNewTagInPicker(tag2)
|
||||
await filesListPage.applyTagPicker()
|
||||
await page.reload()
|
||||
|
||||
const tagList = filesListPage.getInlineTagsForFile('file.txt')
|
||||
// 2 tags, no overflow — both li elements are visible
|
||||
await expect(tagList.locator('li')).toHaveCount(2)
|
||||
await expect(tagList).toContainText(tag1)
|
||||
await expect(tagList).toContainText(tag2)
|
||||
})
|
||||
|
||||
test('See three assigned tags result in overflow entry', async ({ page, filesListPage }) => {
|
||||
const tag1 = crypto.randomUUID()
|
||||
const tag2 = crypto.randomUUID()
|
||||
const tag3 = crypto.randomUUID()
|
||||
|
||||
await filesListPage.openTagPickerForFile('file.txt')
|
||||
await filesListPage.createNewTagInPicker(tag1)
|
||||
await filesListPage.createNewTagInPicker(tag2)
|
||||
await filesListPage.createNewTagInPicker(tag3)
|
||||
await filesListPage.applyTagPicker()
|
||||
await page.reload()
|
||||
|
||||
const tagList = filesListPage.getInlineTagsForFile('file.txt')
|
||||
// 3 tags with overflow: 1 visible + "+2" (aria-hidden, role=presentation) + 2 hidden-visually = 4 li elements
|
||||
await expect(tagList.locator('li')).toHaveCount(4)
|
||||
|
||||
// First li is the visible tag; second li is the aria-hidden overflow indicator
|
||||
await expect(tagList.locator('li').first()).toBeVisible()
|
||||
await expect(tagList.locator('li').nth(1)).toContainText('+2')
|
||||
|
||||
// All 3 tag names are present in the list (1 visible, 2 hidden-visually)
|
||||
await expect(tagList).toContainText(tag1)
|
||||
await expect(tagList).toContainText(tag2)
|
||||
await expect(tagList).toContainText(tag3)
|
||||
})
|
||||
})
|
||||
35
tests/playwright/e2e/systemtags/files-sidebar.spec.ts
Normal file
35
tests/playwright/e2e/systemtags/files-sidebar.spec.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { test, expect } from '../../support/fixtures/systemtags-files-page.ts'
|
||||
import { uploadContent } from '../../support/utils/dav.ts'
|
||||
import { clearTags } from '../../support/utils/systemtags.ts'
|
||||
|
||||
test.describe('Systemtags: Files sidebar integration', () => {
|
||||
test.afterAll(async () => await clearTags())
|
||||
|
||||
test.beforeEach(async ({ page, user, filesListPage }) => {
|
||||
await uploadContent(page.request, user, Buffer.alloc(0), 'text/plain', '/file.txt')
|
||||
await filesListPage.open()
|
||||
})
|
||||
|
||||
test('Can assign tags using the sidebar', async ({ filesListPage, filesSidebar }) => {
|
||||
const tag = crypto.randomUUID()
|
||||
|
||||
await expect(filesListPage.getRowForFile('file.txt')).toBeVisible()
|
||||
|
||||
// Open the file details sidebar
|
||||
await filesListPage.triggerActionForFile('file.txt', 'details')
|
||||
await expect(filesSidebar.sidebar()).toBeVisible()
|
||||
|
||||
// Open the sidebar's Actions menu and click "Add tags"
|
||||
await filesSidebar.triggerAction('Add tags')
|
||||
|
||||
// Create and apply the new tag via the picker
|
||||
await expect(filesListPage.getTagPicker()).toBeVisible()
|
||||
await filesListPage.createNewTagInPicker(tag)
|
||||
await filesListPage.applyTagPicker()
|
||||
})
|
||||
})
|
||||
43
tests/playwright/e2e/systemtags/files-view.spec.ts
Normal file
43
tests/playwright/e2e/systemtags/files-view.spec.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { test, expect } from '../../support/fixtures/systemtags-files-page.ts'
|
||||
import { mkdir, uploadContent } from '../../support/utils/dav.ts'
|
||||
import { clearTags } from '../../support/utils/systemtags.ts'
|
||||
|
||||
test.describe('Systemtags: Files view', () => {
|
||||
test.afterAll(async () => await clearTags())
|
||||
|
||||
test.beforeEach(async ({ page, user, filesListPage }) => {
|
||||
await mkdir(page.request, user, '/folder')
|
||||
await uploadContent(page.request, user, Buffer.alloc(0), 'text/plain', '/file.txt')
|
||||
await filesListPage.open()
|
||||
})
|
||||
|
||||
test('See first assigned tag in the file list', async ({ page, filesListPage }) => {
|
||||
const tag = crypto.randomUUID()
|
||||
|
||||
await expect(filesListPage.getRowForFile('folder')).toBeVisible()
|
||||
await expect(filesListPage.getRowForFile('file.txt')).toBeVisible()
|
||||
|
||||
// Assign tag to the folder via the Manage Tags picker
|
||||
await filesListPage.openTagPickerForFile('folder')
|
||||
await filesListPage.createNewTagInPicker(tag)
|
||||
await filesListPage.applyTagPicker()
|
||||
|
||||
// Navigate to the tags view
|
||||
await page.goto('apps/files/tags')
|
||||
await expect(filesListPage.getRowForFile('folder')).not.toBeVisible()
|
||||
await expect(filesListPage.getRowForFile('file.txt')).not.toBeVisible()
|
||||
|
||||
// The tag should appear as a cell in the tags list view
|
||||
await expect(page.getByRole('cell', { name: tag })).toBeVisible()
|
||||
await page.getByRole('cell', { name: tag }).click()
|
||||
|
||||
// Only the folder (tagged) is shown; file.txt (untagged) is absent
|
||||
await expect(filesListPage.getRowForFile('folder')).toBeVisible()
|
||||
await expect(filesListPage.getRowForFile('file.txt')).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
24
tests/playwright/support/fixtures/systemtags-files-page.ts
Normal file
24
tests/playwright/support/fixtures/systemtags-files-page.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { test as filesTest } from './files-page.ts'
|
||||
import { SystemTagsFilesListPage } from '../sections/SystemTagsFilesListPage.ts'
|
||||
|
||||
type SystemTagsFixtures = {
|
||||
filesListPage: SystemTagsFilesListPage
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the base files-page fixture by replacing `filesListPage` with a
|
||||
* {@link SystemTagsFilesListPage}, which adds SystemTagPicker actions and
|
||||
* inline-tags assertion helpers on top of the standard file list interactions.
|
||||
*/
|
||||
export const test = filesTest.extend<SystemTagsFixtures>({
|
||||
filesListPage: async ({ page }, use) => {
|
||||
await use(new SystemTagsFilesListPage(page))
|
||||
},
|
||||
})
|
||||
|
||||
export { expect } from '../matchers.ts'
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
export class FilesListPage {
|
||||
constructor(private readonly page: Page) {}
|
||||
constructor(protected readonly page: Page) {}
|
||||
|
||||
/**
|
||||
* Open the files app. Pass a view id (e.g. 'recent') to open that view
|
||||
|
|
|
|||
119
tests/playwright/support/sections/SystemTagsFilesListPage.ts
Normal file
119
tests/playwright/support/sections/SystemTagsFilesListPage.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Locator } from '@playwright/test'
|
||||
import { FilesListPage } from './FilesListPage.ts'
|
||||
|
||||
/**
|
||||
* Extension of {@link FilesListPage} for tests involving the SystemTags app.
|
||||
* Adds locators, actions, and assertion helpers for the SystemTagPicker dialog
|
||||
* and the inline collaborative-tags column.
|
||||
*/
|
||||
export class SystemTagsFilesListPage extends FilesListPage {
|
||||
|
||||
/**
|
||||
* The "Manage tags" dialog (SystemTagPicker).
|
||||
*/
|
||||
getTagPicker(): Locator {
|
||||
return this.page.getByRole('dialog', { name: 'Manage tags' })
|
||||
}
|
||||
|
||||
/**
|
||||
* The inline collaborative-tags list rendered in the file row.
|
||||
* The overflow indicator (e.g. "+2") has role="presentation" and is excluded
|
||||
* by getByRole('listitem'), so the accessible listitem count always equals the
|
||||
* number of actual tags regardless of whether an overflow indicator is shown.
|
||||
*/
|
||||
getInlineTagsForFile(filename: string): Locator {
|
||||
return this.getRowForFile(filename).getByRole('list', { name: /collaborative tags/i })
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the SystemTagPicker for a single file via its row action and waits
|
||||
* for the tags PROPFIND to complete before returning the picker locator.
|
||||
*/
|
||||
async openTagPickerForFile(filename: string): Promise<Locator> {
|
||||
const tagsListLoaded = this.page.waitForResponse(
|
||||
(r) => r.url().includes('/remote.php/dav/systemtags/') && r.request().method() === 'PROPFIND' && !r.url().includes('/files'),
|
||||
)
|
||||
await this.triggerActionForFile(filename, 'systemtags:bulk')
|
||||
await tagsListLoaded
|
||||
const picker = this.getTagPicker()
|
||||
await picker.waitFor({ state: 'visible' })
|
||||
return picker
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the SystemTagPicker for the current selection via the selection
|
||||
* actions bar and waits for the tags PROPFIND to complete before returning
|
||||
* the picker locator.
|
||||
*/
|
||||
async openTagPickerForSelection(): Promise<Locator> {
|
||||
const tagsListLoaded = this.page.waitForResponse(
|
||||
(r) => r.url().includes('/remote.php/dav/systemtags/') && r.request().method() === 'PROPFIND' && !r.url().includes('/files'),
|
||||
)
|
||||
await this.triggerSelectionAction('systemtags:bulk')
|
||||
await tagsListLoaded
|
||||
const picker = this.getTagPicker()
|
||||
await picker.waitFor({ state: 'visible' })
|
||||
return picker
|
||||
}
|
||||
|
||||
/**
|
||||
* Types a new tag name into the already-open picker, asserts no existing match,
|
||||
* creates the tag via the "Create new tag" button, verifies it is checked.
|
||||
*/
|
||||
async createNewTagInPicker(tagName: string): Promise<void> {
|
||||
const picker = this.getTagPicker()
|
||||
await picker.waitFor({ state: 'visible' })
|
||||
|
||||
await picker.getByLabel(/Search.*tag/i).fill(tagName)
|
||||
await expect(picker.getByRole('checkbox')).toHaveCount(0)
|
||||
|
||||
const createTagResponse = this.page.waitForResponse(
|
||||
(r) => r.url().includes('/remote.php/dav/systemtags') && !r.url().includes('/files') && r.request().method() === 'POST',
|
||||
)
|
||||
await picker.getByRole('button', { name: /Create new tag/i }).click()
|
||||
await createTagResponse
|
||||
|
||||
await expect(picker.getByRole('checkbox', { name: tagName })).toBeChecked()
|
||||
}
|
||||
|
||||
async selectTagInPicker(tagName: string): Promise<void> {
|
||||
const picker = this.getTagPicker()
|
||||
await picker.waitFor({ state: 'visible' })
|
||||
|
||||
await picker.getByLabel(/Search.*tag/i).fill(tagName)
|
||||
await expect(picker.getByRole('checkbox', { name: new RegExp(tagName, 'i') })).toHaveCount(1)
|
||||
await picker.getByRole('checkbox', { name: new RegExp(tagName, 'i') }).check({ force: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks Apply on the already-open picker and waits for the dialog to close.
|
||||
*/
|
||||
async applyTagPicker(): Promise<void> {
|
||||
const picker = this.getTagPicker()
|
||||
await picker.getByRole('button', { name: 'Apply' }).click()
|
||||
await expect(picker).not.toBeVisible({ timeout: 10_000 })
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the inline tag list for a file shows exactly the expected tags.
|
||||
*/
|
||||
async expectInlineTagsForFile(filename: string, expectedTags: string[]): Promise<void> {
|
||||
const tagList = this.getInlineTagsForFile(filename)
|
||||
|
||||
if (expectedTags.length === 0) {
|
||||
await expect(tagList).toHaveCount(0)
|
||||
return
|
||||
}
|
||||
|
||||
await expect(tagList.getByRole('listitem')).toHaveCount(expectedTags.length)
|
||||
for (const tag of expectedTags) {
|
||||
await expect(tagList).toContainText(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ export async function uploadContent(
|
|||
content: Buffer | string,
|
||||
mimeType: string,
|
||||
path: string,
|
||||
): Promise<number> {
|
||||
): Promise<string> {
|
||||
const requesttoken = await getRequestToken(request)
|
||||
const response = await request.fetch(davUrl(user, path), {
|
||||
method: 'PUT',
|
||||
|
|
@ -54,7 +54,7 @@ export async function uploadContent(
|
|||
throw new Error(`PUT ${path} failed with status ${response.status()}`)
|
||||
}
|
||||
const fileId = response.headers()['oc-fileid']
|
||||
return fileId ? parseInt(fileId, 10) : 0
|
||||
return fileId ? String(parseInt(fileId, 10)) : '0'
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
61
tests/playwright/support/utils/systemtags.ts
Normal file
61
tests/playwright/support/utils/systemtags.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { runOcc } from '@nextcloud/e2e-test-server'
|
||||
|
||||
export interface OccSystemTag {
|
||||
id: string
|
||||
name: string
|
||||
access: 'public' | 'restricted' | 'invisible'
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a system tag via OCC
|
||||
*
|
||||
* @param name - The tag to create
|
||||
* @param access - The access level
|
||||
*/
|
||||
export async function createTag(name: string, access: 'public' | 'restricted' | 'invisible' = 'public'): Promise<OccSystemTag> {
|
||||
const result = await runOcc(['tag:add', '--output=json', '--', name, access])
|
||||
return JSON.parse(result) as OccSystemTag
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a system tag via OCC
|
||||
*
|
||||
* @param id - The id of the tag to delete
|
||||
*/
|
||||
export async function deleteTag(id: string): Promise<void> {
|
||||
await runOcc(['tag:delete', id])
|
||||
}
|
||||
|
||||
/**
|
||||
* List all system tags via OCC
|
||||
*/
|
||||
export async function listTags(): Promise<OccSystemTag[]> {
|
||||
const output = await runOcc(['tag:list', '--output=json'])
|
||||
const json = JSON.parse(output) as Record<string, Omit<OccSystemTag, 'id'>>
|
||||
return Object.entries(json).map(([id, value]) => ({ ...value, id }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all existing tags via OCC.
|
||||
*/
|
||||
export async function clearTags(): Promise<void> {
|
||||
const tags = await listTags()
|
||||
for (const tag of tags) {
|
||||
await deleteTag(tag.id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign tags to a file via OCC.
|
||||
*
|
||||
* @param fileId - The ID of the file to assign tags to
|
||||
* @param tags - An array of tag names to assign to the file
|
||||
*/
|
||||
export async function assignTagsToFile(fileId: string, tags: string[]): Promise<void> {
|
||||
await runOcc(['tag:files:add', fileId, tags.join(','), 'public'])
|
||||
}
|
||||
Loading…
Reference in a new issue