Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>pull/61175/head
parent
2b7415ef4d
commit
ab96b85ac6
@ -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]) |
||||
}) |
||||
@ -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]) |
||||
}) |
||||
}) |
||||
@ -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) |
||||
}) |
||||
}) |
||||
@ -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() |
||||
}) |
||||
}) |
||||
@ -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() |
||||
}) |
||||
}) |
||||
@ -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' |
||||
@ -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) |
||||
} |
||||
} |
||||
} |
||||
@ -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 new issue