Initial Upload

This commit is contained in:
Matt Batchelder
2025-12-02 10:32:59 -05:00
commit 05ce0da296
2240 changed files with 467811 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Applications', function() {
let testRun = '';
beforeEach(function() {
cy.login();
testRun = Cypress._.random(0, 1e9);
});
it('should add edit an application', function() {
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/application/*',
}).as('putRequest');
cy.visit('/application/view');
// Click on the Add Application button
cy.contains('Add Application').click();
cy.get('.modal input#name')
.type('Cypress Test Application ' + testRun);
// Add first by clicking next
cy.get('.modal .save-button').click();
// Check if application is added in toast message
cy.contains('Edit Application');
cy.get('.modal input#name').clear()
.type('Cypress Test Application Edited ' + testRun);
// edit test application
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
// assertion on the "application" value
expect(responseData.name).to.eq('Cypress Test Application Edited ' + testRun);
// Return appKey as a Cypress.Promise to ensure proper scoping
return Cypress.Promise.resolve(responseData.key);
}).then((appKey) => {
if (appKey) {
// TODO cannot be deleted via cypress
// Delete the application and assert success
// cy.deleteApplication(appKey).then((res) => {
// expect(res.status).to.equal(200);
// });
}
});
});
});

View File

@@ -0,0 +1,198 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Folders', function() {
beforeEach(function() {
cy.login();
});
it('creating a new folder and rename it', () => {
cy.visit('/folders/view');
cy.contains('Root Folder').rightclick();
cy.contains('Create').should('be.visible').click();
cy.visit('/folders/view');
cy.contains('New Folder').should('be.visible').rightclick();
cy.contains('Rename').type('Folder123{enter}');
});
it('Moving an image from Root Folder to another folder', () => {
cy.intercept({
url: '/library?*',
query: {media: 'child_folder_media'},
}).as('mediaGridLoadAfterSearch');
// Go to library
cy.visit('/library/view');
cy.get('#media').type('child_folder_media');
// Wait for the search to complete
cy.wait('@mediaGridLoadAfterSearch');
cy.get('#libraryItems tbody tr').should('have.length', 1);
cy.get('#datatable-container').should('contain', 'child_folder_media');
// Click the dropdown menu and choose a folder to move the image to
cy.get('#libraryItems tr:first-child .dropdown-toggle').click({force: true});
cy.get('#libraryItems tr:first-child .library_button_selectfolder').click({force: true});
// Expand the folder tree and select ChildFolder
cy.get('#container-folder-form-tree>ul>li>i').click();
cy.get('#container-folder-form-tree>ul>li:not(.jstree-loading)>i').click();
cy.contains('ChildFolder').click();
// Click the save button
cy.get('.save-button').click();
});
it('Sharing', () => {
// Create and alias for load user permissions for folders
cy.intercept({
url: '/user/permissions/Folder/*',
query: {name: 'folder_user'},
}).as('permissionsFoldersAfterSearch');
cy.visit('/folders/view');
cy.contains('ShareFolder').should('be.visible').rightclick();
cy.get('ul.jstree-contextmenu >li:nth-child(6) > a').click(); // Click on Share Link
cy.get('#name').type('folder_user');
cy.wait('@permissionsFoldersAfterSearch');
cy.get('#permissionsTable tbody tr').should('have.length', 1);
cy.get('#permissionsTable tbody tr:nth-child(1) td:nth-child(1)').contains('folder_user');
cy.get('#permissionsTable tbody tr:nth-child(1) td:nth-child(2)> input').click();
cy.get('.save-button').click();
});
it('Set Home Folders for a user', () => {
// Create and alias for load users
cy.intercept({
url: '/user*',
query: {userName: 'folder_user'},
}).as('userGridLoadAfterSearch');
cy.visit('/user/view');
cy.get('#userName').type('folder_user');
cy.wait('@userGridLoadAfterSearch');
cy.get('#users tbody tr').should('have.length', 1);
cy.get('#users tr:first-child .dropdown-toggle').click({force: true});
cy.get('#users tr:first-child .user_button_set_home').click({force: true});
cy.get('#home-folder').should('be.visible');
cy.get('.jstree-anchor:contains(\'FolderHome\')').should('be.visible').click();
cy.get('.save-button').click();
// Check
cy.visit('/user/view');
cy.get('#userName').clear();
cy.get('#userName').type('folder_user');
cy.wait('@userGridLoadAfterSearch');
cy.get('#users tbody tr').should('have.length', 1);
cy.get('#users tbody tr:nth-child(1) td:nth-child(1)').contains('folder_user');
cy.get('#users tbody tr:nth-child(1) td:nth-child(3)').contains('FolderHome');
});
it('Remove an empty folder', () => {
cy.visit('/folders/view');
// Find the EmptyFolder element and right-click on it
cy.get('.jstree-anchor:contains(\'EmptyFolder\')')
.rightclick()
.should('have.class', 'jstree-hovered'); // Ensure the right-click effect
// Find the context menu item with "Remove" text and click on it
cy.contains('Remove').click();
// Validate
cy.visit('/folders/view');
cy.get('.jstree-anchor:contains(\'EmptyFolder\')').should('not.exist');
});
it('cannot remove a folder with content', () => {
cy.visit('/folders/view');
cy.get('.jstree-anchor:contains(\'FolderWithContent\')')
.rightclick();
// Find the context menu item with "Remove" text and click on it
cy.contains('Remove').click();
// Check folder still exists
cy.visit('/folders/view');
cy.get('.jstree-anchor:contains(\'FolderWithContent\')').should('exist');
});
it('search a media in a folder', () => {
// Go to library
cy.visit('/library/view');
cy.get('.jstree-anchor:contains(\'Root Folder\')')
.should('be.visible') // Ensure the element is visible
.parent()
.find('.jstree-icon.jstree-ocl')
.click();
cy.get('.jstree-anchor:contains(\'FolderWithImage\')').click();
cy.get('#libraryItems tbody tr').should('have.length', 1);
cy.get('#libraryItems tbody').contains('media_for_search_in_folder')
.should('be.visible');
});
it('Hide Folder tree', () => {
// Go to library
cy.visit('/library/view');
// The Folder tree is open by default on a grid
cy.get('#folder-tree-select-folder-button').click();
// clicking on the folder icon hides it
cy.get('#grid-folder-filter').should('have.css', 'display', 'none');
});
it('Move folders and Merge', () => {
// Go to folders
cy.visit('/folders/view');
cy.get('.jstree-anchor:contains(\'MoveFromFolder\')').rightclick();
cy.contains('Move Folder').click();
cy.get('#container-folder-form-tree').within(() => {
// Find the "MoveToFolder" link and click it
cy.contains('MoveToFolder').click();
});
cy.get('#merge').should('be.visible').check();
cy.get('.save-button').click();
// Validate test34 image exist in MoveToFolder
cy.visit('/folders/view');
cy.get('.jstree-anchor:contains(\'MoveFromFolder\')').should('not.exist');
// Validate test34 image exist in MoveToFolder
// Go to library
cy.visit('/library/view');
cy.get('.jstree-anchor:contains(\'Root Folder\')')
.should('be.visible') // Ensure the element is visible
.parent()
.find('.jstree-icon.jstree-ocl')
.click();
cy.get('.jstree-anchor:contains(\'MoveToFolder\')').click();
cy.get('#libraryItems tbody').contains('test34');
});
});

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
describe('Modules Page', function () {
beforeEach(function () {
cy.login();
});
it.skip('should load the modules page and show a complete table of modules', function () {
cy.visit('/module/view');
cy.contains('Modules');
// Click on the first page of the pagination
cy.get('.pagination > :nth-child(2) > a').click();
cy.contains('Showing 1 to');
});
});

View File

@@ -0,0 +1,157 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Tags', function() {
let testRun = '';
beforeEach(function() {
cy.login();
testRun = Cypress._.random(0, 1e9);
});
it('should add a tag', function() {
cy.visit('/tag/view');
// Click on the Add Tag button
cy.contains('Add Tag').click();
cy.get('.modal input#name')
.type('Cypress Test Tag ' + testRun + '_1');
// Add first by clicking next
cy.get('.modal .save-button').click();
// Check if tag is added in toast message
cy.contains('Added Cypress Test Tag ' + testRun + '_1');
});
it('searches and edit existing tag', function() {
// Create a new tag and then search for it and delete it
cy.createTag('Cypress Test Tag ' + testRun).then((res) => {
cy.intercept({
url: '/tag?*',
query: {tag: 'Cypress Test Tag ' + testRun},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/tag/*',
}).as('putRequest');
cy.visit('/tag/view');
// Filter for the created tag
cy.get('#Filter input[name="tag"]')
.type('Cypress Test Tag ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#tags tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#tags tr:first-child .dropdown-toggle').click({force: true});
cy.get('#tags tr:first-child .tag_button_edit').click({force: true});
cy.get('.modal input#name').clear()
.type('Cypress Test Tag Edited ' + testRun);
// edit test tag
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
const tag = responseData.tag;
// assertion on the "tag" value
expect(tag).to.eq('Cypress Test Tag Edited ' + testRun);
});
});
});
it('searches and delete existing tag', function() {
// Create a new tag and then search for it and delete it
cy.createTag('Cypress Test Tag ' + testRun).then((res) => {
cy.intercept({
url: '/tag?*',
query: {tag: 'Cypress Test Tag ' + testRun},
}).as('loadGridAfterSearch');
cy.visit('/tag/view');
// Filter for the created tag
cy.get('#Filter input[name="tag"]')
.type('Cypress Test Tag ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#tags tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#tags tr:first-child .dropdown-toggle').click({force: true});
cy.get('#tags tr:first-child .tag_button_delete').click({force: true});
// Delete test tag
cy.get('.bootbox .save-button').click();
// Check if tag is deleted in toast message
cy.get('.toast').contains('Deleted Cypress Test Tag');
});
});
it('selects multiple tags and delete them', function() {
// Create a new tag and then search for it and delete it
cy.createTag('Cypress Test Tag ' + testRun).then((res) => {
cy.intercept({
url: '/tag?*',
query: {tag: 'Cypress Test Tag'},
}).as('loadGridAfterSearch');
// Delete all test tags
cy.visit('/tag/view');
// Clear filter
cy.get('.clear-filter-btn').click();
cy.get('#Filter input[name="tag"]')
.type('Cypress Test Tag');
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
// Select all
cy.get('button[data-toggle="selectAll"]').click();
// Delete all
cy.get('.dataTables_info button[data-toggle="dropdown"]').click();
cy.get('.dataTables_info a[data-button-id="tag_button_delete"]').click();
cy.get('button.save-button').click();
// Modal should contain one successful delete at least
cy.get('.modal-body').contains(': Success');
});
});
});

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Tasks', function() {
beforeEach(function() {
cy.login();
});
it('should edit a task', function() {
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/task/*',
}).as('putRequest');
cy.visit('/task/view');
// Click on the first row element to open the delete modal
cy.get('#tasks tr:first-child .dropdown-toggle').click({force: true});
cy.get('#tasks tr:first-child .task_button_edit').click({force: true});
// Assuming you have an input field with the id 'myInputField'
cy.get('.modal input#name').invoke('val').then((value) => {
return Cypress.Promise.resolve(value);
}).then((value) => {
if (value) {
cy.get('.modal input#name').clear()
.type(value + ' Edited');
// edit test tag
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
// assertion on the "task" value
expect(responseData.name).to.eq(value + ' Edited');
});
}
});
});
});

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Transitions', function() {
beforeEach(function() {
cy.login();
});
it('should edit an transition', function() {
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/transition/*',
}).as('putRequest');
cy.visit('/transition/view');
cy.get('#transitions tbody tr').should('have.length', 3);
// Click on the first row element to open the delete modal
cy.get('#transitions tr:first-child .dropdown-toggle').click({force: true});
cy.get('#transitions tr:first-child .transition_button_edit').click({force: true});
cy.get('.modal #availableAsIn').then(($checkbox) => {
const isChecked = $checkbox.prop('checked');
cy.get('#availableAsIn').should('be.visible').click(); // Click to check/uncheck
// edit
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
// assertion on the "task" value
if (isChecked) {
expect(responseData.availableAsIn).to.eq(0);
} else {
expect(responseData.availableAsIn).to.eq(1);
}
});
});
});
});

View File

@@ -0,0 +1,128 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Usergroups', function() {
let testRun = '';
beforeEach(function() {
cy.login();
testRun = Cypress._.random(0, 1e9);
});
it('should add a usergroup', function() {
cy.visit('/group/view');
// Click on the Add Usergroup button
cy.contains('Add User Group').click();
cy.get('.modal input#group')
.type('Cypress Test Usergroup ' + testRun + '_1');
// Add first by clicking next
cy.get('.modal .save-button').click();
// Check if usergroup is added in toast message
cy.contains('Added Cypress Test Usergroup');
});
it('searches and edit existing usergroup', function() {
// Create a new usergroup and then search for it and delete it
cy.createUsergroup('Cypress Test Usergroup ' + testRun).then((groupId) => {
cy.intercept({
url: '/group?*',
query: {userGroup: 'Cypress Test Usergroup ' + testRun},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/group/*',
}).as('putRequest');
cy.visit('/group/view');
// Filter for the created usergroup
cy.get('#Filter input[name="userGroup"]')
.type('Cypress Test Usergroup ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#userGroups tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#userGroups tr:first-child .dropdown-toggle').click({force: true});
cy.get('#userGroups tr:first-child .usergroup_button_edit').click({force: true});
cy.get('.modal input#group').clear()
.type('Cypress Test Usergroup Edited ' + testRun);
// edit test usergroup
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
// assertion on the "usergroup" value
expect(responseData.group).to.eq('Cypress Test Usergroup Edited ' + testRun);
// Delete the usergroup and assert success
cy.deleteUsergroup(groupId).then((response) => {
expect(response.status).to.equal(200);
});
});
});
});
it('searches and delete existing usergroup', function() {
// Create a new usergroup and then search for it and delete it
cy.createUsergroup('Cypress Test Usergroup ' + testRun).then((groupId) => {
cy.intercept({
url: '/group?*',
query: {userGroup: 'Cypress Test Usergroup ' + testRun},
}).as('loadGridAfterSearch');
cy.visit('/group/view');
// Filter for the created usergroup
cy.get('#Filter input[name="userGroup"]')
.type('Cypress Test Usergroup ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#userGroups tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#userGroups tr:first-child .dropdown-toggle').click({force: true});
cy.get('#userGroups tr:first-child .usergroup_button_delete').click({force: true});
// Delete test usergroup
cy.get('.bootbox .save-button').click();
// Check if usergroup is deleted in toast message
cy.get('.toast').contains('Deleted Cypress Test Usergroup');
});
});
});

View File

@@ -0,0 +1,166 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Users', function() {
let testRun = '';
beforeEach(function() {
cy.login();
testRun = Cypress._.random(0, 1e9);
});
it('should add a user', function() {
cy.intercept({
url: '/user/form/homepages?groupId=1&userTypeId=3*',
query: {},
}).as('loadHomepageAfterSearch');
cy.visit('/user/view');
// Click on the Add User button
cy.contains('Add User').click();
cy.get('.radio input[value="manual"]').click();
cy.get('#onboarding-steper-next-button').click();
cy.get('.modal input#userName')
.type('CypressTestUser' + testRun);
cy.get('.modal input#password')
.type('cypress');
// Error checking - for incorrect email format
cy.get('.modal input#email').type('cypress');
cy.get('.select2-container--bootstrap').eq(1).click();
cy.log('Before waiting for Icon Dashboard element');
cy.wait('@loadHomepageAfterSearch');
cy.get('.select2-results__option')
.should('contain', 'Icon Dashboard')
.click();
// Try saving
cy.get('.modal .save-button').click();
cy.contains('Please enter a valid email address.');
cy.get('.modal input#email').clear().type('cypress@test.com');
// Save
cy.get('.modal .save-button').click();
// Check if user is added in toast message
cy.contains('Added CypressTestUser');
});
it('searches and edit existing user', function() {
// Create a new user and then search for it and delete it
cy.createUser('CypressTestUser' + testRun, 'password', 3, 1).then((id) => {
cy.intercept({
url: '/user?*',
query: {userName: 'CypressTestUser' + testRun},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/user/*',
}).as('putRequest');
cy.visit('/user/view');
// Filter for the created user
cy.get('#Filter input[name="userName"]')
.type('CypressTestUser' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#users tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#users tr:first-child .dropdown-toggle').click({force: true});
cy.get('#users tr:first-child .user_button_edit').click({force: true});
cy.get('.modal input#userName').clear()
.type('CypressTestUserEdited' + testRun);
cy.get('.modal input#newPassword').clear().type('newPassword');
cy.get('.modal input#retypeNewPassword').clear().type('wrongPassword');
// edit test user
cy.get('.bootbox .save-button').click();
cy.wait('@putRequest')
// Error checking - for password mismatch
cy.contains('Passwords do not match');
cy.get('.modal input#retypeNewPassword').clear().type('newPassword');
// edit test user
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
// assertion on the "user" value
expect(responseData.userName).to.eq('CypressTestUserEdited' + testRun);
});
// Delete the user and assert success
cy.deleteUser(id).then((res) => {
expect(res.status).to.equal(200);
});
});
});
it('searches and delete existing user', function() {
// Create a new user and then search for it and delete it
cy.createUser('CypressTestUser' + testRun, 'password', 3, 1).then((id) => {
cy.intercept({
url: '/user?*',
query: {userName: 'CypressTestUser' + testRun},
}).as('loadGridAfterSearch');
cy.visit('/user/view');
// Filter for the created user
cy.get('#Filter input[name="userName"]')
.type('CypressTestUser' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#users tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#users tr:first-child .dropdown-toggle').click({force: true});
cy.get('#users tr:first-child .user_button_delete').click({force: true});
// Delete test User
cy.get('.bootbox .save-button').click();
// Check if User is deleted in toast message
cy.get('.toast').contains('Deleted CypressTestUser');
});
});
});

View File

@@ -0,0 +1,167 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Campaigns', function() {
const testRun = Cypress._.random(0, 1e9);
beforeEach(function() {
cy.login();
});
// Create a list campaign
// Assign layout to it
// and add the id to the session
it('should add a campaign and assign a layout', function() {
cy.intercept('/campaign?draw=4&*').as('campaignGridLoad');
cy.intercept({
url: '/campaign?*',
query: {name: 'Cypress Test Campaign ' + testRun},
}).as('campaignGridLoadAfterSearch');
cy.intercept({
url: '/layout?*',
query: {layout: 'List Campaign Layout'},
}).as('layoutLoadAfterSearch');
// Intercept the POST request to get the campaign Id
cy.intercept('/campaign').as('postCampaign');
cy.intercept('/campaign/form/add?*').as('campaignFormAdd');
cy.visit('/campaign/view');
// Click on the Add Campaign button
cy.contains('Add Campaign').click();
cy.get('.modal input#name')
.type('Cypress Test Campaign ' + testRun);
cy.get('.modal .save-button').click();
// Wait for the edit form to pop open
cy.contains('.modal .modal-title', testRun);
// Wait for the intercepted POST request to complete and the response to be received
cy.wait('@postCampaign').then((interception) => {
// Access the response body and extract the ID
const id = interception.response.body.id;
// Save the ID to the Cypress.env object
Cypress.env('sessionCampaignId', id);
});
// Switch to the layouts tab.
cy.contains('.modal .nav-tabs .nav-link', 'Layouts').click();
// Should have no layouts assigned
cy.get('.modal #LayoutAssignSortable').children()
.should('have.length', 0);
// Search for 2 layouts names 'List Campaign Layout 1' and 'List Campaign Layout 2'
cy.get('.form-inline input[name="layout"]')
.type('List Campaign Layout').blur();
// Wait for the intercepted request and check the URL for the desired query parameter value
cy.wait('@layoutLoadAfterSearch').then((interception) => {
// Perform your desired actions or assertions here
cy.log('Layout Loading');
cy.get('#layoutAssignments tbody tr').should('have.length', 2);
// Assign a layout
cy.get('#layoutAssignments tr:nth-child(1) a.assignItem').click();
cy.get('#layoutAssignments tr:nth-child(2) a.assignItem').click();
// Save
cy.get('.bootbox .save-button').click();
// Wait for 4th campaign grid reload
cy.wait('@campaignGridLoad');
// Filter for the created campaign
cy.get('#Filter input[name="name"]')
.type('Cypress Test Campaign ' + testRun);
cy.wait('@campaignGridLoadAfterSearch');
// Should have 2 layouts assigned
cy.get('#campaigns tbody tr').should('have.length', 1);
cy.get('#campaigns tbody tr:nth-child(1) td:nth-child(5)').contains('2');
});
});
it('should schedule a campaign and should set display status to green', function() {
// At this point we know the campaignId
const displayName = 'List Campaign Display 1';
const sessionCampaignId = Cypress.env('sessionCampaignId');
// Schedule the campaign
cy.scheduleCampaign(sessionCampaignId, displayName).then((res) => {
cy.displaySetStatus(displayName, 1);
// Go to display grid
cy.intercept('/display?draw=3&*').as('displayGridLoad');
cy.visit('/display/view');
// Filter for the created campaign
cy.get('.FilterDiv input[name="display"]')
.type(displayName);
// Should have the display
cy.get('#displays tbody tr').should('have.length', 1);
// Check the display status is green
cy.get('#displays tbody tr:nth-child(1)').should('have.class', 'table-success'); // For class "table-success"
cy.get('#displays tbody tr:nth-child(1)').should('have.class', 'odd'); // For class "odd"
});
});
it('delete a campaign and check if the display status is pending', function() {
cy.intercept('/campaign?draw=2&*').as('campaignGridLoad');
cy.intercept('DELETE', '/campaign/*', (req) => {
}).as('deleteCampaign');
cy.visit('/campaign/view');
// Filter for the created campaign
cy.get('#Filter input[name="name"]')
.type('Cypress Test Campaign ' + testRun);
// Wait for 2nd campaign grid reload
cy.wait('@campaignGridLoad');
cy.get('#campaigns tbody tr').should('have.length', 1);
cy.get('#campaigns tr:first-child .dropdown-toggle').click({force: true});
cy.get('#campaigns tr:first-child .campaign_button_delete').click({force: true});
// Delete the campaign
cy.get('.bootbox .save-button').click();
// Wait for the intercepted DELETE request to complete with status 200
cy.wait('@deleteCampaign').its('response.statusCode').should('eq', 200);
// check the display status
cy.displayStatusEquals('List Campaign Display 1', 3).then((res) => {
expect(res.body).to.be.true;
});
});
});

View File

@@ -0,0 +1,318 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Display Groups', function() {
let testRun = '';
beforeEach(function() {
cy.login();
testRun = Cypress._.random(0, 1e9);
});
it('should add one empty and one filled display groups', function() {
cy.visit('/displaygroup/view');
// Click on the Add Displaygroup button
cy.contains('Add Display Group').click();
cy.get('.modal input#displayGroup')
.type('Cypress Test Displaygroup ' + testRun + '_1');
// Add first by clicking next
cy.get('.modal').contains('Next').click();
// Check if displaygroup is added in toast message
cy.contains('Added Cypress Test Displaygroup ' + testRun + '_1');
cy.get('.modal input#displayGroup')
.type('Cypress Test Displaygroup ' + testRun + '_2');
cy.get('.modal input#description')
.type('Description');
cy.get('.modal input#isDynamic').check();
cy.get('.modal input#dynamicCriteria')
.type('testLayoutId');
// Add first by clicking next
cy.get('.modal .save-button').click();
// Check if displaygroup is added in toast message
cy.contains('Added Cypress Test Displaygroup ' + testRun + '_2');
});
it('copy an existing displaygroup', function() {
// Create a new displaygroup and then search for it and delete it
cy.createDisplaygroup('Cypress Test Displaygroup ' + testRun).then((res) => {
cy.intercept({
url: '/displaygroup?*',
query: {displayGroup: 'Cypress Test Displaygroup ' + testRun},
}).as('loadGridAfterSearch');
// Intercept the POST request
cy.intercept({
method: 'POST',
url: /\/displaygroup\/\d+\/copy$/,
}).as('postRequest');
cy.visit('/displaygroup/view');
// Filter for the created displaygroup
cy.get('#Filter input[name="displayGroup"]')
.type('Cypress Test Displaygroup ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#displaygroups tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#displaygroups tr:first-child .dropdown-toggle').click({force: true});
cy.get('#displaygroups tr:first-child .displaygroup_button_copy').click({force: true});
// Delete test displaygroup
cy.get('.bootbox .save-button').click();
// Wait for the intercepted POST request and check the form data
cy.wait('@postRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
expect(responseData.displayGroup).to.include('Cypress Test Displaygroup ' + testRun + ' 2');
});
});
});
it('searches and delete existing displaygroup', function() {
// Create a new displaygroup and then search for it and delete it
cy.createDisplaygroup('Cypress Test Displaygroup ' + testRun).then((res) => {
cy.intercept({
url: '/displaygroup?*',
query: {displayGroup: 'Cypress Test Displaygroup ' + testRun},
}).as('loadGridAfterSearch');
cy.visit('/displaygroup/view');
// Filter for the created displaygroup
cy.get('#Filter input[name="displayGroup"]')
.type('Cypress Test Displaygroup ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#displaygroups tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#displaygroups tr:first-child .dropdown-toggle').click({force: true});
cy.get('#displaygroups tr:first-child .displaygroup_button_delete').click({force: true});
// Delete test displaygroup
cy.get('.bootbox .save-button').click();
// Check if displaygroup is deleted in toast message
cy.get('.toast').contains('Deleted Cypress Test Displaygroup');
});
});
// Seeded displays: dispgrp_disp1, dispgrp_disp2
it('manage membership for a displaygroup', function() {
cy.createDisplaygroup('Cypress Test Displaygroup ' + testRun).then((res) => {
// assign displays to display group
cy.intercept({
url: '/displaygroup?*',
query: {displayGroup: 'Cypress Test Displaygroup ' + testRun},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'POST',
url: /\/displaygroup\/\d+\/display\/assign$/,
}).as('postRequest');
cy.intercept({
url: '/display*',
query: {display: 'dispgrp_disp1'},
}).as('loadDisplayAfterSearch');
cy.visit('/displaygroup/view');
// Filter for the created displaygroup
cy.get('#Filter input[name="displayGroup"]')
.type('Cypress Test Displaygroup ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#displaygroups tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#displaygroups tr:first-child .dropdown-toggle').click({force: true});
cy.get('#displaygroups tr:first-child .displaygroup_button_group_members').click({force: true});
cy.get('.modal #display').type('dispgrp_disp1');
cy.wait('@loadDisplayAfterSearch');
cy.get('#displaysMembersTable').within(() => {
// count the rows within table
cy.get('tbody').find('tr').should('have.length', 1);
cy.get('tbody tr:first-child input[type="checkbox"]').check();
});
// Save assignments
cy.get('.bootbox .save-button').click();
// Wait for the intercepted POST request and check the form data
cy.wait('@postRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const body = response.body;
expect(body.success).to.eq(true);
});
});
});
// -------
// Seeded displays: dispgrp_disp_dynamic1, dispgrp_disp_dynamic2
it('should add a dynamic display group', function() {
cy.intercept({
url: '/display?*',
query: {display: 'dynamic'},
}).as('loadDisplayGridAfterSearch');
cy.visit('/displaygroup/view');
// Click on the Add Displaygroup button
cy.contains('Add Display Group').click();
cy.get('.modal input#displayGroup')
.type('Cypress Test Displaygroup ' + testRun);
// Add first by clicking next
cy.get('.modal #isDynamic').check();
// Type "dynamic" into the input field with the name "dynamicCriteria"
cy.get('.modal input[name="dynamicCriteria"]').type('dynamic');
cy.wait('@loadDisplayGridAfterSearch');
// Add first by clicking next
cy.get('.modal .save-button').click();
// Check if displaygroup is added in toast message
cy.contains('Added Cypress Test Displaygroup ' + testRun);
});
it('should edit the criteria of a dynamic display group', function() {
// Create a new displaygroup with dynamic criteria
cy.createDisplaygroup('Cypress Test Displaygroup Dynamic ' + testRun, true, 'dynamic').then((res) => {
cy.intercept({
url: '/displaygroup?*',
query: {displayGroup: 'Cypress Test Displaygroup Dynamic ' + testRun},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/displaygroup/*',
}).as('putRequest');
cy.visit('/displaygroup/view');
// Filter for the created displaygroup
cy.get('#Filter input[name="displayGroup"]')
.type('Cypress Test Displaygroup Dynamic ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#displaygroups tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#displaygroups tr:first-child .dropdown-toggle').click({force: true});
cy.get('#displaygroups tr:first-child .displaygroup_button_edit').click({force: true});
cy.get('.modal input[name="dynamicCriteria"]').clear().type('dynamic_edited');
// Delete test displaygroup
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
// assertion on the "display" value
expect(responseData.dynamicCriteria).to.eq('dynamic_edited');
});
});
});
// -------
// -- Delete Many
it('selects multiple display groups and delete them', function() {
// Create a new displaygroup and then search for it and delete it
cy.createDisplaygroup('Cypress Test Displaygroup ' + testRun).then((res) => {
cy.intercept('GET', '/displaygroup?draw=2&*').as('displaygroupGridLoad');
// Delete all test displaygroups
cy.visit('/displaygroup/view');
// Clear filter
cy.get('#Filter input[name="displayGroup"]')
.clear()
.type('Cypress Test Displaygroup');
// Wait for the grid reload
cy.wait('@displaygroupGridLoad');
// Select all
cy.get('button[data-toggle="selectAll"]').click();
// Delete all
cy.get('.dataTables_info button[data-toggle="dropdown"]').click();
cy.get('.dataTables_info a[data-button-id="displaygroup_button_delete"]').click();
cy.get('input#checkbox-confirmDelete').check();
cy.get('button.save-button').click();
// Modal should contain one successful delete at least
cy.get('.modal-body').contains(': Success');
});
});
// ---------
// Tests - Error handling
it('should not add a displaygroup without dynamic criteria', function() {
cy.visit('/displaygroup/view');
// Click on the Add Displaygroup button
cy.contains('Add Display Group').click();
cy.get('.modal input#displayGroup')
.type('Cypress Test Displaygroup ' + testRun + '_1');
cy.get('.modal input#isDynamic').check();
// Add first by clicking next
cy.get('.modal .save-button').click();
// Check toast message
cy.contains('Dynamic Display Groups must have at least one Criteria specified.');
});
});

View File

@@ -0,0 +1,334 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Displays', function() {
let testRun = '';
beforeEach(function() {
cy.login();
testRun = Cypress._.random(0, 1e9);
});
// Seeded displays: disp1, disp2, disp3, disp4, disp5
// Seeded display Groups: disp5_dispgrp
// Seeded layouts: disp4_default_layout
it('searches and edit existing display', function() {
// search for a display disp1 and edit
cy.intercept({
url: '/display?*',
query: {display: 'dis_disp1'},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/display/*',
}).as('putRequest');
cy.visit('/display/view');
// Filter for the created display
cy.get('#Filter input[name="display"]')
.type('dis_disp1');
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#displays tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#displays tr:first-child .dropdown-toggle').click({force: true});
cy.get('#displays tr:first-child .display_button_edit').click({force: true});
cy.get('.modal input#display').clear()
.type('dis_disp1 Edited');
cy.get('.modal input#license').clear()
.type('dis_disp1_license');
cy.get('.modal input#description').clear()
.type('description');
// edit test display
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
// assertion on the "display" value
expect(responseData.display).to.eq('dis_disp1 Edited');
expect(responseData.description).to.eq('description');
expect(responseData.license).to.eq('dis_disp1_license');
});
});
// Display: disp2
it('searches and delete existing display', function() {
cy.intercept({
url: '/display?*',
query: {display: 'dis_disp2'},
}).as('loadGridAfterSearch');
cy.visit('/display/view');
// Filter for the created display
cy.get('#Filter input[name="display"]')
.type('dis_disp2');
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#displays tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#displays tr:first-child .dropdown-toggle').click({force: true});
cy.get('#displays tr:first-child .display_button_delete').click({force: true});
// Delete test display
cy.get('.bootbox .save-button').click();
// Check if display is deleted in toast message
cy.get('.toast').contains('Deleted dis_disp2');
});
// Display: disp3
it('searches and authorise an unauthorised display', function() {
// search for a display disp1 and edit
cy.intercept({
url: '/display?*',
query: {display: 'dis_disp3'},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/display/authorise/*',
}).as('putRequest');
cy.visit('/display/view');
// Filter for the created display
cy.get('#Filter input[name="display"]')
.type('dis_disp3');
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#displays tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#displays tr:first-child .dropdown-toggle').click({force: true});
cy.get('#displays tr:first-child .display_button_authorise').click({force: true});
// edit test display
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
// assertion
expect(response.body.message).to.eq('Authorised set to 1 for dis_disp3');
});
});
// Display: disp4
it('set a default layout', function() {
cy.intercept({
url: '/display?*',
query: {display: 'dis_disp4'},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/display/defaultlayout/*',
}).as('putRequest');
cy.intercept({
url: '/layout*',
query: {
layout: 'disp4_default_layout',
},
}).as('loadLayoutAfterSearch');
cy.visit('/display/view');
// Filter for the created display
cy.get('#Filter input[name="display"]')
.type('dis_disp4');
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#displays tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#displays tr:first-child .dropdown-toggle').click({force: true});
cy.get('#displays tr:first-child .display_button_defaultlayout').click({force: true});
// Set the default layout
cy.get('.modal .select2-container--bootstrap').click();
cy.get('.select2-search__field').type('disp4_default_layout');
cy.wait('@loadLayoutAfterSearch');
cy.get('.select2-results__option').contains('disp4_default_layout').click();
// edit test display
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const body = response.body;
expect(body.success).to.eq(true);
});
});
// Display: disp5
it('manage membership for disp5', function() {
cy.intercept({
url: '/display?*',
query: {display: 'dis_disp5'},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'POST',
url: /\/display\/\d+\/displaygroup\/assign$/,
}).as('postRequest');
cy.intercept({
url: '/displaygroup*',
query: {
displayGroup: 'disp5_dispgrp',
},
}).as('loadDisplaypGroupAfterSearch');
cy.visit('/display/view');
// Filter for the created display
cy.get('#Filter input[name="display"]')
.type('dis_disp5');
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#displays tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#displays tr:first-child .dropdown-toggle').click({force: true});
cy.get('#displays tr:first-child .display_button_group_membership').click({force: true});
cy.get('.modal #displayGroup').type('disp5_dispgrp');
cy.wait('@loadDisplaypGroupAfterSearch');
cy.get('#displaysGroupsMembersTable').within(() => {
// count the rows within table
cy.get('tbody').find('tr')
.should('have.length', 1)
.and('contain', 'disp5_dispgrp');
cy.get('tbody tr:first-child input[type="checkbox"]')
.should('not.be.checked')
.check();
});
// Save assignments
cy.get('.bootbox .save-button').click();
// Wait for the intercepted POST request and check the form data
cy.wait('@postRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const body = response.body;
expect(body.success).to.eq(true);
});
});
it('should display map and revert back to table', function() {
cy.intercept('GET', '/user/pref?preference=displayGrid').as('displayPrefsLoad');
cy.intercept('GET', '/display?draw=2*').as('displayLoad');
cy.intercept('POST', '/user/pref').as('userPrefPost');
cy.visit('/display/view');
cy.wait('@displayPrefsLoad');
cy.wait('@displayLoad');
cy.wait('@userPrefPost');
cy.get('#map_button').click();
cy.get('#display-map.leaflet-container').should('be.visible');
cy.get('#list_button').click();
cy.get('#displays_wrapper.dataTables_wrapper').should('be.visible');
});
// ---------
// Tests - Error handling
it('should not be able to save while editing existing display with incorrect latitude/longitude', function() {
// search for a display disp1 and edit
cy.intercept({
url: '/display?*',
query: {display: 'dis_disp1'},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/display/*',
}).as('putRequest');
cy.visit('/display/view');
// Filter for the created display
cy.get('#Filter input[name="display"]')
.type('dis_disp1');
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#displays tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#displays tr:first-child .dropdown-toggle').click({force: true});
cy.get('#displays tr:first-child .display_button_edit').click({force: true});
cy.contains('Details').click();
cy.get('.modal input#latitude').type('1234');
// edit test display
cy.get('.bootbox .save-button').click();
// Check error message
cy.contains('The latitude entered is not valid.');
cy.get('.modal input#latitude').clear();
cy.get('.modal input#longitude').type('1234');
// edit test display
cy.get('.bootbox .save-button').click();
// Check error message
cy.contains('The longitude entered is not valid.');
});
});

View File

@@ -0,0 +1,166 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Display Settings', function() {
let testRun = '';
beforeEach(function() {
cy.login();
testRun = Cypress._.random(0, 1e9);
});
it('should and edit a display setting', function() {
// Intercept the POST request
cy.intercept({
method: 'POST',
url: '/displayprofile',
}).as('postRequest');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/displayprofile/*',
}).as('putRequest');
cy.visit('/displayprofile/view');
// Click on the Add Display Setting button
cy.contains('Add Profile').click();
cy.get('.modal input#name')
.type('Cypress Test Display Setting ' + testRun);
// Add first by clicking next
cy.get('.modal .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@postRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
// assertion on the "tag" value
expect(responseData.name).to.eq('Cypress Test Display Setting ' + testRun);
cy.get('.modal input#name').clear()
.type('Cypress Test Display Setting Edited ' + testRun);
// Select the option with the value "10 minutes"
cy.get('.modal #collectInterval').select('600');
// Add first by clicking next
cy.get('.modal .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
// assertion on the "tag" value
expect(responseData.name).to.eq('Cypress Test Display Setting Edited ' + testRun);
});
});
});
it('searches and edit existing display setting', function() {
// Create a new tag and then search for it and delete it
cy.createDisplayProfile('Cypress Test Display Setting ' + testRun, 'android').then((id) => {
cy.intercept({
url: '/displayprofile?*',
query: {displayProfile: 'Cypress Test Display Setting ' + testRun},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/displayprofile/*',
}).as('putRequest');
cy.visit('/displayprofile/view');
// Filter for the created tag
cy.get('#Filter input[name="displayProfile"]')
.type('Cypress Test Display Setting ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#displayProfiles tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#displayProfiles tr:first-child .dropdown-toggle').click({force: true});
cy.get('#displayProfiles tr:first-child .displayprofile_button_edit').click({force: true});
cy.get('.modal input#name').clear()
.type('Cypress Test Display Setting Edited ' + testRun);
// edit test tag
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
// assertion on the "tag" value
expect(responseData.name).to.eq('Cypress Test Display Setting Edited ' + testRun);
});
// Delete the user and assert success
cy.deleteDisplayProfile(id).then((res) => {
expect(res.status).to.equal(204);
});
});
});
it('searches and delete existing display setting', function() {
// Create a new tag and then search for it and delete it
cy.createDisplayProfile('Cypress Test Display Setting ' + testRun, 'android').then((id) => {
cy.intercept({
url: '/displayprofile?*',
query: {displayProfile: 'Cypress Test Display Setting ' + testRun},
}).as('loadGridAfterSearch');
cy.visit('/displayprofile/view');
// Filter for the created tag
cy.get('#Filter input[name="displayProfile"]')
.type('Cypress Test Display Setting ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#displayProfiles tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#displayProfiles tr:first-child .dropdown-toggle').click({force: true});
cy.get('#displayProfiles tr:first-child .displayprofile_button_delete').click({force: true});
// Delete test tag
cy.get('.bootbox .save-button').click();
// Check if tag is deleted in toast message
cy.get('.toast').contains('Deleted Cypress Test Display Setting');
});
});
});

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Sync Groups', function() {
let testRun = '';
beforeEach(function() {
cy.login();
testRun = Cypress._.random(0, 1e9);
});
it('should add one empty syncgroups', function() {
cy.visit('/syncgroup/view');
// Click on the Add Sync Group button
cy.contains('Add Sync Group').click();
cy.get('.modal input#name')
.type('Cypress Test Sync Group ' + testRun);
// Add first by clicking next
cy.get('.modal .save-button').click();
// Check if syncgroup is added in toast message
cy.contains('Added Cypress Test Sync Group ' + testRun);
});
it('searches and delete existing syncgroup', function() {
// Create a new syncgroup and then search for it and delete it
cy.createSyncGroup('Cypress Test Sync Group ' + testRun).then((res) => {
cy.intercept({
url: '/syncgroup?*',
query: {name: 'Cypress Test Sync Group ' + testRun},
}).as('loadGridAfterSearch');
cy.visit('/syncgroup/view');
// Filter for the created syncgroup
cy.get('#Filter input[name="name"]')
.type('Cypress Test Sync Group ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#syncgroups tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#syncgroups tr:first-child .dropdown-toggle').click({force: true});
cy.get('#syncgroups tr:first-child .syncgroup_button_group_delete').click({force: true});
// Delete test syncgroup
cy.get('.bootbox .save-button').click();
// Check if syncgroup is deleted in toast message
cy.get('.toast').contains('Deleted Cypress Test Sync Group');
});
});
// ---------
// Tests - Error handling
it.only('should not add a syncgroup without publisher port', function() {
cy.visit('/syncgroup/view');
// Click on the Add Sync Group button
cy.contains('Add Sync Group').click();
cy.get('.modal input#name')
.type('Cypress Test Sync Group ' + testRun);
cy.get('#syncPublisherPort').clear();
// Add first by clicking next
cy.get('.modal .save-button').click();
// Check if syncgroup is added in toast message
cy.contains('Sync Publisher Port cannot be empty');
});
});

View File

@@ -0,0 +1,134 @@
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Layout Editor Toolbar (Back button, Interactive Mode, Layout jump list)', () => {
beforeEach(function() {
cy.login();
cy.intercept('GET', '/user/pref?preference=toolbar').as('toolbarPrefsLoad');
cy.intercept('GET', '/user/pref?preference=editor').as('editorPrefsLoad');
cy.visit('/layout/view');
cy.get('button.layout-add-button').click();
cy.get('#layout-viewer').should('be.visible');
cy.wait('@toolbarPrefsLoad');
cy.wait('@editorPrefsLoad');
});
it('Back button should be present and navigate correctly', () => {
cy.get('#backBtn')
.should('have.class', 'btn btn-lg')
.and('have.attr', 'href', '/layout/view')
.click({force: true});
cy.url().should('include', '/layout/view');
});
it('should display Interactive Mode with OFF status initially', () => { // done
cy.get('li.interactive-control')
.should('have.attr', 'data-status', 'off')
.within(() => {
cy.contains('.interactive-control-label', 'Interactive Mode');
cy.get('.interactive-control-status-off').should('be.visible').and('contain.text', 'OFF');
cy.get('.interactive-control-status-on').should('not.be.visible');
});
});
it('should toggle Interactive Mode status on click', () => { // done
cy.get('li.nav-item.interactive-control[data-status="off"]')
.should(($el) => {
expect($el).to.be.visible;
})
.click({force: true});
cy.get('.interactive-control-status-off').should('not.be.visible');
});
it.only('should open and close the layout jump list dropdown safely', () => {
cy.intercept('GET', '/layout?onlyMyLayouts=*').as('onlyMyLayouts');
const layoutName = 'Audio-Video-PDF';
cy.get('#select2-layoutJumpList-container')
.should('be.visible');
// Force click because the element intermittently detaches in CI environment
cy.get('#layoutJumpListContainer .select2-selection')
.should('be.visible')
.click({force: true});
// Check for status
cy.wait('@onlyMyLayouts').then((interception) => {
const result = interception.response.body.data[0];
cy.log('result:', result.layoutId);
});
// Type into the search input
cy.get('.select2-search__field')
.should('be.visible')
.clear()
.type(layoutName, {delay: 100});
// Click the matching option
cy.get('.select2-results__option')
.contains(layoutName)
.click();
});
it('Options dropdown menu toggles and contains expected items', () => {
cy.get('#optionsContainerTop').should('be.visible');
cy.get('#optionsContainerTop').click({force: true});
cy.get('.navbar-submenu-options-container')
.should('be.visible')
.within(() => {
cy.get('#publishLayout').should('be.visible');
cy.get('#checkoutLayout').should('have.class', 'd-none');
cy.get('#discardLayout').should('be.visible');
cy.get('#newLayout').should('be.visible');
cy.get('#deleteLayout').should('have.class', 'd-none');
cy.get('#saveTemplate').should('have.class', 'd-none');
cy.get('#scheduleLayout').should('have.class', 'd-none');
cy.get('#clearLayout').should('be.visible');
cy.get('#displayTooltips').should('be.checked');
cy.get('#deleteConfirmation').should('be.checked');
});
});
it('Tooltips and popovers appear on hover', () => {
// Tooltip
cy.get('.layout-info-name')
.should('be.visible')
.trigger('mouseover');
cy.get('.tooltip').should('be.visible');
cy.get('.layout-info-name')
.should('be.visible')
.trigger('mouseout');
// Popover
cy.get('#layout-info-status')
.should('be.visible')
.trigger('mouseover');
cy.get('.popover').should('be.visible');
cy.get('#layout-info-status')
.should('be.visible')
.trigger('mouseout');
});
});

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Layout Editor Background', function() {
const SELECTORS = {
layoutAddButton: 'button.layout-add-button',
layoutViewer: '#layout-viewer',
propertiesPanel: '#properties-panel',
colorPickerTrigger: '.input-group-prepend',
colorPickerSaturation: '.colorpicker-saturation',
backgroundColorInput: '#input_backgroundColor',
backgroundzIndex: '#input_backgroundzIndex',
resolutionDropdown: '#input_resolutionId',
select2Selection: '.select2-selection',
select2SearchInput: '.select2-container--open input[type="search"]',
layoutInfoDimensions: '.layout-info-dimensions span',
};
beforeEach(function() {
cy.login();
cy.visit('/layout/view');
cy.get(SELECTORS.layoutAddButton).click();
cy.get(SELECTORS.layoutViewer).should('be.visible'); // Assert that the URL has changed to the layout editor
});
it('should update the background according to the colour set via colour picker', function() {
cy.get(SELECTORS.propertiesPanel).should('be.visible'); // Verify properties panel is present
cy.get(SELECTORS.colorPickerTrigger).click(); // Open colour picker
cy.get(SELECTORS.colorPickerSaturation).click(68, 28); // Select on a specific saturation
cy.get(SELECTORS.propertiesPanel).click(30, 60); // Click outside color picker to close
// Verify the selected color is applied to the background
cy.get(SELECTORS.layoutViewer).should('have.css', 'background-color', 'rgb(243, 248, 255)');
});
it('should update the background according to the colour set via hex input', function() {
cy.get(SELECTORS.propertiesPanel).should('be.visible');
cy.get(SELECTORS.backgroundColorInput).clear().type('#b53939{enter}');
// Verify the selected color is applied to the background
cy.get(SELECTORS.layoutViewer).should('have.css', 'background-color', 'rgb(243, 248, 255)');
});
it('should update the layer according to the input', function() {
cy.get(SELECTORS.propertiesPanel).should('be.visible');
cy.get(SELECTORS.backgroundzIndex).clear().type('1{enter}');
// Verify the selected number is applied to the layer
cy.get(SELECTORS.backgroundzIndex).should('have.value', '1');
});
// This is failing and a bug reported
it.skip('should update the layout resolution', function() {
cy.get(SELECTORS.propertiesPanel).should('be.visible');
const resName = 'cinema';
cy.get(SELECTORS.resolutionDropdown).parent().find(SELECTORS.select2Selection).click();
cy.get(SELECTORS.select2SearchInput).type(resName);
cy.selectOption(resName);
cy.get(SELECTORS.layoutInfoDimensions)
.should('be.visible')
.and('contain', '4096x2304');
});
});

View File

@@ -0,0 +1,164 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Layout Designer (Empty)', function() {
beforeEach(function() {
cy.login();
});
context('Unexisting Layout', function() {
it('show layout not found if layout does not exist', function() {
// Use a huge id to test a layout not found
cy.visit({
url: '/layout/designer/111111111111',
failOnStatusCode: false,
});
// See page not found message
cy.contains('Layout not found');
});
});
context('Empty layout (published)', function() {
const layoutTempName = '';
beforeEach(function() {
// Import a layout and go to the Layout's designer page - we need a Layout in a Published state
cy.importLayout('../assets/export_test_layout.zip').as('testLayoutId').then((res) => {
cy.goToLayoutAndLoadPrefs(res);
});
});
it.skip('goes into draft mode when checked out', function() {
// Get the done button from the checkout modal
cy.get('[data-test="welcomeModal"] button.btn-bb-checkout').click();
// Check if campaign is deleted in toast message
cy.contains('Checked out ' + layoutTempName);
});
it.skip('should prevent a layout edit action, and show a toast message', function() {
// Should contain widget options form
cy.get('#properties-panel-form-container').contains('Edit Layout');
// The save button should not be visible
cy.get('#properties-panel-form-container [data-action="save"]').should('not.exist');
});
});
context('Empty layout (draft)', function() {
beforeEach(function() {
// Create random name
const uuid = Cypress._.random(0, 1e9);
// Create a new layout and go to the layout's designer page, then load toolbar prefs
cy.createLayout(uuid).as('testLayoutId').then((res) => {
cy.goToLayoutAndLoadPrefs(res);
});
});
it.skip('should create a new region from within the navigator edit', () => {
// Open navigator edit
cy.get('.editor-bottom-bar #navigator-edit-btn').click();
// Click on add region button
cy.get('.editor-bottom-bar #add-btn').click();
// Check if there are 2 regions in the timeline ( there was 1 by default )
cy.get('#layout-timeline [data-type="region"]').should('have.length', 2);
});
it.skip('should delete a region using the toolbar bin', () => {
cy.intercept('GET', '/layout?layoutId=*').as('reloadLayout');
// Open navigator edit
cy.get('.editor-bottom-bar #navigator-edit-btn').click();
// Select a region from the navigator
cy.get('#layout-navigator-content [data-type="region"]:first-child').click().then(($el) => {
const regionId = $el.attr('id');
// Click trash container
cy.get('.editor-bottom-bar #delete-btn').click();
// Confirm delete on modal
cy.get('[data-test="deleteObjectModal"] button.btn-bb-confirm').click();
// Check toast message
cy.get('.toast-success').contains('Deleted');
// Wait for the layout to reload
cy.wait('@reloadLayout');
// Check that region is not on timeline
cy.get('#layout-timeline [data-type="region"]#' + regionId).should('not.exist');
});
});
it.skip('creates a new widget by selecting a searched media from the toolbar to layout-navigator region', () => {
cy.populateLibraryWithMedia();
// Create and alias for reload Layout
cy.intercept('GET', '/layout?layoutId=*').as('reloadLayout');
cy.intercept('GET', '/library/search?*').as('mediaLoad');
// Open library search tab
cy.get('.editor-main-toolbar #btn-menu-0').should('be.visible').click({force: true});
cy.get('.editor-main-toolbar #btn-menu-1').should('be.visible').click({force: true});
cy.wait('@mediaLoad');
cy.get('.editor-bottom-bar #navigator-edit-btn').click({force: true});
cy.get('.editor-main-toolbar #media-content-1 .toolbar-card:nth-of-type(2)').find('img').should('be.visible');
// Get a table row, select it and add to the region
cy.get('.editor-main-toolbar #media-content-1 .toolbar-card:nth-of-type(2) .select-button').click({force: true}).then(() => {
cy.get('#layout-navigator [data-type="region"]:first-child').click().then(() => {
// Wait for the layout to reload
cy.wait('@reloadLayout');
// Check if there is just one widget in the timeline
cy.get('#layout-timeline [data-type="region"] [data-type="widget"]').then(($widgets) => {
expect($widgets.length).to.eq(1);
});
});
});
});
it.skip('shows the file upload form by adding a uploadable media from the toolbar to layout-navigator region', () => {
cy.populateLibraryWithMedia();
// Open toolbar Widgets tab
cy.get('.editor-main-toolbar #btn-menu-1').should('be.visible').click({force: true});
cy.get('.editor-main-toolbar #btn-menu-2').should('be.visible').click({force: true});
cy.get('.editor-bottom-bar #navigator-edit-btn').click();
cy.get('.editor-main-toolbar #content-2 .toolbar-pane-content .toolbar-card.upload-card').should('be.visible').then(() => {
cy.get('.editor-main-toolbar #content-2 .toolbar-pane-content .toolbar-card.upload-card .select-upload').click({force: true});
cy.get('#layout-navigator [data-type="region"]:first-child').click({force: true});
cy.get('[data-test="uploadFormModal"]').contains('Upload media');
});
});
});
});

View File

@@ -0,0 +1,348 @@
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Layout Editor Options', function() {
beforeEach(function() {
cy.login();
cy.visit('/layout/view');
});
it.skip('should be able to publish, checkout and discard layout', function() {
let layoutName;
cy.intercept('GET', '/layout?layoutId=*').as('layoutStatus');
cy.intercept('PUT', '/layout/discard/*').as('discardLayout');
cy.get('button.layout-add-button').click();
cy.get('#layout-viewer').should('be.visible');
// Publish layout
cy.openOptionsMenu();
cy.get('#publishLayout').click();
cy.get('button.btn-bb-Publish').click();
cy.wait('@layoutStatus').then((interception) => {
expect(interception.response.statusCode).to.eq(200);
// Check if the publishedStatus is "Published"
const layoutData = interception.response.body.data[0];
expect(layoutData).to.have.property('publishedStatus', 'Published');
});
// Checkout published layout
cy.openOptionsMenu();
cy.get('#checkoutLayout').click();
cy.wait('@layoutStatus').then((interception) => {
expect(interception.response.statusCode).to.eq(200);
// Check if the publishedStatus is back to "Draft"
const layoutData = interception.response.body.data[0];
expect(layoutData).to.have.property('publishedStatus', 'Draft');
});
// Capture layout name before discarding draft layout
cy.get('.layout-info-name span')
.invoke('text')
.then((name) => {
layoutName = name.trim().replace(/^"|"$/g, ''); // Remove double quotes
cy.log(`Layout Name: ${layoutName}`);
cy.openOptionsMenu();
cy.get('#discardLayout').click();
cy.get('button.btn-bb-Discard').click();
// Verify that the layout has been discarded
cy.wait('@discardLayout').then((interception) => {
expect(interception.response.statusCode).to.equal(200);
});
// Check if the user is redirected to the layouts page
cy.url().should('include', '/layout/view');
// Search for the layout name
cy.get('input[name="layout"]').clear().type(`${layoutName}{enter}`);
// Check status of the layout with matching layout name
cy.get('#layouts tbody')
.find('tr')
.should('contain', layoutName)
.should('contain', 'Published');
});
});
it.skip('should display an error when publishing an invalid layout', function() {
cy.intercept('GET', '/playlist/widget/form/edit/*').as('addElement');
cy.intercept('PUT', '/layout/publish/*').as('publishLayout');
cy.get('button.layout-add-button').click();
cy.get('#layout-viewer').should('be.visible');
// Open widgets toolbox
cy.openToolbarMenu(0, false);
cy.get('[data-sub-type="ics-calendar"]').click();
cy.get('[data-template-id="daily_light"]').click();
cy.get('.viewer-object').click();
// Wait for element to be loaded on layout
cy.wait('@addElement').then((interception) => {
expect(interception.response.statusCode).to.eq(200);
});
// Publish layout
cy.openOptionsMenu();
cy.get('#publishLayout').click();
cy.get('button.btn-bb-Publish').click();
// Verify response
cy.wait('@publishLayout').then((interception) => {
expect(interception.response.statusCode).to.eq(200);
expect(interception.response.body).to.have.property('message', 'There is an error with this Layout: Missing required property Feed URL');
});
// Verify that a toast message is displayed
cy.get('.toast-message')
.should('be.visible')
.and('contain.text', 'There is an error with this Layout');
});
it.skip('should be able to create new layout', function() {
cy.intercept('GET', '/layout?layoutId=*').as('newLayout');
cy.get('button.layout-add-button').click();
cy.get('#layout-viewer').should('be.visible');
// Capture the layout ID of the initial layout loaded
cy.get('#layout-editor')
.invoke('attr', 'data-layout-id')
.then((initialLayoutId) => {
// Create new layout
cy.wait(1000);
cy.openOptionsMenu();
cy.get('#newLayout').click();
cy.wait('@newLayout').then((interception) => {
expect(interception.response.statusCode).to.eq(200); // Check if the request was successful
// Get the new layout ID
cy.get('#layout-editor')
.invoke('attr', 'data-layout-id')
.then((newLayoutId) => {
// Assert that the new layout ID is different from the initial layout ID
expect(newLayoutId).to.not.eq(initialLayoutId);
});
});
});
});
it.skip('should be able to unlock layout', function() {
let layoutName;
cy.intercept('GET', '/layout?layoutId=*').as('checkLockStatus');
cy.intercept('GET', '/playlist/widget/form/edit/*').as('addElement');
cy.get('button.layout-add-button').click();
cy.get('#layout-viewer').should('be.visible');
// Capture layout name to navigate back to it after unlocking
cy.get('.layout-info-name span')
.invoke('text')
.then((name) => {
layoutName = name.trim().replace(/^"|"$/g, '');
cy.log(`Layout Name: ${layoutName}`);
// Open global elements toolbox
cy.openToolbarMenu(1, false);
cy.get('[data-template-id="text"]').click();
cy.get('.viewer-object').click();
// Wait for element to be loaded on layout
cy.wait('@addElement').then((interception) => {
expect(interception.response.statusCode).to.eq(200);
});
// Check for lock status
cy.wait('@checkLockStatus').then((interception) => {
const isLocked = interception.response.body.data[0].isLocked;
expect(isLocked).to.not.be.empty;
cy.log('isLocked:', isLocked);
});
cy.intercept('PUT', '/layout/lock/release/*').as('unlock');
// Unlock layout
cy.wait(1000);
cy.openOptionsMenu();
cy.get('#unlockLayout').should('be.visible').click();
cy.get('button.btn-bb-unlock').click();
// Wait for the release lock request to complete
cy.wait('@unlock').then((interception) => {
expect(interception.response.statusCode).to.equal(200);
});
// Check if the user is redirected to the /layout/view page
cy.url().should('include', '/layout/view');
// Search for the layout name
cy.get('input[name="layout"]').clear().type(`${layoutName}{enter}`);
cy.get('#layouts tbody tr').should('contain.text', layoutName);
cy.get('#layouts tbody tr').should('have.length', 1);
cy.openRowMenu();
cy.get('#layout_button_design').click();
cy.get('#layout-viewer').should('be.visible');
// Check for lock status
cy.wait('@checkLockStatus').then((interception) => {
const isLocked = interception.response.body.data[0].isLocked;
expect(isLocked).be.empty;
cy.log('isLocked:', isLocked);
});
});
});
it.skip('should enable tooltips', function() {
cy.intercept('POST', '/user/pref').as('updatePreferences');
cy.get('button.layout-add-button').click();
cy.get('#layout-viewer').should('be.visible');
cy.openOptionsMenu();
// Enable tooltips
// Check the current state of the tooltips checkbox
cy.get('#displayTooltips').then(($checkbox) => {
if (!$checkbox.is(':checked')) {
// Check the checkbox if it is currently unchecked
cy.wrap($checkbox).click();
cy.wait('@updatePreferences');
// Confirm the checkbox is checked
cy.get('#displayTooltips').should('be.checked');
}
});
// Verify that tooltips are present
cy.get('.navbar-nav .btn-menu-option[data-toggle="tooltip"]').each(($element) => {
// Trigger hover to show tooltip
cy.wrap($element).trigger('mouseover');
// Check that the tooltip is visible for each button
cy.get('.tooltip').should('be.visible'); // Expect tooltip to be present
});
});
it.skip('should disable tooltips', function() {
cy.intercept('POST', '/user/pref').as('updatePreferences');
cy.get('button.layout-add-button').click();
cy.get('#layout-viewer').should('be.visible');
cy.openOptionsMenu();
// Disable tooltips
// Check the current state of the tooltips checkbox
cy.get('#displayTooltips').then(($checkbox) => {
if ($checkbox.is(':checked')) {
// Uncheck the checkbox if it is currently checked
cy.wrap($checkbox).click();
cy.wait('@updatePreferences');
// Confirm the checkbox is now unchecked
cy.get('#displayTooltips').should('not.be.checked');
}
});
// Verify that tooltips are gone
cy.get('.navbar-nav .btn-menu-option[data-toggle="tooltip"]').each(($element) => {
cy.wrap($element).trigger('mouseover'); // Trigger hover to show tooltip
cy.get('.tooltip').should('not.exist'); // Check if tooltip is gone for each button on the toolbox
});
});
it.skip('should enable delete confirmation', function() {
cy.intercept('POST', '/user/pref').as('updatePreferences');
cy.get('button.layout-add-button').click();
cy.get('#layout-viewer').should('be.visible');
cy.openOptionsMenu();
// Check the current state of the delete confirmation checkbox
cy.get('#deleteConfirmation').then(($checkbox) => {
if (!$checkbox.is(':checked')) {
// Check the checkbox if it is currently unchecked
cy.wrap($checkbox).click();
cy.wait('@updatePreferences');
// Confirm the checkbox is checked
cy.get('#deleteConfirmation').should('be.checked');
}
});
// Add an element then attempt to delete
cy.openToolbarMenu(0, false);
cy.get('[data-sub-type="clock"]').click();
cy.get('[data-sub-type="clock-analogue"]').click();
cy.get('.viewer-object').click();
cy.get('#delete-btn').click();
// Verify that delete confirmation modal appears
cy.get('.modal-content')
.should('be.visible')
.and('contain.text', 'Delete Widget');
});
it.skip('should disable delete confirmation', function() {
cy.intercept('POST', '/user/pref').as('updatePreferences');
cy.get('button.layout-add-button').click();
cy.get('#layout-viewer').should('be.visible');
cy.openOptionsMenu();
// Check the current state of the delete confirmation checkbox
cy.get('#deleteConfirmation').then(($checkbox) => {
if ($checkbox.is(':checked')) {
// Uncheck the checkbox if it is currently checked
cy.wrap($checkbox).click();
cy.wait('@updatePreferences');
// Confirm the checkbox is now unchecked
cy.get('#displayTooltips').should('not.be.checked');
}
});
cy.intercept('DELETE', '/region/*').as('deleteElement');
// Add an element then attempt to delete
cy.openToolbarMenu(0, false);
cy.get('[data-sub-type="clock"]').click();
cy.get('[data-sub-type="clock-analogue"]').click();
cy.get('.viewer-object').click();
cy.get('#delete-btn').click();
// Verify that the widget is immediately deleted without confirmation
cy.wait('@deleteElement').then((interception) => {
expect(interception.response.statusCode).to.equal(200);
});
cy.get('.viewer-object').within(() => {
cy.get('[data-type="region"]').should('not.exist');
});
});
});

View File

@@ -0,0 +1,240 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Layout Designer (Populated)', function() {
beforeEach(function() {
cy.login();
// Import existing
cy.importLayout('../assets/export_test_layout.zip').as('testLayoutId').then((res) => {
cy.checkoutLayout(res);
cy.goToLayoutAndLoadPrefs(res);
});
});
// Open widget form, change the name and duration, save, and see the name change result
it.skip('changes and saves widget properties', () => {
// Create and alias for reload widget
cy.intercept('GET', '/playlist/widget/form/edit/*').as('reloadWidget');
// Select the first widget from the first region on timeline ( image )
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').click();
// Type the new name in the input
cy.get('#properties-panel input[name="name"]').clear().type('newName');
// Set a duration
cy.get('#properties-panel #useDuration').check();
cy.get('#properties-panel input[name="duration"]').clear().type(12);
// Save form
cy.get('#properties-panel button[data-action="save"]').click();
// Should show a notification for the name change
cy.get('.toast-success').contains('newName');
// Check if the values are the same entered after reload
cy.wait('@reloadWidget').then(() => {
cy.get('#properties-panel input[name="name"]').should('have.attr', 'value').and('equal', 'newName');
cy.get('#properties-panel input[name="duration"]').should('have.attr', 'value').and('equal', '12');
});
});
// On layout edit form, change background color and layer, save and check the changes
it.skip('changes and saves layout properties', () => {
// Create and alias for reload layout
cy.intercept('GET', '/layout?layoutId=*').as('reloadLayout');
// Change background color
cy.get('#properties-panel input[name="backgroundColor"]').clear().type('#ccc');
// Change layer
cy.get('#properties-panel input[name="backgroundzIndex"]').clear().type(1);
// Save form
cy.get('#properties-panel button[data-action="save"]').click();
// Should show a notification for the successful save
cy.get('.toast-success').contains('Edited');
// Check if the values are the same entered after reload
cy.wait('@reloadLayout').then(() => {
cy.get('#properties-panel input[name="backgroundColor"]').should('have.attr', 'value').and('equal', '#cccccc');
cy.get('#properties-panel input[name="backgroundzIndex"]').should('have.value', '1');
});
});
// On layout edit form, change background image check the changes
it.skip('should change layout´s background image', () => {
// Create and alias for reload layout
cy.intercept('GET', '/layout?layoutId=*').as('reloadLayout');
cy.intercept('GET', '/library/search?*').as('mediaLoad');
cy.get('#properties-panel #backgroundRemoveButton').click();
// Open library search tab
cy.get('.editor-main-toolbar #btn-menu-0').click();
cy.get('.editor-main-toolbar #btn-menu-1').click();
cy.wait('@mediaLoad');
cy.get('.editor-bottom-bar #navigator-edit-btn').click();
cy.get('.editor-main-toolbar #media-content-1 .toolbar-card:nth-of-type(2)').find('img').should('be.visible');
// Get a table row, select it and add to the region
cy.get('.editor-main-toolbar #media-content-1 .toolbar-card:nth-of-type(2) .select-button').click({force: true}).then(() => {
cy.get('#properties-panel-form-container .background-image-drop').click().then(() => {
// Save form
cy.get('#properties-panel button[data-action="save"]').click();
// Should show a notification for the successful save
cy.get('.toast-success').contains('Edited');
// Check if the background field has an image
cy.get('#properties-panel .background-image-add img#bg_image_image').should('be.visible');
});
});
});
// Navigator
it.skip('should change and save the region´s position', () => {
// Create and alias for position save and reload layout
cy.intercept('GET', '/layout?layoutId=*').as('reloadLayout');
cy.intercept('GET', '/region/form/edit/*').as('reloadRegion');
cy.intercept('GET', '**/region/preview/*').as('regionPreview');
// Open navigator edit
cy.get('.editor-bottom-bar #navigator-edit-btn').click();
// Wait for the region to preview
cy.wait('@regionPreview');
cy.get('#layout-navigator [data-type="region"]:first').then(($originalRegion) => {
const regionId = $originalRegion.attr('id');
// Select region
cy.get('#layout-navigator-content #' + regionId).click();
// Move region 50px for each dimension
cy.get('#layout-navigator-content #' + regionId).then(($movedRegion) => {
const regionOriginalPosition = {
top: Math.round($movedRegion.position().top),
left: Math.round($movedRegion.position().left),
};
const offsetToAdd = 50;
// Move the region
cy.get('#layout-navigator-content #' + regionId)
.trigger('mousedown', {
which: 1,
})
.trigger('mousemove', {
which: 1,
pageX: $movedRegion.width() / 2 + $movedRegion.offset().left + offsetToAdd,
pageY: $movedRegion.height() / 2 + $movedRegion.offset().top + offsetToAdd,
})
.trigger('mouseup');
// Close the navigator edit
cy.wait('@reloadRegion');
// Save
cy.get('#properties-panel button#save').click();
// Wait for the layout to reload
cy.wait('@reloadLayout');
// Check if the region´s position are not the original
cy.get('#layout-navigator-content #' + regionId).then(($changedRegion) => {
expect(Math.round($changedRegion.position().top)).to.not.eq(regionOriginalPosition.top);
expect(Math.round($changedRegion.position().left)).to.not.eq(regionOriginalPosition.left);
});
});
});
});
it.skip('should delete a widget using the toolbar bin', () => {
cy.intercept('GET', '/layout?layoutId=*').as('reloadLayout');
cy.intercept('GET', '/region/preview/*').as('regionPreview');
// Select a widget from the timeline
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').click().then(($el) => {
const widgetId = $el.attr('id');
// Wait for the widget to be loaded
cy.wait('@regionPreview');
// Click trash container
cy.get('.editor-bottom-bar button#delete-btn').click({force: true});
// Confirm delete on modal
cy.get('[data-test="deleteObjectModal"] button.btn-bb-confirm').click();
// Check toast message
cy.get('.toast-success').contains('Deleted');
// Wait for the layout to reload
cy.wait('@reloadLayout');
// Check that widget is not on timeline
cy.get('#layout-timeline [data-type="widget"]#' + widgetId).should('not.exist');
});
});
it.skip('saves the widgets order when sorting by dragging', () => {
cy.intercept('GET', 'POST', '**/playlist/order/*').as('saveOrder');
cy.intercept('GET', '/layout?layoutId=*').as('reloadLayout');
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').then(($oldWidget) => {
const offsetX = 50;
// Move to the second widget position ( plus offset )
cy.wrap($oldWidget)
.trigger('mousedown', {
which: 1,
})
.trigger('mousemove', {
which: 1,
pageX: $oldWidget.offset().left + $oldWidget.width() * 1.5 + offsetX,
})
.trigger('mouseup', {force: true});
cy.wait('@saveOrder');
// Should show a notification for the order change
cy.get('.toast-success').contains('Order Changed');
// Reload layout and check if the new first widget has a different Id
cy.wait('@reloadLayout');
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').then(($newWidget) => {
expect($oldWidget.attr('id')).not.to.eq($newWidget.attr('id'));
});
});
});
});

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Layout Editor Status Bar', function() {
const layoutStatusSelector = '#layout-info-status';
const layoutNameSelector = '.layout-info-name span';
const layoutDurationSelector = '.layout-info-duration .layout-info-duration-value';
const layoutDimensionsSelector = '.layout-info-dimensions span';
const tooltipSelector = '.popover';
beforeEach(function() {
cy.login();
cy.visit('/layout/view');
cy.get('button.layout-add-button').click();
cy.get('#layout-viewer').should('be.visible');
});
it('should display the correct Layout status icon and tooltip', function() {
cy.get(layoutStatusSelector)
.should('be.visible')
.and('have.class', 'badge-danger')
.trigger('mouseover');
cy.get(tooltipSelector)
.should('be.visible')
.and('contain', 'This Layout is invalid');
cy.get(layoutStatusSelector).trigger('mouseout');
});
it('should display the correct Layout name', () => {
// Verify the Layout name text
cy.get(layoutNameSelector)
.should('be.visible')
.and('contain', 'Untitled');
});
it('should display the correct Layout duration', () => {
// Verify the duration is correctly displayed
cy.get(layoutDurationSelector)
.should('be.visible')
.and('contain', '00:00');
});
it('should display the correct Layout dimensions', () => {
// Verify the dimensions are correctly displayed
cy.get(layoutDimensionsSelector)
.should('be.visible')
.and('contain', '1920x1080');
});
});

View File

@@ -0,0 +1,181 @@
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Layout Editor Toolbar', function() {
beforeEach(function() {
cy.login();
});
it.skip('should expand and close the toolbox', function() {
cy.visit('/layout/view');
cy.get('button[href="/layout"]').click();
cy.openToolbarMenu(0);
cy.get('.close-content').filter(':visible').click();
});
const setZoomLevel = (level) => {
cy.intercept('POST', '/user/pref').as('updatePreferences');
cy.openToolbarMenu(0);
cy.get('.toolbar-level-control-menu').click();
cy.get('nav.navbar').then(($toolbar) => {
if ($toolbar.hasClass(`toolbar-level-${level}`)) return;
cy.get(`i[data-level="${level}"]`).click();
cy.wait('@updatePreferences');
});
cy.get('nav.navbar').should('have.class', `toolbar-level-${level}`);
};
it.skip('should be able to set zoom level to 1', function() {
cy.visit('/layout/view');
cy.get('button[href="/layout"]').click();
setZoomLevel(1);
});
it.skip('should be able to set zoom level to 2', function() {
cy.visit('/layout/view');
cy.get('button[href="/layout"]').click();
setZoomLevel(2);
});
function searchAndAddElement(tabIndex, keyword, elementSelector, subTypeSelector, paneSelector) {
cy.intercept('POST', '/region/*').as('addRegion');
// Search for the element
cy.toolbarSearch(keyword);
cy.get(paneSelector + '.active')
.find('.toolbar-pane-content')
.find('.toolbar-card')
.should('have.length.greaterThan', 0)
.each(($card) => {
cy.wrap($card)
.find('.card-title')
.should('include.text', keyword);
});
// Add the widget to layout
cy.get(elementSelector).click();
if (subTypeSelector) {
cy.get(subTypeSelector).click();
}
cy.get('.viewer-object').click();
cy.wait('@addRegion').then((interception) => { // todo: error here
expect(interception.response.statusCode).to.eq(200);
});
}
it.skip('should navigate to Widgets tab, search and add a widget', function() {
cy.visit('/layout/view');
cy.get('button[href="/layout"]').click();
// Open the respective toolbar tab
cy.openToolbarMenu(0);
searchAndAddElement(0, 'Clock', '[data-sub-type="clock"]', '[data-sub-type="clock-analogue"]', '.toolbar-widgets-pane');
});
it.skip('should navigate to Global Elements tab, search and add an element', function() {
cy.visit('/layout/view');
cy.get('button[href="/layout"]').click();
searchAndAddElement(1, 'Text', '[data-template-id="text"]', '', '.toolbar-global-pane');
});
function testLibrarySearchAndAddMedia(mediaType, tabIndex, keyword, folderName, mediaTitle) {
cy.intercept('POST', '/user/pref').as('updatePreferences');
cy.intercept('GET', '/folders?start=0&length=10').as('loadFolders');
cy.intercept('GET', '/library/search*').as('librarySearch');
cy.intercept('POST', '/region/*').as('addRegion');
cy.openToolbarMenu(tabIndex);
// Conditionally filter media by Folder if folderName is provided
if (folderName) {
cy.get(`.toolbar-pane.toolbar-${mediaType}-pane.active`)
.find('#input-folder')
.parent()
.find('.select2-selection')
.click();
cy.wait('@loadFolders');
cy.get('.select2-container--open')
.contains(folderName)
.click();
cy.wait('@updatePreferences');
cy.wait('@librarySearch');
}
// Search for a media
cy.toolbarSearchWithActiveFilter(keyword);
cy.get(`.toolbar-pane.toolbar-${mediaType}-pane.active`)
.find('.toolbar-pane-content')
.find('.toolbar-card[data-type="media"]')
.each(($card) => {
cy.wrap($card)
.find('span.media-title')
.should('include.text', keyword);
});
cy.wait('@librarySearch');
cy.wait('@updatePreferences');
// Add media to layout
cy.get(`.toolbar-pane.toolbar-${mediaType}-pane.active`)
.find(`[data-card-title="${mediaTitle}"]`)
.should('exist')
.click();
cy.get('.viewer-object').click();
cy.wait('@addRegion');
}
// Test cases
it.skip('should navigate to Library Image Search tab, filter, search and add media', function() {
testLibrarySearchAndAddMedia('image', 2, 'media_for_search', 'FolderWithImage', 'media_for_search_in_folder');
});
it.skip('should navigate to Library Audio Search tab, filter, search and add media', function() {
testLibrarySearchAndAddMedia('audio', 3, 'test-audio', null, 'test-audio.mp3');
});
it.skip('should navigate to Library Video Search tab, filter, search and add media', function() {
testLibrarySearchAndAddMedia('video', 4, 'test-video', null, 'test-video.mp4');
});
it.skip('should navigate to Interactive Actions tab and search for actions', function() {
const keyword = 'Next';
cy.visit('/layout/view');
cy.get('button[href="/layout"]').click();
cy.openToolbarMenu(7, false);
cy.toolbarSearch(keyword);
cy.get('.toolbar-pane.toolbar-actions-pane.active')
.find('.toolbar-pane-content .toolbar-card')
.each(($card) => {
cy.wrap($card).find('.card-title').should('include.text', keyword);
});
});
});

View File

@@ -0,0 +1,168 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
describe('Layout Designer (Populated/Unchanged)', function() {
before(function() {
// Import existing
// cy.importLayout('../assets/export_test_layout.zip').as('testLayoutId').then((res) => {
// cy.checkoutLayout(res);
// });
});
beforeEach(function() {
cy.login();
// cy.goToLayoutAndLoadPrefs(this.testLayoutId);
});
it.skip('should load all the layout designer elements', function() {
// Check if the basic elements of the designer loaded
cy.get('#layout-editor').should('be.visible');
cy.get('.timeline-panel').should('be.visible');
cy.get('#layout-viewer-container').should('be.visible');
cy.get('#properties-panel').should('be.visible');
});
it.skip('shows widget properties in the properties panel when clicking on a widget in the timeline', function() {
// Select the first widget from the first region on timeline ( image )
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').click();
// Check if the properties panel title is Edit Image
cy.get('#properties-panel').contains('Edit Image');
});
it.skip('should open the playlist editor and be able to show modals', function() {
cy.intercept('GET', '/playlist/widget/form/edit/*').as('reloadWidget');
// Open the playlist editor
cy.get('#layout-timeline .designer-region-info:first .open-playlist-editor').click();
// Wait for the widget to load
cy.wait('@reloadWidget');
// Right click on the first widget in the playlist editor
cy.get('.editor-modal #timeline-container .playlist-widget:first').rightclick();
// Open the delete modal for the first widget
cy.get('.context-menu-overlay .context-menu-widget .deleteBtn').should('be.visible').click();
// Modal should be visible
cy.get('[data-test="deleteObjectModal"]').should('be.visible');
});
it.skip('should revert a saved form to a previous state', () => {
let oldName;
// Create and alias for reload widget
cy.intercept('GET', '/playlist/widget/form/edit/*').as('reloadWidget');
cy.intercept('PUT', '/playlist/widget/*').as('saveWidget');
// Select the first widget on timeline ( image )
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').click();
// Wait for the widget to load
cy.wait('@reloadWidget');
// Get the input field
cy.get('#properties-panel input[name="name"]').then(($input) => {
// Save old name
oldName = $input.val();
// Type the new name in the input
cy.get('#properties-panel input[name="name"]').clear().type('newName');
// Save form
cy.get('#properties-panel button[data-action="save"]').click();
// Should show a notification for the name change
cy.get('.toast-success');
// Wait for the widget to save
cy.wait('@reloadWidget');
// Click the revert button
cy.get('.editor-bottom-bar #undo-btn').click();
// Wait for the widget to save
cy.wait('@saveWidget');
// Test if the revert made the name go back to the old name
cy.get('#properties-panel input[name="name"]').should('have.attr', 'value').and('equal', oldName);
});
});
it.skip('should revert the widgets order when using the undo feature', () => {
cy.intercept('POST', '**/playlist/order/*').as('saveOrder');
cy.intercept('GET', '/layout?layoutId=*').as('reloadLayout');
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').then(($oldWidget) => {
const offsetX = 50;
// Move to the second widget position ( plus offset )
cy.wrap($oldWidget)
.trigger('mousedown', {
which: 1,
})
.trigger('mousemove', {
which: 1,
pageX: $oldWidget.offset().left + $oldWidget.width() * 1.5 + offsetX,
})
.trigger('mouseup', {force: true});
cy.wait('@saveOrder');
// Should show a notification for the order change
cy.get('.toast-success').contains('Order Changed');
// Reload layout and check if the new first widget has a different Id
cy.wait('@reloadLayout');
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').then(($newWidget) => {
expect($oldWidget.attr('id')).not.to.eq($newWidget.attr('id'));
});
// Click the revert button
cy.get('.editor-bottom-bar #undo-btn').click();
// Wait for the order to save
cy.wait('@saveOrder');
cy.wait('@reloadLayout');
// Test if the revert made the name go back to the first widget
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').then(($newWidget) => {
expect($oldWidget.attr('id')).to.eq($newWidget.attr('id'));
});
});
});
it.skip('should play a preview in the viewer', () => {
cy.intercept('GET', '**/region/preview/*').as('loadRegion');
// Wait for the viewer and region to load
cy.get('#layout-viewer-container .viewer-object.layout-player').should('be.visible');
cy.wait('@loadRegion');
// Click play
cy.get('.editor-bottom-bar #play-btn').click();
// Check if the fullscreen iframe has loaded
cy.get('#layout-viewer-container #layout-viewer .viewer-object > iframe').should('be.visible');
});
});

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
describe('Test IA: Toggle Mode ON/OFF', () => {
beforeEach(() => {
cy.login();
// Navigate to Layouts page
cy.visit('/layout/view');
//Click the Add Layout button
cy.get('button.layout-add-button').click();
cy.get('#layout-viewer').should('be.visible');
});
it('should verify default status = OFF and checks the status of IA Mode when toggled to ON or OFF', () => {
//check default IA Mode = OFF
cy.get('li.nav-item.interactive-control')
.should('have.attr', 'data-status', 'off')
.then(($el) => {
cy.wrap($el).click({ force: true })
})
//Toggle Mode = ON
cy.get('li.nav-item.interactive-control')
.should('have.attr', 'data-status', 'on')
.and('contain.text', 'ON')
//Toggle OFF back to Layout Editor
cy.get('li.nav-item.interactive-control').click({ force: true })
cy.get('li.nav-item.interactive-control')
.should('have.attr', 'data-status', 'off')
.and('contain.text', 'OFF')
});
});

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Clock Analogue Widget', function() {
beforeEach(function() {
cy.login();
});
it('should create a new layout and be redirected to the layout designer, add/delete analogue clock', function() {
cy.intercept('/playlist/widget/*').as('saveWidget');
cy.intercept('DELETE', '**/region/**').as('deleteWidget');
cy.intercept('POST', '/user/pref').as('userPref');
cy.visit('/layout/view');
cy.get('button[href="/layout"]').click();
// Open widget menu
cy.openToolbarMenu(0);
cy.get('[data-sub-type="clock"]')
.should('be.visible')
.click();
cy.wait('@userPref');
cy.get('[data-sub-type="clock-analogue"] > .toolbar-card-thumb')
.should('be.visible')
.click();
cy.wait('@userPref');
cy.get('.viewer-object.layout.ui-droppable-active')
.should('be.visible')
.click();
// Check if the widget is in the viewer
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_clock-analogue"]').should('exist');
cy.get('[name="themeId"]').select('Dark', {force: true});
cy.get('[name="offset"]').clear().type('1').trigger('change');
cy.wait('@saveWidget');
cy.get('.widget-form .nav-link[href="#advancedTab"]').click();
// Type the new name in the input
cy.get('#advancedTab input[name="name"]').clear().type('newName');
cy.wait('@saveWidget');
// Set a duration
cy.get('#advancedTab input[name="useDuration"]').check();
cy.wait('@saveWidget');
cy.get('#advancedTab input[name="duration"]').clear().type('12').trigger('change');
cy.wait('@saveWidget');
// Change the background of the layout
cy.get('.viewer-object').click({force: true});
cy.get('[name="backgroundColor"]').clear().type('#ffffff').trigger('change');
// Validate background color changed wo white
cy.get('.viewer-object').should('have.css', 'background-color', 'rgb(255, 255, 255)');
// Check if the name and duration values are the same entered
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_clock-analogue"]').parents('.designer-region').click();
cy.get('.widget-form .nav-link[href="#advancedTab"]').click();
cy.get('#advancedTab input[name="name"]').should('have.attr', 'value').and('equal', 'newName');
cy.get('#advancedTab input[name="duration"]').should('have.attr', 'value').and('equal', '12');
// Delete
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_clock-analogue"]')
.parents('.designer-region')
.rightclick();
// todo -investigate further why this is not working in ci/cdk mode
// cy.get('[data-title="Delete"]').click().then(() => {
// cy.wait('@deleteWidget').its('response.statusCode').should('eq', 200);
// cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_clock-analogue"]')
// .should('not.exist');
// });
});
});

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Dataset', function() {
beforeEach(function() {
cy.login();
});
it('should create a new layout, add/delete dataset widget', function() {
cy.intercept('/dataset?start=*').as('loadDatasets');
cy.intercept('DELETE', '**/region/**').as('deleteWidget');
cy.intercept('POST', '/user/pref').as('userPref');
cy.visit('/layout/view');
cy.get('button[href="/layout"]').click();
// Open widget menu and add dataset widget
cy.openToolbarMenu(0);
cy.get('[data-sub-type="dataset"]')
.should('be.visible')
.click();
cy.wait('@userPref');
cy.get('[data-template-id="dataset_table_1"]')
.should('be.visible')
.click();
cy.wait('@userPref');
cy.get('.viewer-object.layout.ui-droppable-active')
.should('be.visible')
.click();
// Verify widget exists in the layout viewer
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_dataset"]').should('exist');
// Select and configure the dataset
cy.get('#configureTab .select2-selection').click();
cy.wait('@loadDatasets');
cy.get('.select2-container--open input[type="search"]').type('8 items');
cy.get('.select2-container--open').contains('8 items').click();
cy.get('[name="lowerLimit"]').clear().type('1');
cy.get('[name="upperLimit"]').clear().type('10');
cy.get('.order-clause-row > :nth-child(2) > .form-control').select('Col1', {force: true});
cy.get('.order-clause-row > .btn').click();
cy.get(':nth-child(2) > :nth-child(2) > .form-control').select('Col2', {force: true});
// Open Appearance Tab
cy.get('.nav-link[href="#appearanceTab"]').click();
// Ensure dataset has exactly two columns
cy.get('#columnsOut li').should('have.length', 2);
// Move columns to "Columns Selected"
cy.get('#columnsOut li:first').trigger('mousedown', {which: 1}).trigger('mousemove', {which: 1, pageX: 583, pageY: 440});
cy.get('#columnsIn').click();
cy.get('#columnsOut li:first').trigger('mousedown', {which: 1}).trigger('mousemove', {which: 1, pageX: 583, pageY: 440});
cy.get('#columnsIn').click();
// Customize appearance settings
cy.get('[name="showHeadings"]').check();
cy.get('[name="rowsPerPage"]').clear().type('5');
cy.get('[name="fontSize"]').clear().type('48');
cy.get('[name="backgroundColor"]').clear().type('#333333');
// Delete widget
// The .moveable-control-box overlay obstructing the right-click interaction on the designer region, causing the test to fail.
// By invoking .hide(), we remove the overlay temporarily to allow uninterrupted interaction with the underlying elements.
cy.get('.moveable-control-box').invoke('hide');
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_dataset"]')
.parents('.designer-region')
.scrollIntoView()
.should('be.visible')
.rightclick();
// Wait until the widget has been deleted
// cy.get('[data-title="Delete"]').click().then(() => {
// cy.wait('@deleteWidget').its('response.statusCode').should('eq', 200);
// cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_dataset"]')
// .should('not.exist');
// });
});
});

View File

@@ -0,0 +1,91 @@
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Mastodon', function() {
beforeEach(function() {
cy.login();
});
it('should create a new layout and be redirected to the layout designer, add/delete Mastodon widget', function() {
cy.intercept('DELETE', '**/region/**').as('deleteWidget');
cy.intercept('POST', '/user/pref').as('userPref');
cy.visit('/layout/view');
cy.get('button[href="/layout"]').click();
// Open widget menu
cy.openToolbarMenu(0);
cy.get('[data-sub-type="mastodon"]')
.should('be.visible')
.click();
cy.wait('@userPref');
cy.get('[data-template-id="social_media_static_1"] > .toolbar-card-thumb')
.should('be.visible')
.click();
cy.wait('@userPref');
cy.get('.viewer-object.layout.ui-droppable-active')
.should('be.visible')
.click();
// Check if the widget is in the viewer
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_mastodon"]')
.should('exist');
cy.get('[name="hashtag"]').clear();
cy.get('[name="hashtag"]').type('#cat');
cy.get('[name="searchOn"]').select('local', {force: true});
cy.get('[name="numItems"]').clear().type('10').trigger('change');
cy.get('[name="onlyMedia"]').check();
// Click on Appearance Tab
cy.get('.nav-link[href="#appearanceTab"]').click();
cy.get('[name="itemsPerPage"]').clear().type('2').trigger('change');
// Vertical/Fade/100/Right/Bottom
cy.get('[name="displayDirection"]').select('Vertical', {force: true});
cy.get('[name="effect"]').select('Fade', {force: true});
cy.get('[name="speed"]').clear().type('100').trigger('change');
cy.get('[name="alignmentH"]').select('Right', {force: true});
cy.get('[name="alignmentV"]').select('Bottom', {force: true});
// Delete widget
// The .moveable-control-box overlay obstructing the right-click interaction on the designer region, causing the test to fail.
// By invoking .hide(), we remove the overlay temporarily to allow uninterrupted interaction with the underlying elements.
cy.get('.moveable-control-box').invoke('hide');
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_mastodon"]')
.parents('.designer-region')
.scrollIntoView()
.should('be.visible')
.rightclick();
// Wait until the widget has been deleted
// cy.get('[data-title="Delete"]').click().then(() => {
// cy.wait('@deleteWidget').its('response.statusCode').should('eq', 200);
// cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_mastodon"]')
// .should('not.exist');
// });
});
});

View File

@@ -0,0 +1,101 @@
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('RSS Ticker', function() {
beforeEach(function() {
cy.login();
});
it('should create a new layout and be redirected to the layout designer, add/delete RSS ticker widget', function() {
cy.intercept('DELETE', '**/region/**').as('deleteWidget');
cy.intercept('POST', '/user/pref').as('userPref');
cy.visit('/layout/view');
cy.get('button[href="/layout"]').click();
// Open widget menu
cy.openToolbarMenu(0);
cy.get('[data-sub-type="rss-ticker"]')
.scrollIntoView()
.should('be.visible')
.click();
cy.wait('@userPref');
cy.get('[data-template-id="article_image_only"] > .toolbar-card-thumb')
.scrollIntoView()
.should('be.visible')
.click();
cy.wait('@userPref');
cy.get('.viewer-object.layout.ui-droppable-active')
.should('be.visible')
.click();
// Check if the widget is in the viewer
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_rss-ticker"]').should('exist');
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_rss-ticker"]').parents('.designer-region').click();
// Validate if uri is not provide we show an error message
cy.get('[name="numItems"]').clear().type('10').trigger('change');
cy.get('.form-container').contains('Missing required property Feed URL');
cy.get('[name="uri"]').clear();
cy.get('[name="uri"]').type('http://xibo.org.uk/feed');
cy.get('[name="numItems"]').clear().type('10').trigger('change');
cy.get('[name="durationIsPerItem"]').check();
cy.get('[name="takeItemsFrom"]').select('End of the Feed', {force: true});
cy.get('[name="reverseOrder"]').check();
cy.get('[name="randomiseItems"]').check();
cy.get('[name="userAgent"]').clear().type('Mozilla/5.0');
cy.get('[name="updateInterval"]').clear().type('10').trigger('change');
// Click on Appearance Tab
cy.get('.nav-link[href="#appearanceTab"]').click();
cy.get('[name="backgroundColor"]').clear().type('#dddddd');
cy.get('[name="itemImageFit"]').select('Fill', {force: true});
cy.get('[name="effect"]').select('Fade', {force: true});
cy.get('[name="speed"]').clear().type('500');
// Update CKEditor value
cy.updateCKEditor('noDataMessage', 'No data to show');
cy.get('[name="copyright"]').clear().type('Xibo').trigger('change');
// Delete widget
// The .moveable-control-box overlay obstructing the right-click interaction on the designer region, causing the test to fail.
// By invoking .hide(), we remove the overlay temporarily to allow uninterrupted interaction with the underlying elements.
cy.get('.moveable-control-box').invoke('hide');
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_rss-ticker"]')
.parents('.designer-region')
.scrollIntoView()
.should('be.visible')
.rightclick();
// Wait until the widget has been deleted
// cy.get('[data-title="Delete"]').click().then(() => {
// cy.wait('@deleteWidget').its('response.statusCode').should('eq', 200);
// cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_rss-ticker"]')
// .should('not.exist');
// });
});
});

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Layout View', function() {
beforeEach(function() {
cy.login();
});
it('searches and delete existing layout', function() {
// Create random name
const uuid = Cypress._.random(0, 1e10);
// Create a new layout and go to the layout's designer page, then load toolbar prefs
cy.createLayout(uuid).as('testLayoutId').then((res) => {
cy.intercept('GET', '/layout?draw=2&*').as('layoutGridLoad');
cy.visit('/layout/view');
// Filter for the created layout
cy.get('#Filter input[name="layout"]')
.type(uuid);
// Wait for the layout grid reload
cy.wait('@layoutGridLoad');
// Click on the first row element to open the designer
cy.get('#layouts tr:first-child .dropdown-toggle').click({force: true});
cy.get('#layouts tr:first-child .layout_button_delete').click({force: true});
// Delete test layout
cy.get('.bootbox .save-button').click();
// Check if layout is deleted in toast message
cy.get('.toast').contains('Deleted ' + uuid);
});
});
});

View File

@@ -0,0 +1,322 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Datasets', function() {
let testRun = '';
beforeEach(function() {
cy.login();
testRun = Cypress._.random(0, 1e9);
});
it('should add one empty dataset', function() {
cy.visit('/dataset/view');
// Click on the Add Dataset button
cy.contains('Add DataSet').click();
cy.get('.modal input#dataSet')
.type('Cypress Test Dataset ' + testRun + '_1');
// Add first by clicking next
cy.get('.modal .save-button').click();
// Check if dataset is added in toast message
cy.contains('Added Cypress Test Dataset ' + testRun + '_1');
});
it('should be able to cancel creating dataset', function() {
cy.visit('/dataset/view');
// Click on the Add Dataset button
cy.contains('Add DataSet').click();
cy.get('.modal input#dataSet')
.type('Cypress Test Dataset ' + testRun + '_1');
// Click cancel
cy.get('.modal #dialog_btn_1').click();
// Check if you are back to the view page
cy.url().should('include', '/dataset/view');
});
it('searches and edit existing dataset', function() {
// Create a new dataset and then search for it and delete it
cy.createDataset('Cypress Test Dataset ' + testRun).then((id) => {
cy.intercept({
url: '/dataset?*',
query: {dataSet: 'Cypress Test Dataset ' + testRun},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/dataset/*',
}).as('putRequest');
cy.visit('/dataset/view');
// Filter for the created dataset
cy.get('#Filter input[name="dataSet"]')
.type('Cypress Test Dataset ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#datasets tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#datasets tr:first-child .dropdown-toggle').click({force: true});
cy.get('#datasets tr:first-child .dataset_button_edit').click({force: true});
cy.get('.modal input#dataSet').clear()
.type('Cypress Test Dataset Edited ' + testRun);
// edit test dataset
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
// assertion on the "dataset" value
expect(responseData.dataSet).to.eq('Cypress Test Dataset Edited ' + testRun);
});
// Delete the dataset and assert success
cy.deleteDataset(id).then((res) => {
expect(res.status).to.equal(204);
});
});
});
it('add row/column to an existing dataset', function() {
// Create a new dataset and then search for it and delete it
cy.createDataset('Cypress Test Dataset ' + testRun).then((id) => {
cy.intercept({
url: '/dataset?*',
query: {dataSet: 'Cypress Test Dataset ' + testRun},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'POST',
url: /\/dataset\/\d+\/column$/,
}).as('postRequestAddColumn');
cy.intercept({
method: 'POST',
url: /\/dataset\/data\/\d+/,
}).as('postRequestAddRow');
cy.visit('/dataset/view');
// Filter for the created dataset
cy.get('#Filter input[name="dataSet"]')
.type('Cypress Test Dataset ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#datasets tbody tr').should('have.length', 1);
// Click on the first row element to open the View data
cy.get('#datasets tr:first-child .dropdown-toggle').click({force: true});
cy.get('#datasets tr:first-child .dataset_button_viewcolumns').click({force: true});
cy.get('#datasets').contains('No data available in table');
// Add data row to dataset
cy.contains('Add Column').click();
cy.get('.modal input#heading').type('Col1');
// Save
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@postRequestAddColumn').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
// assertion on the "dataset" value
expect(responseData.heading).to.eq('Col1');
cy.contains('View Data').click();
cy.get('#datasets').contains('No data available in table');
// Add data row to dataset
cy.contains('Add Row').click();
cy.get('#dataSetDataAdd').within(() => {
cy.get('input:first').type('Your text goes here');
});
// Save
cy.get('.bootbox .save-button').click();
// Wait for the intercepted request and check data
cy.wait('@postRequestAddRow').then((interception) => {
cy.contains('Added Row');
});
});
// Now try to delete the dataset
cy.visit('/dataset/view');
// Filter for the created dataset
cy.get('#Filter input[name="dataSet"]')
.type('Cypress Test Dataset ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#datasets tbody tr').should('have.length', 1);
// Click on the first row element to open the View data
cy.get('#datasets tr:first-child .dropdown-toggle').click({force: true});
cy.get('#datasets tr:first-child .dataset_button_delete').click({force: true});
});
});
it('copy an existing dataset', function() {
// Create a new dataset and then search for it and copy it
cy.createDataset('Cypress Test Dataset ' + testRun).then((res) => {
cy.intercept({
url: '/dataset?*',
query: {dataSet: 'Cypress Test Dataset ' + testRun},
}).as('loadGridAfterSearch');
// Intercept the POST request
cy.intercept({
method: 'POST',
url: /\/dataset\/copy\/\d+/,
}).as('postRequest');
cy.visit('/dataset/view');
// Filter for the created dataset
cy.get('#Filter input[name="dataSet"]')
.type('Cypress Test Dataset ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#datasets tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#datasets tr:first-child .dropdown-toggle').click({force: true});
cy.get('#datasets tr:first-child .dataset_button_copy').click({force: true});
// save
cy.get('.bootbox .save-button').click();
// Wait for the intercepted POST request and check the form data
cy.wait('@postRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
expect(responseData.dataSet).to.include('Cypress Test Dataset ' + testRun + ' 2');
});
});
});
it('searches and delete existing dataset', function() {
// Create a new dataset and then search for it and delete it
cy.createDataset('Cypress Test Dataset ' + testRun).then((res) => {
cy.intercept('GET', '/dataset?draw=2&*').as('datasetGridLoad');
cy.visit('/dataset/view');
// Filter for the created dataset
cy.get('#Filter input[name="dataSet"]')
.type('Cypress Test Dataset ' + testRun);
// Wait for the grid reload
cy.wait('@datasetGridLoad');
cy.get('#datasets tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#datasets tr:first-child .dropdown-toggle').click({force: true});
cy.get('#datasets tr:first-child .dataset_button_delete').click({force: true});
// Delete test dataset
cy.get('.bootbox .save-button').click();
// Check if dataset is deleted in toast message
cy.get('.toast').contains('Deleted Cypress Test Dataset');
});
});
it('selects multiple datasets and delete them', function() {
// Create a new dataset and then search for it and delete it
cy.createDataset('Cypress Test Dataset ' + testRun).then((res) => {
cy.intercept('GET', '/dataset?draw=2&*').as('datasetGridLoad');
// Delete all test datasets
cy.visit('/dataset/view');
// Clear filter
cy.get('#Filter input[name="dataSet"]')
.clear()
.type('Cypress Test Dataset');
// Wait for the grid reload
cy.wait('@datasetGridLoad');
// Select all
cy.get('button[data-toggle="selectAll"]').click();
// Delete all
cy.get('.dataTables_info button[data-toggle="dropdown"]').click();
cy.get('.dataTables_info a[data-button-id="dataset_button_delete"]').click();
cy.get('input#deleteData').check();
cy.get('button.save-button').click();
// Modal should contain one successful delete at least
cy.get('.modal-body').contains(': Success');
});
});
// ---------
// Tests - Error handling
it('should not add a remote dataset without URI', function() {
cy.visit('/dataset/view');
// Click on the Add Dataset button
cy.contains('Add DataSet').click();
cy.get('.modal input#dataSet')
.type('Cypress Test Dataset ' + testRun);
cy.get('.modal input#isRemote').check();
// Add first by clicking next
cy.get('.modal .save-button').click();
// Click on the "Remote" tab
cy.get(':nth-child(2) > .nav-link').should('be.visible').click();
// Check that the error message is displayed for the missing URI field
cy.get('#uri-error').should('have.text', 'This field is required.');
});
});

View File

@@ -0,0 +1,111 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Media Admin', function() {
let testRun;
beforeEach(function() {
cy.login();
testRun = Cypress._.random(0, 1e9);
});
it('should add a media via url', function() {
cy.visit('/library/view');
// Click on the Add Playlist button
cy.contains('Add media (URL)').click();
cy.get('#url')
.type('https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4');
cy.get('#optionalName')
.type('Cypress Test Media ' + testRun);
cy.get('.modal .save-button').click();
cy.wait(24000);
// Filter for the created playlist
cy.get('#media')
.type('Cypress Test Media ' + testRun);
// Should have the added playlist
cy.get('#libraryItems tbody tr').should('have.length', 1);
cy.get('#libraryItems tbody tr:nth-child(1) td:nth-child(2)').contains('Cypress Test Media ' + testRun);
});
it('should cancel adding a media', function() {
cy.visit('/library/view');
// Click on the Add Playlist button
cy.contains('Add media (URL)').click();
cy.get('#url')
.type('https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4');
cy.get('#optionalName')
.type('Cypress Test Media ' + testRun);
// Click cancel
cy.get('#dialog_btn_1').click();
// Check if you are back to the view page
cy.url().should('include', '/library/view');
});
it('should show a list of Media', function() {
// Wait for playlist grid reload
cy.intercept('/library?draw=1&*').as('mediaGridLoad');
cy.visit('/library/view').then(function() {
cy.wait('@mediaGridLoad');
cy.get('#libraryItems');
});
});
it.skip('selects media and delete them', function() {
// Create a new playlist and then search for it and delete it
cy.intercept('/library?draw=1&*').as('mediaGridLoad');
// Delete all test playlists
cy.visit('/library/view');
// Clear filter and search for text playlists
cy.get('#media')
.clear()
.type('Cypress Test Media');
// Wait for 1st playlist grid reload
cy.wait('@mediaGridLoad');
// Select first entry
cy.get('table#libraryItems').contains('Cypress Test Media').parents('tr.odd').should('be.visible').click();
cy.get('button[data-toggle="dropdown"]').first().click();
// Click Delete
cy.contains('Delete').click();
cy.get('button.save-button').click();
// Modal should contain one successful delete at least
cy.get('div[class="toast-message"]').should('contain', 'Deleted');
});
});

View File

@@ -0,0 +1,439 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Menuboards', function() {
let testRun = '';
beforeEach(function() {
cy.login();
testRun = Cypress._.random(0, 1e9);
});
it('should add a menuboard', function() {
cy.visit('/menuboard/view');
// Click on the Add Menuboard button
cy.contains('Add Menu Board').click();
cy.get('.modal input#name')
.type('Cypress Test Menuboard ' + testRun + '_1');
cy.get('.modal input#code')
.type('MENUBOARD');
cy.get('.modal textarea#description')
.type('Menuboard Description');
// Add first by clicking next
cy.get('.modal .save-button').click();
// Check if menuboard is added in toast message
cy.contains('Added Menu Board');
});
it('searches and edit existing menuboard', function() {
// Create a new menuboard and then search for it and delete it
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((res) => {
cy.intercept({
url: '/menuboard?*',
query: {name: 'Cypress Test Menuboard ' + testRun},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/menuboard/*',
}).as('putRequest');
cy.visit('/menuboard/view');
// Filter for the created menuboard
cy.get('#Filter input[name="name"]')
.type('Cypress Test Menuboard ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#menuBoards tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#menuBoards tr:first-child .dropdown-toggle').click({force: true});
cy.get('#menuBoards tr:first-child .menuBoard_edit_button').click({force: true});
cy.get('.modal input#name').clear()
.type('Cypress Test Menuboard Edited ' + testRun);
// edit test menuboard
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
// assertion on the "menuboard" value
expect(responseData.name).to.eq('Cypress Test Menuboard Edited ' + testRun);
});
});
});
it('searches and delete existing menuboard', function() {
// Create a new menuboard and then search for it and delete it
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((res) => {
cy.intercept({
url: '/menuboard?*',
query: {name: 'Cypress Test Menuboard ' + testRun},
}).as('loadGridAfterSearch');
cy.visit('/menuboard/view');
// Filter for the created menuboard
cy.get('#Filter input[name="name"]')
.type('Cypress Test Menuboard ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#menuBoards tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#menuBoards tr:first-child .dropdown-toggle').click({force: true});
cy.get('#menuBoards tr:first-child .menuBoard_delete_button').click({force: true});
// Delete test menuboard
cy.get('.bootbox .save-button').click();
// Check if menuboard is deleted in toast message
cy.get('.toast').contains('Deleted Cypress Test Menuboard');
});
});
// -------------------
it('should add categories and products to a menuboard', function() {
// Create a new menuboard and then search for it and delete it
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((menuId) => {
cy.intercept({
url: '/menuboard?*',
query: {name: 'Cypress Test Menuboard ' + testRun},
}).as('loadGridAfterSearch');
cy.visit('/menuboard/view');
// Filter for the created menuboard
cy.get('#Filter input[name="name"]')
.type('Cypress Test Menuboard ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#menuBoards tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#menuBoards tr:first-child .dropdown-toggle').click({force: true});
cy.get('#menuBoards tr:first-child .menuBoard_button_viewcategories').click({force: true});
// Click on the Add Category button
cy.contains('Add Category').click();
cy.get('.modal input#name')
.type('Cypress Test Category ' + testRun + '_1');
cy.get('.modal input#code')
.type('MENUBOARDCAT');
// Add first by clicking next
cy.get('.modal .save-button').click();
// Check if menuboard is added in toast message
cy.contains('Added Menu Board Category');
// Wait for the grid reload
// cy.wait('@loadCategoryGridAfterSearch');
// Click on the first row element to open the delete modal
cy.get('#menuBoardCategories tr:first-child .dropdown-toggle').click({force: true});
cy.get('#menuBoardCategories tr:first-child .menuBoardCategory_button_viewproducts').click({force: true});
// Click on the Add Product button
cy.contains('Add Product').click();
cy.get('.modal input#name')
.type('Cypress Test Product ' + testRun + '_1');
cy.get('.modal input#code')
.type('MENUBOARDPROD');
// Add first by clicking next
cy.get('.modal .save-button').click();
// Check if menuboard is added in toast message
cy.contains('Added Menu Board Product');
});
});
// -------------------
// Categories
it('should add a category', function() {
// Create a new menuboard and then search for it and delete it
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((menuId) => {
// GO to products page
cy.visit('/menuboard/' + menuId + '/categories/view');
// Click on the Add Category button
cy.contains('Add Category').click();
cy.get('.modal input#name')
.type('Cypress Test Category ' + testRun + '_1');
cy.get('.modal input#code')
.type('MENUBOARDCAT');
// Add first by clicking next
cy.get('.modal .save-button').click();
// Check toast message
cy.contains('Added Menu Board Category');
// Delete the menuboard and assert success
cy.deleteMenuboard(menuId).then((response) => {
expect(response.status).to.equal(204);
});
});
});
it('searches and edit existing category', function() {
// Create a new menuboard and then search for it and delete it
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((menuId) => {
cy.createMenuboardCat('Cypress Test Category ' + testRun, menuId).then((menuCatId) => {
cy.intercept({
url: '/menuboard/' + menuId + '/categories?*',
query: {name: 'Cypress Test Category ' + testRun},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/menuboard/' + menuCatId + '/category',
}).as('putRequest');
// GO to products page
cy.visit('/menuboard/' + menuId + '/categories/view');
// Filter for the created menuboard
cy.get('#Filter input[name="name"]')
.type('Cypress Test Category ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#menuBoardCategories tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#menuBoardCategories tr:first-child .dropdown-toggle').click({force: true});
cy.get('#menuBoardCategories tr:first-child .menuBoardCategory_edit_button').click({force: true});
// EDIT
cy.get('.modal input#name').clear()
.type('Cypress Test Category Edited ' + testRun);
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
// assertion on the "menuboard" value
expect(responseData.name).to.eq('Cypress Test Category Edited ' + testRun);
});
// Delete the menuboard and assert success
cy.deleteMenuboard(menuId).then((response) => {
expect(response.status).to.equal(204);
});
});
});
});
it('searches and delete existing category', function() {
// Create a new menuboard and then search for it and delete it
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((menuId) => {
cy.createMenuboardCat('Cypress Test Category ' + testRun, menuId).then((menuCatId) => {
cy.intercept({
url: '/menuboard/' + menuId + '/categories?*',
query: {name: 'Cypress Test Category ' + testRun},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/menuboard/' + menuCatId + '/category',
}).as('putRequest');
// GO to products page
cy.visit('/menuboard/' + menuId + '/categories/view');
// Filter for the created menuboard
cy.get('#Filter input[name="name"]')
.type('Cypress Test Category ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#menuBoardCategories tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#menuBoardCategories tr:first-child .dropdown-toggle').click({force: true});
cy.get('#menuBoardCategories tr:first-child .menuBoardCategory_delete_button').click({force: true});
// Delete test category
cy.get('.bootbox .save-button').click();
// Check toast message
cy.get('.toast').contains('Deleted Cypress Test Category');
// Delete the menuboard and assert success
cy.deleteMenuboard(menuId).then((response) => {
expect(response.status).to.equal(204);
});
});
});
});
// -------------------
// Products
it('should add a product', function() {
// Create a new menuboard and then search for it and delete it
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((menuId) => {
cy.createMenuboardCat('Cypress Test Category ' + testRun, menuId).then((menuCatId) => {
// GO to products page
cy.visit('/menuboard/' + menuCatId + '/products/view');
// Click on the Add Product button
cy.contains('Add Product').click();
cy.get('.modal input#name')
.type('Cypress Test Product ' + testRun);
cy.get('.modal input#code')
.type('MENUBOARDPROD');
// Add first by clicking next
cy.get('.modal .save-button').click();
// Check if menuboard is added in toast message
cy.contains('Added Menu Board Product');
});
});
});
it('searches and edit existing product', function() {
// Create a new menuboard and then search for it and delete it
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((menuId) => {
cy.log(menuId);
cy.createMenuboardCat('Cypress Test Category ' + testRun, menuId).then((menuCatId) => {
cy.log(menuCatId);
cy.createMenuboardCatProd('Cypress Test Product ' + testRun, menuCatId).then((menuProdId) => {
cy.log(menuProdId);
cy.intercept({
url: '/menuboard/' + menuCatId + '/products?*',
query: {name: 'Cypress Test Product ' + testRun},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/menuboard/' + menuProdId + '/product',
}).as('putRequest');
// GO to products page
cy.visit('/menuboard/' + menuCatId + '/products/view');
// Filter for the created menuboard
cy.get('#Filter input[name="name"]')
.type('Cypress Test Product ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#menuBoardProducts tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#menuBoardProducts tr:first-child .dropdown-toggle').click({force: true});
cy.get('#menuBoardProducts tr:first-child .menuBoardProduct_edit_button').click({force: true});
// EDIT
cy.get('.modal input#name').clear()
.type('Cypress Test Product Edited ' + testRun);
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
// assertion on the "menuboard" value
expect(responseData.name).to.eq('Cypress Test Product Edited ' + testRun);
});
// Delete the menuboard and assert success
cy.deleteMenuboard(menuId).then((response) => {
expect(response.status).to.equal(204);
});
});
});
});
});
it('searches and delete existing product', function() {
// Create a new menuboard and then search for it and delete it
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((menuId) => {
cy.createMenuboardCat('Cypress Test Category ' + testRun, menuId).then((menuCatId) => {
cy.createMenuboardCatProd('Cypress Test Product ' + testRun, menuCatId).then((menuProdId) => {
cy.intercept({
url: '/menuboard/' + menuCatId + '/products?*',
query: {name: 'Cypress Test Product ' + testRun},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/menuboard/' + menuProdId + '/product',
}).as('putRequest');
// GO to products page
cy.visit('/menuboard/' + menuCatId + '/products/view');
// Filter for the created menuboard
cy.get('#Filter input[name="name"]')
.type('Cypress Test Product ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#menuBoardProducts tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#menuBoardProducts tr:first-child .dropdown-toggle').click({force: true});
cy.get('#menuBoardProducts tr:first-child .menuBoardProduct_delete_button').click({force: true});
// Delete test menuboard
cy.get('.bootbox .save-button').click();
// Check toast message
cy.get('.toast').contains('Deleted Cypress Test Product');
// Delete the menuboard and assert success
cy.deleteMenuboard(menuId).then((response) => {
expect(response.status).to.equal(204);
});
});
});
});
});
});

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
describe('Playlist Editor (Empty)', function() {
beforeEach(function() {
cy.login();
// Create random name
let uuid = Cypress._.random(0, 1e9);
// Create a new layout and go to the layout's designer page
cy.createNonDynamicPlaylist(uuid).as('testPlaylistId').then((res) => {
cy.openPlaylistEditorAndLoadPrefs(res);
});
});
it('should show the droppable zone and toolbar', function() {
cy.get('#playlist-editor-container').should('be.visible');
cy.get('div[class="container-toolbar container-fluid flex-column flex-column justify-content-between"]').should('be.visible');
});
});

View File

@@ -0,0 +1,208 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
describe('Playlist Editor (Populated)', function() {
beforeEach(function() {
cy.login();
// Create random name
let uuid = Cypress._.random(0, 1e9);
// Create a new layout and go to the layout's designer page
cy.createNonDynamicPlaylist(uuid).as('testPlaylistId').then((res) => {
// Populate playlist with some widgets and media
cy.addWidgetToPlaylist(res, 'embedded', {
name: 'Embedded Widget'
});
cy.addMediaToLibrary("file/example.zip");
cy.addWidgetToPlaylist(res, 'clock', {
name: 'Clock Widget'
});
cy.openPlaylistEditorAndLoadPrefs(res);
});
});
it('changes and saves widget properties', () => {
// Create and alias for reload widget
// cy.intercept('GET','/playlist/widget/form/edit/*').as('reloadWidget');
// Select the first widget on timeline ( image )
cy.get('#timeline-container [data-type="widget"]').first().click();
// Wait for the widget to load
// cy.wait('@reloadWidget');
// Type the new name in the input
cy.get('a[href="#advancedTab"]').click();
cy.get('#properties-panel-form-container input[name="name"]').clear().type('newName');
// Set a duration
cy.get('#properties-panel-form-container input[name="useDuration"]').check();
cy.get('#properties-panel-form-container input[name="duration"]').clear().type(12);
// Save form
cy.get('#properties-panel-form-container button[data-action="save"]').click();
// Should show a notification for the name change
// cy.get('.toast-success');
// Wait for the widget to reload
// cy.wait('@reloadWidget');
// Check if the values are the same entered after reload
cy.get('#properties-panel-form-container input[name="name"]').should('have.prop', 'value').and('equal', 'newName');
cy.get('#properties-panel-form-container input[name="duration"]').should('have.prop', 'value').and('equal', '12');
});
it.skip('should revert a saved form to a previous state', () => {
let oldName;
// Create and alias for reload widget
// cy.intercept('GET', '/playlist/widget/form/edit/*').as('reloadWidget');
// cy.intercept('PUT', '/playlist/widget/*').as('saveWidget');
// Select the first widget on timeline ( image )
cy.get('#timeline-container [data-type="widget"]').first().click();
// Wait for the widget to load
// cy.wait('@reloadWidget');
// Get the input field
cy.get('a[href="#advancedTab"]').click();
cy.get('#properties-panel-form-container input[name="name"]').then(($input) => {
// Save old name
oldName = $input.val();
//Type the new name in the input
cy.get('#properties-panel-form-container input[name="name"]').clear().type('newName');
// Save form
cy.get('#properties-panel-form-container button[data-action="save"]').click();
// Should show a notification for the name change
// cy.get('.toast-success');
// Wait for the widget to save
// cy.wait('@reloadWidget');
// Click the revert button
cy.get('#playlist-editor-toolbar #undoContainer').click();
// Wait for the widget to save
// cy.wait('@saveWidget');
// Test if the revert made the name go back to the old name
cy.get('#properties-panel-form-container input[name="name"]').should('have.prop', 'value').and('equal', oldName);
});
});
it('should delete a widget using the toolbar bin', () => {
// cy.intercept('/playlist?playlistId=*').as('reloadPlaylist');
// Select a widget from the navigator
cy.get('#playlist-timeline [data-type="widget"]').first().click().then(($el) => {
const widgetId = $el.attr('id');
// Click trash container
cy.get('div[class="widgetDelete"]').first().click({force: true});
// Confirm delete on modal
cy.get('button[class*="btn-bb-confirm"]').click();
// Check toast message
// cy.get('.toast-success').contains('Deleted');
// Wait for the layout to reload
// cy.wait('@reloadPlaylist');
// Check that widget is not on timeline
cy.get('#playlist-timeline [data-type="widget"]#' + widgetId).should('not.exist');
});
});
it.skip('should add an audio clip to a widget by the context menu, and adds a link to open the form in the timeline', () => {
cy.populateLibraryWithMedia();
// Create and alias for reload playlist
cy.intercept('/playlist?playlistId=*').as('reloadPlaylist');
// Right click to open the context menu and select add audio
cy.get('#timeline-container [data-type="widget"]').first().should('be.visible').rightclick();
cy.get('.context-menu-btn[data-property="Audio"]').should('be.visible').click();
// Select the 1st option
cy.get('[data-test="widgetPropertiesForm"] #mediaId > option').eq(1).then(($el) => {
cy.get('[data-test="widgetPropertiesForm"] #mediaId').select($el.val());
});
// Save and close the form
cy.get('[data-test="widgetPropertiesForm"] .btn-bb-done').click();
// Check if the widget has the audio icon
// cy.wait('@reloadPlaylist');
cy.get('#timeline-container [data-type="widget"]:first-child')
.find('i[data-property="Audio"]').should('exist').click({force: true});
cy.get('[data-test="widgetPropertiesForm"]').contains('Audio for');
});
// Skip test for now ( it's failing in the test suite and being tested already in layout designer spec )
it.skip('attaches expiry dates to a widget by the context menu, and adds a link to open the form in the timeline', () => {
// Create and alias for reload playlist
// cy.intercept('/playlist?playlistId=*').as('reloadPlaylist');
// Right click to open the context menu and select add audio
cy.get('#timeline-container [data-type="widget"]').first().should('be.visible').rightclick();
cy.get('.context-menu-btn[data-property="Expiry"]').should('be.visible').click();
// Add dates
cy.get('[data-test="widgetPropertiesForm"] .starttime-control .date-clear-button').click();
// cy.get('[data-test="widgetPropertiesForm"] #fromDt').find('input[class="datePickerHelper form-control dateControl dateTime active"]').click();
cy.get('div[class="flatpickr-wrapper"]').first().click();
cy.get('.flatpickr-calendar.open .dayContainer .flatpickr-day:first').click();
cy.get('[data-test="widgetPropertiesForm"] .endtime-control .date-clear-button').click();
// cy.get('[data-test="widgetPropertiesForm"] #toDt').find('input[class="datePickerHelper form-control dateControl dateTime active"]').click();
cy.get('div[class="flatpickr-wrapper"]').last().click();
cy.get('.flatpickr-calendar.open .dayContainer .flatpickr-day:first').click();
// Save and close the form
cy.get('[data-test="widgetPropertiesForm"] .btn-bb-done').click();
// Check if the widget has the expiry dates icon
// cy.wait('@reloadPlaylist');
cy.get('#timeline-container [data-type="widget"]:first-child')
.find('i[data-property="Expiry"]').should('exist').click({force: true});
cy.get('[data-test="widgetPropertiesForm"]').contains('Expiry for');
});
});

View File

@@ -0,0 +1,110 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
describe('Playlist Editor (Populated/Unchanged)', function() {
before(function() {
cy.login();
// Create random name
let uuid = Cypress._.random(0, 1e9);
// Create a new layout and go to the layout's designer page
cy.createNonDynamicPlaylist(uuid).as('testPlaylistId').then((res) => {
// Populate playlist with some widgets and media
cy.addWidgetToPlaylist(res, 'embedded', {
name: 'Embedded Widget'
});
// TODO skip so that the test success
// cy.addRandomMediaToPlaylist(res);
cy.addWidgetToPlaylist(res, 'clock', {
name: 'Clock Widget'
});
});
});
beforeEach(function() {
cy.login();
cy.openPlaylistEditorAndLoadPrefs(this.testPlaylistId);
});
it('opens a media tab in the toolbar and searches for items', () => {
// cy.intercept('/library/search?*').as('mediaLoad');
cy.populateLibraryWithMedia();
// Open audio tool tab
cy.get('a[id="btn-menu-3"]').should('be.visible').click();
// cy.wait('@mediaLoad');
// Check if there are audio items in the search content
cy.get('div[class="toolbar-card-preview"]').last().should('be.visible');
});
it('creates a new widget by selecting a searched media from the toolbar to the editor, and then reverts the change', () => {
cy.populateLibraryWithMedia();
// Create and alias for reload playlist
// cy.intercept('/playlist?playlistId=*').as('reloadPlaylist');
// cy.intercept('DELETE', '/playlist/widget/*').as('deleteWidget');
// cy.intercept('/library/search?*').as('mediaLoad');
// Open library search tab
cy.get('a[id="btn-menu-0"]').should('be.visible').click();
// cy.wait('@mediaLoad');
cy.wait(1000);
// Get a table row, select it and add to the dropzone
cy.get('div[class="toolbar-card ui-draggable ui-draggable-handle"]').eq(2).should('be.visible').click({force: true}).then(() => {
cy.get('#timeline-overlay-container').click({force: true}).then(() => {
// Wait for the layout to reload
// cy.wait('@reloadPlaylist');
cy.wait(3000);
// Check if there is just one widget in the timeline
cy.get('#timeline-container [data-type="widget"]').then(($widgets) => {
expect($widgets.length).to.eq(3);
});
// Click the revert button
cy.get('#timeline-container [id^="widget_"]').last().click();
cy.get('button[data-action="undo"]').click({force: true});
// Wait for the widget to be deleted and for the playlist to reload
// cy.wait('@deleteWidget');
// cy.wait('@reloadPlaylist');
cy.wait(3000);
// Check if there is just one widget in the timeline
cy.get('#timeline-container [data-type="widget"]').then(($widgets) => {
expect($widgets.length).to.eq(2);
});
});
});
});
});

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Playlists Admin', function() {
let testRun;
beforeEach(function() {
cy.login();
testRun = Cypress._.random(0, 1e9);
});
it('should add a non-dynamic playlist', function() {
cy.visit('/playlist/view');
// Click on the Add Playlist button
cy.contains('Add Playlist').click();
cy.get('.modal input#name')
.type('Cypress Test Playlist ' + testRun);
cy.get('.modal .save-button').click();
// Filter for the created playlist
cy.get('#Filter input[name="name"]')
.type('Cypress Test Playlist ' + testRun);
// Should have the added playlist
cy.get('#playlists tbody tr').should('have.length', 1);
cy.get('#playlists tbody tr:nth-child(1) td:nth-child(2)').contains('Cypress Test Playlist ' + testRun);
});
it('should cancel adding a non-dynamic playlist', function() {
cy.visit('/playlist/view');
// Click on the Add Playlist button
cy.contains('Add Playlist').click();
cy.get('.modal input#name')
.type('Cypress Test Playlist ' + testRun);
// Click cancel
cy.get('#dialog_btn_1').click();
// Check if you are back to the view page
cy.url().should('include', '/playlist/view');
});
it('should show a list of Playlists', function() {
// Wait for playlist grid reload
cy.intercept('/playlist?draw=1&*').as('playlistGridLoad');
cy.visit('/playlist/view').then(function() {
cy.wait('@playlistGridLoad');
cy.get('#playlists');
});
});
it('selects multiple playlists and delete them', function() {
// Create a new playlist and then search for it and delete it
cy.createNonDynamicPlaylist('Cypress Test Playlist ' + testRun).then(() => {
cy.intercept('/playlist?draw=2&*').as('playlistGridLoad');
// Delete all test playlists
cy.visit('/playlist/view');
// Clear filter and search for text playlists
cy.get('#Filter input[name="name"]')
.clear()
.type('Cypress Test Playlist');
// Wait for 2nd playlist grid reload
cy.wait('@playlistGridLoad');
// Select all
cy.get('button[data-toggle="selectAll"]').click();
// Delete all
cy.get('.dataTables_info button[data-toggle="dropdown"]').click({force: true});
cy.get('.dataTables_info a[data-button-id="playlist_button_delete"]').click({force: true});
cy.get('button.save-button').click();
// Modal should contain one successful delete at least
cy.get('.modal-body').contains(': Success');
});
});
});

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Bandwidth', function() {
const display1 = 'POP Display 1';
beforeEach(function() {
cy.login();
});
it('should load tabular data and charts', () => {
// Create and alias for load Display
cy.intercept({
url: '/display?start=*',
query: {display: display1},
}).as('loadDisplayAfterSearch');
cy.intercept('/report/data/bandwidth?*').as('reportData');
cy.visit('/report/form/bandwidth');
// Click on the select2 selection
cy.get('#displayId + span .select2-selection').click();
cy.get('.select2-container--open input[type="search"]').type(display1);
cy.wait('@loadDisplayAfterSearch');
cy.selectOption(display1);
// Click on the Apply button
cy.contains('Apply').should('be.visible').click();
cy.get('.chart-container').should('be.visible');
// Click on Tabular
cy.contains('Tabular').should('be.visible').click();
cy.wait('@reportData');
// Should have media stats
cy.get('#bandwidthTbl tbody tr:nth-child(1) td:nth-child(1)').contains('Submit Stats');
cy.get('#bandwidthTbl tbody tr:nth-child(1) td:nth-child(2)').contains(200); // Bandwidth
cy.get('#bandwidthTbl tbody tr:nth-child(1) td:nth-child(3)').contains('bytes'); // Unit
});
});

View File

@@ -0,0 +1,130 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Distribution by Layout, Media or Event', function() {
const display1 = 'POP Display 1';
const layout1 = 'POP Layout 1';
beforeEach(function() {
cy.login();
});
it('Range: Today, Checks duration and count of a layout stat', () => {
// Create and alias for load layout
cy.intercept({
url: '/display?start=*',
query: {display: display1},
}).as('loadDisplayAfterSearch');
cy.intercept({
url: '/layout?start=*',
query: {layout: layout1},
}).as('loadLayoutAfterSearch');
cy.intercept('/report/data/distributionReport?*').as('reportData');
cy.visit('/report/form/distributionReport');
// Click on the select2 selection
cy.get('#displayId + span .select2-selection').click();
cy.get('.select2-container--open input[type="search"]').type(display1);
cy.wait('@loadDisplayAfterSearch');
cy.selectOption(display1);
// Click on the select2 selection
cy.get('#layoutId + span .select2-selection').click();
cy.get('.select2-container--open input[type="search"]').type(layout1);
cy.wait('@loadLayoutAfterSearch');
cy.selectOption(layout1);
// Click on the Apply button
cy.contains('Apply').should('be.visible').click();
cy.get('.chart-container').should('be.visible');
// Click on Tabular
cy.contains('Tabular').should('be.visible').click();
cy.contains('Next').should('be.visible').click();
cy.wait('@reportData');
// Should have media stats
cy.get('#distributionTbl tbody tr:nth-child(3) td:nth-child(1)').contains('12:00 PM'); // Period
cy.get('#distributionTbl tbody tr:nth-child(3) td:nth-child(2)').contains(60); // Duration
cy.get('#distributionTbl tbody tr:nth-child(3) td:nth-child(3)').contains(1); // Count
});
it.skip('Create/Delete a Daily Distribution Report Schedule', () => {
const reportschedule = 'Daily Distribution by Layout 1 and Display 1';
// Create and alias for load layout
cy.intercept({
url: '/display?start=*',
query: {display: display1},
}).as('loadDisplayAfterSearch');
cy.intercept({
url: '/layout?start=*',
query: {layout: layout1},
}).as('loadLayoutAfterSearch');
cy.intercept({
url: '/report/reportschedule?*',
query: {name: reportschedule},
}).as('loadReportScheduleAfterSearch');
cy.visit('/report/form/distributionReport');
// Click on the select2 selection
cy.get('#layoutId + span .select2-selection').click();
cy.get('.select2-container--open input[type="search"]').type(layout1);
cy.wait('@loadLayoutAfterSearch');
cy.selectOption(layout1);
// ------
// ------
// Create a Daily Distribution Report Schedule
cy.get('#reportAddBtn').click();
cy.get('#reportScheduleAddForm #name ').type(reportschedule);
// Click on the select2 selection
cy.get('#reportScheduleAddForm #displayId + span .select2-selection').click();
cy.get('.select2-container--open input[type="search"]').type(display1);
cy.wait('@loadDisplayAfterSearch');
cy.selectOption(display1);
cy.get('#dialog_btn_2').should('be.visible').click();
cy.visit('/report/reportschedule/view');
cy.get('#name').type(reportschedule);
cy.wait('@loadReportScheduleAfterSearch');
// Click on the first row element to open the designer
cy.get('#reportschedules_wrapper tr:first-child .dropdown-toggle').click({force: true});
cy.get('#reportschedules_wrapper tr:first-child .reportschedule_button_delete').click({force: true});
// Delete test campaign
cy.get('.bootbox .save-button').click();
// Check if layout is deleted in toast message
cy.get('.toast').contains('Deleted ' + reportschedule);
});
});

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Library Usage', function() {
beforeEach(function() {
cy.login();
});
it('should load tabular data and charts', () => {
cy.visit('/report/form/libraryusage');
cy.get('#libraryUsage_wrapper').should('be.visible');
cy.get('#libraryChart').should('be.visible');
cy.get('#userChart').should('be.visible');
});
});

View File

@@ -0,0 +1,183 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Proof of Play', function() {
const display1 = 'POP Display 1';
beforeEach(function() {
cy.login();
});
it('Range: Test export', function() {
// Create and alias for load displays
cy.intercept({
url: '/display?start=*',
query: {display: display1},
}).as('loadDisplayAfterSearch');
cy.visit('/report/view');
cy.contains('Export').click();
cy.get(':nth-child(1) > .col-sm-10 > .input-group > .flatpickr-wrapper > .datePickerHelper').click();
cy.get('.open > .flatpickr-innerContainer > .flatpickr-rContainer > .flatpickr-days > .dayContainer > .today').click();
cy.get(':nth-child(2) > .col-sm-10 > .input-group > .flatpickr-wrapper > .datePickerHelper').click();
cy.get('.open > .flatpickr-innerContainer > .flatpickr-rContainer > .flatpickr-days > .dayContainer > .today').next().click();
// Click on the select2 selection
cy.get('#displayId + span .select2-selection').click();
cy.get('.select2-container--open input[type="search"]').type(display1);
cy.wait('@loadDisplayAfterSearch');
cy.selectOption(display1);
cy.get('.total-stat').contains('Total number of records to be exported 5');
});
it('Range: Today - Test layout/media stats for a layout and a display', function() {
cy.intercept('/report/data/proofofplayReport?*').as('reportData');
cy.visit('/report/form/proofofplayReport');
cy.get('#type').select('media');
// Click on the Apply button
cy.contains('Apply').click();
// Wait for
cy.wait('@reportData');
cy.get('#stats tbody').contains('media');
// Should have media stats - Test media stats for a layout and a display
cy.get('#stats tbody tr:nth-child(1) td:nth-child(1)').contains('media'); // stat type
cy.get('#stats tbody tr:nth-child(1) td:nth-child(3)').contains('POP Display 1'); // display
cy.get('#stats tbody tr:nth-child(1) td:nth-child(6)').contains('POP Layout 1'); // layout
cy.get('#stats tbody tr:nth-child(1) td:nth-child(8)').contains('child_folder_media'); // media
cy.get('#stats tbody tr:nth-child(1) td:nth-child(10)').contains(2); // number of plays
cy.get('#stats tbody tr:nth-child(1) td:nth-child(12)').contains(120); // total duration
cy.get('#type').select('layout');
// Click on the Apply button
cy.contains('Apply').click();
// Wait for
cy.wait('@reportData');
cy.contains('Tabular').should('be.visible').click();
cy.get('#stats tbody').contains('layout');
// Should have layout stat - Test a layout stat for an ad campaign, a layout and a display
cy.get('#stats tbody tr:nth-child(1) td:nth-child(1)').contains('layout'); // stat type
cy.get('#stats tbody tr:nth-child(1) td:nth-child(3)').contains('POP Display 1'); // display
cy.get('#stats tbody tr:nth-child(1) td:nth-child(4)').contains('POP Ad Campaign 1'); // ad campaign
cy.get('#stats tbody tr:nth-child(1) td:nth-child(6)').contains('POP Layout 1'); // layout
cy.get('#stats tbody tr:nth-child(1) td:nth-child(10)').contains(1); // number of plays
cy.get('#stats tbody tr:nth-child(1) td:nth-child(12)').contains(60); // total duration
});
it('Range: Lastweek - Test media stats for a layout and a display', function() {
cy.intercept('/report/data/proofofplayReport?*').as('reportData');
cy.visit('/report/form/proofofplayReport');
// Range: Lastweek
cy.get('#reportFilter').select('lastweek');
cy.get('#type').select('media');
// Click on the Apply button
cy.contains('Apply').click();
// Wait for
cy.wait('@reportData');
cy.get('#stats').within(() => {
// Check if the "No data available in table" message is not present
cy.contains('No data available in table').should('not.exist');
cy.get('tbody tr').should('have.length', 1);
// Should have media stats
cy.get('tbody td').eq(0).should('contain', 'media'); // stat type
cy.get('tbody td').eq(2).contains('POP Display 1'); // display
cy.get('tbody td').eq(5).contains('POP Layout 1'); // layout
cy.get('tbody td').eq(7).contains('child_folder_media'); // media
cy.get('tbody td').eq(9).contains(2); // number of plays
cy.get('tbody td').eq(11).contains(120); // total duration
});
cy.get('#type').select('layout');
// Click on the Apply button
cy.contains('Apply').click();
// Wait for
cy.wait('@reportData');
cy.get('#stats').within(() => {
// Check if the "No data available in table" message is not present
cy.contains('No data available in table').should('not.exist');
cy.get('tbody tr').should('have.length', 1);
// Should have layout stat - Test a layout stat for an ad campaign, a layout and a display
cy.get('tbody td').eq(0).contains('layout'); // stat type
cy.get('tbody td').eq(2).contains('POP Display 1'); // display
cy.get('tbody td').eq(3).contains('POP Ad Campaign 1'); // ad campaign
cy.get('tbody td').eq(5).contains('POP Layout 1'); // layout
cy.get('tbody td').eq(9).contains(1); // number of plays
cy.get('tbody td').eq(11).contains(60); // total duration
});
});
it('Range: Today - Test event/widget stats for a layout and a display', function() {
cy.intercept('/report/data/proofofplayReport?*').as('reportData');
cy.visit('/report/form/proofofplayReport');
cy.get('#type').select('event');
// Click on the Apply button
cy.contains('Apply').click();
// Wait for
cy.wait('@reportData');
// Should have media stats - Test media stats for a layout and a display
cy.get('#stats tbody tr:nth-child(1) td:nth-child(1)').contains('event'); // stat type
cy.get('#stats tbody tr:nth-child(1) td:nth-child(3)').contains('POP Display 1'); // display
cy.get('#stats tbody tr:nth-child(1) td:nth-child(9)').contains('Event123'); // tag/eventname
cy.get('#stats tbody tr:nth-child(1) td:nth-child(10)').contains(1); // number of plays
cy.get('#stats tbody tr:nth-child(1) td:nth-child(12)').contains(60); // total duration
cy.get('#type').select('widget');
// Click on the Apply button
cy.contains('Apply').click();
// Wait for
cy.wait('@reportData');
cy.contains('Tabular').should('be.visible').click();
// Should have layout stat - Test a layout stat for an ad campaign, a layout and a display
cy.get('#stats tbody tr:nth-child(1) td:nth-child(1)').contains('widget'); // stat type
cy.get('#stats tbody tr:nth-child(1) td:nth-child(3)').contains('POP Display 1'); // display
cy.get('#stats tbody tr:nth-child(1) td:nth-child(6)').contains('POP Layout 1'); // layout
cy.get('#stats tbody tr:nth-child(1) td:nth-child(10)').contains(1); // number of plays
cy.get('#stats tbody tr:nth-child(1) td:nth-child(12)').contains(60); // total duration
});
});

View File

@@ -0,0 +1,134 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Summary by Layout, Media or Event', function() {
const display1 = 'POP Display 1';
const layout1 = 'POP Layout 1';
beforeEach(function() {
cy.login();
});
it('Range: Today, Checks duration and count of a layout stat', () => {
// Create alias
cy.intercept({
url: '/display?start=*',
query: {display: display1},
}).as('loadDisplayAfterSearch');
cy.intercept({
url: '/layout?start=*',
query: {layout: layout1},
}).as('loadLayoutAfterSearch');
cy.intercept('/report/data/summaryReport?*').as('reportData');
cy.visit('/report/form/summaryReport');
// Click on the select2 selection
cy.get('#displayId + span .select2-selection').click();
cy.get('.select2-container--open input[type="search"]').type(display1);
cy.wait('@loadDisplayAfterSearch');
cy.selectOption(display1);
// Click on the select2 selection
cy.get('#layoutId + span .select2-selection').click();
cy.get('.select2-container--open input[type="search"]').type(layout1);
cy.wait('@loadLayoutAfterSearch');
cy.selectOption(layout1);
// Click on the Apply button
cy.contains('Apply').should('be.visible').click();
// Wait for report data
cy.wait('@reportData');
cy.get('.chart-container').should('be.visible');
// Click on Tabular
cy.contains('Tabular').should('be.visible').click();
cy.contains('Next').should('be.visible').click();
// Should have media stats
cy.get('#summaryTbl tbody tr:nth-child(3) td:nth-child(1)').contains('12:00 PM'); // Period
cy.get('#summaryTbl tbody tr:nth-child(3) td:nth-child(2)').contains(60); // Duration
cy.get('#summaryTbl tbody tr:nth-child(3) td:nth-child(3)').contains(1); // Count
});
it('Create/Delete a Daily Summary Report Schedule', () => {
const reportschedule = 'Daily Summary by Layout 1 and Display 1';
// Create and alias for load layout
cy.intercept('/report/reportschedule/form/add*').as('reportScheduleAddForm');
cy.intercept({
url: '/display?start=*',
query: {display: display1},
}).as('loadDisplayAfterSearch');
cy.intercept({
url: '/layout?start=*',
query: {layout: layout1},
}).as('loadLayoutAfterSearch');
cy.intercept({
url: '/report/reportschedule?*',
query: {name: reportschedule},
}).as('loadReportScheduleAfterSearch');
cy.visit('/report/form/summaryReport');
// Click on the select2 selection
cy.get('#layoutId + span .select2-selection').click();
cy.get('.select2-container--open input[type="search"]').type(layout1);
cy.wait('@loadLayoutAfterSearch');
cy.selectOption(layout1);
// ------
// ------
// Create a Daily Summary Report Schedule
cy.get('#reportAddBtn').click();
cy.wait('@reportScheduleAddForm');
cy.get('#reportScheduleAddForm #name ').type(reportschedule);
// Click on the select2 selection
cy.get('#reportScheduleAddForm #displayId + span .select2-selection').click();
cy.get('.select2-container--open input[type="search"]').type(display1);
cy.wait('@loadDisplayAfterSearch');
cy.selectOption(display1);
cy.get('#dialog_btn_2').should('be.visible').click();
cy.visit('/report/reportschedule/view');
cy.get('#name').type(reportschedule);
cy.wait('@loadReportScheduleAfterSearch');
// Click on the first row element to open the designer
cy.get('#reportschedules_wrapper tr:first-child .dropdown-toggle').click({force: true});
cy.get('#reportschedules_wrapper tr:first-child .reportschedule_button_delete').click({force: true});
// Delete test campaign
cy.get('.bootbox .save-button').click();
// Check if layout is deleted in toast message
cy.get('.toast').contains('Deleted ' + reportschedule);
});
});

View File

@@ -0,0 +1,45 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Time Connected', function() {
beforeEach(function() {
cy.login();
});
it('should load time connected data of displays', () => {
cy.visit('/report/form/timeconnected');
// Click on the select2 selection
cy.get('.select2-search__field').click();
// Type the display name
cy.get('.select2-container--open textarea[type="search"]').type('POP Display Group');
cy.get('.select2-container--open .select2-results > ul').contains('POP Display Group').click();
// Click on the Apply button
cy.contains('Apply').should('be.visible').click();
// Should have media stats
cy.get('#records_table tr:nth-child(1) th:nth-child(1)').contains('POP Display 1');
cy.get('#records_table tr:nth-child(2) td:nth-child(2)').contains('100%');
});
});

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Time Connected', function() {
const display1 = 'POP Display 1';
beforeEach(function() {
cy.login();
});
it('should load time connected data of displays', () => {
// Create and alias for load display
cy.intercept({
url: '/display?start=*',
query: {display: display1},
}).as('loadDisplayAfterSearch');
cy.visit('/report/form/timedisconnectedsummary');
// Click on the select2 selection
cy.get('#displayId + span .select2-selection').click();
cy.get('.select2-container--open input[type="search"]').type(display1);
cy.wait('@loadDisplayAfterSearch');
cy.selectOption(display1);
// Select "Yesterday" from the dropdown
cy.get('#reportFilter').select('yesterday');
// Click on the Apply button
cy.contains('Apply').should('be.visible').click();
cy.get('.chart-container').should('be.visible');
// Click on Tabular
cy.contains('Tabular').should('be.visible').click();
// Should have media stats
cy.get('#timeDisconnectedTbl tr:nth-child(1) td:nth-child(2)').contains('POP Display 1');
cy.get('#timeDisconnectedTbl tr:nth-child(1) td:nth-child(3)').contains('10');
});
});

View File

@@ -0,0 +1,220 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Dayparts', function() {
let testRun = '';
beforeEach(function() {
cy.login();
testRun = Cypress._.random(0, 1e9);
});
it('should add a daypart', function() {
cy.visit('/daypart/view');
// Click on the Add Daypart button
cy.contains('Add Daypart').click();
cy.get('.modal input#name')
.type('Cypress Test Daypart ' + testRun + '_1');
cy.get(':nth-child(3) > .col-sm-10 > .input-group > .flatpickr-wrapper > .datePickerHelper').click();
// cy.get('.open > .flatpickr-time > :nth-child(1) > .arrowUp').click();
cy.get('.open > .flatpickr-time > :nth-child(1) > .numInput').type('8');
cy.get(':nth-child(4) > .col-sm-10 > .input-group > .flatpickr-wrapper > .datePickerHelper').click();
cy.get('.open > .flatpickr-time > :nth-child(1) > .numInput').type('17');
// Add first by clicking next
cy.get('.modal .save-button').click();
// Check if daypart is added in toast message
cy.contains('Added Cypress Test Daypart ' + testRun + '_1');
});
it('searches and edit existing daypart', function() {
// Create a new daypart and then search for it and edit it
cy.createDayPart('Cypress Test Daypart ' + testRun).then((id) => {
cy.intercept({
url: '/daypart?*',
query: {name: 'Cypress Test Daypart ' + testRun},
}).as('loadGridAfterSearch');
// Intercept the PUT request
cy.intercept({
method: 'PUT',
url: '/daypart/*',
}).as('putRequest');
cy.visit('/daypart/view');
// Filter for the created daypart
cy.get('#Filter input[name="name"]')
.type('Cypress Test Daypart ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#dayparts tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#dayparts tr:first-child .dropdown-toggle').click({force: true});
cy.get('#dayparts tr:first-child .daypart_button_edit').click({force: true});
cy.get('.modal input#name').clear()
.type('Cypress Test Daypart Edited ' + testRun);
// edit test daypart
cy.get('.bootbox .save-button').click();
// Wait for the intercepted PUT request and check the form data
cy.wait('@putRequest').then((interception) => {
// Get the request body (form data)
const response = interception.response;
const responseData = response.body.data;
// assertion on the "daypart" value
expect(responseData.name).to.eq('Cypress Test Daypart Edited ' + testRun);
});
// Delete the daypart and assert success
cy.deleteDayPart(id).then((res) => {
expect(res.status).to.equal(204);
});
});
});
it('searches and delete existing daypart', function() {
// Create a new daypart and then search for it and delete it
cy.createDayPart('Cypress Test Daypart ' + testRun).then((res) => {
cy.intercept({
url: '/daypart?*',
query: {name: 'Cypress Test Daypart ' + testRun},
}).as('loadGridAfterSearch');
cy.visit('/daypart/view');
// Filter for the created daypart
cy.get('#Filter input[name="name"]')
.type('Cypress Test Daypart ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#dayparts tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#dayparts tr:first-child .dropdown-toggle').click({force: true});
cy.get('#dayparts tr:first-child .daypart_button_delete').click({force: true});
// Delete test daypart
cy.get('.bootbox .save-button').click();
// Check if daypart is deleted in toast message
cy.get('.toast').contains('Deleted Cypress Test Daypart');
});
});
it('searches and share existing daypart', function() {
// Create a new daypart and then search for it and share it
cy.createDayPart('Cypress Test Daypart ' + testRun).then((res) => {
cy.intercept({
url: '/daypart?*',
query: {name: 'Cypress Test Daypart ' + testRun},
}).as('loadGridAfterSearch');
cy.intercept({
query: {name: 'Everyone'},
url: /\/user\/permissions\/DayPart\/\d+\?draw=2/,
}).as('draw2');
cy.intercept({
query: {name: 'Everyone'},
url: /\/user\/permissions\/DayPart\/\d+\?draw=3/,
}).as('draw3');
cy.visit('/daypart/view');
// Filter for the created daypart
cy.get('#Filter input[name="name"]')
.type('Cypress Test Daypart ' + testRun);
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
cy.get('#dayparts tbody tr').should('have.length', 1);
// Click on the first row element to open the delete modal
cy.get('#dayparts tr:first-child .dropdown-toggle').click({force: true});
cy.get('#dayparts tr:first-child .daypart_button_permissions').click({force: true});
cy.get('.modal #name').type('Everyone');
cy.wait('@draw2');
cy.get('#permissionsTable tbody tr').should('have.length', 1);
cy.get('#permissionsTable').within(() => {
cy.get('input[type="checkbox"][data-permission="view"]').should('be.visible').check();
// DOM is refreshed at this point, so wait for it
cy.wait('@draw3');
// We have no other option but to put {force: true} here
cy.get('input[type="checkbox"][data-permission="edit"]').check();
});
// Save
cy.get('.bootbox .save-button').click();
// Check if daypart is deleted in toast message
cy.get('.toast').contains('Share option Updated');
});
});
it('selects multiple dayparts and delete them', function() {
// Create a new daypart and then search for it and delete it
cy.createDayPart('Cypress Test Daypart ' + testRun).then((res) => {
cy.intercept({
url: '/daypart?*',
query: {name: 'Cypress Test Daypart'},
}).as('loadGridAfterSearch');
// Delete all test dayparts
cy.visit('/daypart/view');
// Clear filter
cy.get('#Filter input[name="name"]')
.clear()
.type('Cypress Test Daypart');
// Wait for the grid reload
cy.wait('@loadGridAfterSearch');
// Select all
cy.get('button[data-toggle="selectAll"]').click();
// Delete all
cy.get('.dataTables_info button[data-toggle="dropdown"]').click();
cy.get('.dataTables_info a[data-button-id="daypart_button_delete"]').click();
cy.get('button.save-button').click();
// Modal should contain one successful delete at least
cy.get('.modal-body').contains(': Success');
});
});
});

View File

@@ -0,0 +1,392 @@
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
describe('Schedule Events', function() {
// Seeded Data
const campaignSchedule1 = 'Campaign for Schedule 1';
const layoutSchedule1 = 'Layout for Schedule 1';
const display1 = 'List Campaign Display 1';
const display2 = 'List Campaign Display 2';
const command1 = 'Set Timezone';
beforeEach(function() {
cy.login();
});
it('should list all scheduled events', function() {
// Make a GET request to the API endpoint '/schedule/data/events'??
cy.request({
method: 'GET',
url: '/schedule/data/events',
}).then((response) => {
// Assertions on the response
expect(response.status).to.equal(200);
expect(response.body).to.have.property('result');
});
});
// TC-01
it('should schedule an event layout that has no priority, no recurrence', function() {
cy.intercept('GET', '/schedule/form/add?*').as('scheduleAddForm');
// Set up intercepts with aliases
cy.intercept({
url: '/display?start=*',
query: {display: display1},
}).as('loadDisplayAfterSearch');
cy.intercept({
url: '/displaygroup?*',
query: {displayGroup: display1},
}).as('loadDisplaygroupAfterSearch');
cy.intercept({
url: '/campaign?type=list*',
query: {name: layoutSchedule1},
}).as('loadListCampaignsAfterSearch');
// Click on the Add Event button
cy.visit('/schedule/view');
cy.contains('Clear Filters').should('be.visible').click();
cy.contains('Add Event').click();
cy.get('.bootbox.modal')
.should('be.visible') // essential: Ensure the modal is visible
.then(() => {
cy.get('.modal-content #eventTypeId').select('Layout');
// Select layout
cy.selectFromDropdown('.layout-control .select2-selection', layoutSchedule1, layoutSchedule1, '@loadListCampaignsAfterSearch');
// Click Next and check toast message
cy.get('.modal .modal-footer').contains('Next').click();
// Select display
cy.selectFromDropdown('.display-group-control .select2-selection', display1, display1, '@loadDisplaygroupAfterSearch', 1);
// Click Next and check toast message
cy.get('.modal .modal-footer').contains('Next').click();
// Select day part and set name
cy.get('.modal-content [name="dayPartId"]').select('Always');
// Click Next and check toast message
cy.get('.modal .modal-footer').contains('Next').click();
cy.get('.modal-content [name="name"]').type('Always - Layout Event');
cy.get('.modal .modal-footer').contains('Finish').click();
cy.contains('Added Event');
});
// Validate - schedule creation should be successful
cy.visit('/schedule/view');
cy.contains('Clear Filters').should('be.visible').click();
cy.get('#DisplayList + span .select2-selection').click();
// Type the display name
cy.get('.select2-container--open textarea[type="search"]').type(display1);
// Wait for Display to load
cy.wait('@loadDisplayAfterSearch');
cy.get('.select2-container--open').contains(display1);
cy.get('.select2-container--open .select2-results > ul > li').should('have.length', 1);
// Select the display from the dropdown
cy.get('.select2-container--open .select2-results > ul > li:first').contains(display1).click();
// Verify that the schedule is successfully created and listed in the grid
cy.get('#schedule-grid').contains(layoutSchedule1);
// Should have 1
cy.get('#schedule-grid tbody tr').should('have.length', 1);
});
// relies on TC-01
it('should edit a scheduled event', function() {
cy.intercept('GET', '/schedule/form/add?*').as('scheduleAddForm');
// Set up intercepts with aliases
cy.intercept({
url: '/display?start=*',
query: {display: display1},
}).as('loadDisplayAfterSearch');
cy.intercept({
url: '/displaygroup?*',
query: {displayGroup: display2},
}).as('loadDisplaygroupAfterSearch');
cy.intercept({
url: '/campaign?type=list*',
query: {name: layoutSchedule1},
}).as('loadListCampaignsAfterSearch');
// Click on the Add Event button
cy.visit('/schedule/view');
cy.contains('Clear Filters').should('be.visible').click();
cy.get('#DisplayList + span .select2-selection').click();
// Type the display name
cy.get('.select2-container--open textarea[type="search"]').type(display1);
// Wait for Display to load
cy.wait('@loadDisplayAfterSearch');
cy.get('.select2-container--open').contains(display1);
cy.get('.select2-container--open .select2-results > ul > li').should('have.length', 1);
// Select the display from the dropdown
cy.get('.select2-container--open .select2-results > ul > li:first').contains(display1).click();
// Verify that the schedule is successfully created and listed in the grid
cy.get('#schedule-grid').contains(layoutSchedule1);
// Should have 1
cy.get('#schedule-grid tbody tr').should('have.length', 1);
cy.get('#schedule-grid tr:first-child .dropdown-toggle').click({force: true});
cy.get('#schedule-grid tr:first-child .schedule_button_edit').click({force: true});
cy.contains('.stepwizard-step', 'Displays')
.find('a')
.click();
// Select display
cy.get('.display-group-control > .col-sm-10 > .select2 > .selection > .select2-selection').type(display2);
// Wait for the display group to load after search
cy.wait('@loadDisplaygroupAfterSearch');
cy.get('.select2-container--open .select2-dropdown .select2-results > ul')
.should('contain', display2);
cy.get('.select2-container--open .select2-dropdown .select2-results > ul > li')
.should('have.length', 2)
.last()
.click();
cy.contains('.stepwizard-step', 'Optional')
.find('a')
.click();
cy.get('.modal-content [name="name"]').clear().type('Always - Layout Event Edited');
// Click Next and check toast message
cy.get('.modal .modal-footer').contains('Save').click();
cy.contains('Edited Event');
});
it('should schedule an event campaign that has no priority, no recurrence', function() {
cy.intercept('GET', '/schedule/form/add?*').as('scheduleAddForm');
// Set up intercepts with aliases
cy.intercept({
url: '/display?start=*',
query: {display: display1},
}).as('loadDisplayAfterSearch');
cy.intercept({
url: '/displaygroup?*',
query: {displayGroup: display1},
}).as('loadDisplaygroupAfterSearch');
cy.intercept({
url: '/campaign?type=list*',
query: {name: campaignSchedule1},
}).as('loadListCampaignsAfterSearch');
// Visit the page and click on the Add Event button
cy.visit('/schedule/view');
cy.contains('Clear Filters').should('be.visible').click();
cy.contains('Add Event').click();
cy.get('.bootbox.modal')
.should('be.visible') // essential: Ensure the modal is visible
.then(() => {
cy.get('.modal-content #eventTypeId').select('Campaign');
// Select campaign
cy.selectFromDropdown('.layout-control .select2-selection', campaignSchedule1, campaignSchedule1, '@loadListCampaignsAfterSearch');
// Click Next and check toast message
cy.get('.modal .modal-footer').contains('Next').click();
// Select display
cy.selectFromDropdown('.display-group-control .select2-selection', display1, display1, '@loadDisplaygroupAfterSearch', 1);
// Click Next and check toast message
cy.get('.modal .modal-footer').contains('Next').click();
// Select day part and campaign
cy.get('.modal-content [name="dayPartId"]').select('Always');
// Click Next and check toast message
cy.get('.modal .modal-footer').contains('Next').click();
cy.get('.modal-content [name="name"]').type('Always - Campaign Event');
cy.get('.modal .modal-footer').contains('Finish').click();
cy.contains('Added Event');
});
// Validate - schedule creation should be successful
cy.visit('/schedule/view');
cy.contains('Clear Filters').should('be.visible').click();
cy.get('#DisplayList + span .select2-selection').click();
// Type the display name
cy.get('.select2-container--open textarea[type="search"]').type(display1);
// Wait for Display to load
cy.wait('@loadDisplayAfterSearch');
cy.get('.select2-container--open').contains(display1);
cy.get('.select2-container--open .select2-results > ul > li').should('have.length', 1);
// Select the display from the dropdown
cy.get('.select2-container--open .select2-results > ul > li:first').contains(display1).click();
// Verify that the schedule is successfully created and listed in the grid
cy.get('#schedule-grid').contains(campaignSchedule1);
});
it('should schedule an event command layout that has no priority, no recurrence', function() {
cy.intercept('GET', '/schedule/form/add?*').as('scheduleAddForm');
cy.intercept({
url: '/displaygroup?*',
query: {displayGroup: display1},
}).as('loadDisplaygroupAfterSearch');
cy.intercept({
url: '/command?*',
query: {command: command1},
}).as('loadCommandAfterSearch');
// Click on the Add Event button
cy.visit('/schedule/view');
cy.contains('Clear Filters').should('be.visible').click();
cy.contains('Add Event').click();
cy.get('.bootbox.modal')
.should('be.visible') // essential: Ensure the modal is visible
.then(() => {
cy.get('.modal-content #eventTypeId').select('Command');
// Select command
cy.selectFromDropdown('.command-control .select2-selection', command1, command1, '@loadCommandAfterSearch');
// Click Next and check toast message
cy.get('.modal .modal-footer').contains('Next').click();
// Select display
cy.selectFromDropdown('.display-group-control .select2-selection', display1, display1, '@loadDisplaygroupAfterSearch', 1);
// Click Next and check toast message
cy.get('.modal .modal-footer').contains('Next').click();
cy.get('.starttime-control > .col-sm-10 > .input-group > .flatpickr-wrapper > .datePickerHelper').click();
cy.get('.open > .flatpickr-innerContainer > .flatpickr-rContainer > .flatpickr-days > .dayContainer > .today').click();
cy.get('.open > .flatpickr-time > :nth-child(3) > .arrowUp').click();
cy.get('.modal .modal-footer').contains('Next').click();
cy.get('.modal-content [name="name"]').type('Custom - Command Event');
cy.get('.modal .modal-footer').contains('Finish').click();
});
});
it('should schedule an event overlay layout that has no priority, no recurrence', function() {
cy.intercept('GET', '/schedule/form/add?*').as('scheduleAddForm');
cy.intercept({
url: '/displaygroup?*',
query: {displayGroup: display1},
}).as('loadDisplaygroupAfterSearch');
cy.intercept({
url: '/campaign?type=list*',
query: {name: layoutSchedule1},
}).as('loadListCampaignsAfterSearch');
// Click on the Add Event button
cy.visit('/schedule/view');
cy.contains('Clear Filters').should('be.visible').click();
cy.contains('Add Event').click();
cy.get('.bootbox.modal')
.should('be.visible') // essential: Ensure the modal is visible
.then(() => {
cy.get('.modal-content #eventTypeId').select('Overlay Layout');
// Select layout
cy.selectFromDropdown('.layout-control .select2-selection', layoutSchedule1, layoutSchedule1, '@loadListCampaignsAfterSearch');
// Click Next and check toast message
cy.get('.modal .modal-footer').contains('Next').click();
// Select display
cy.selectFromDropdown('.display-group-control .select2-selection', display1, display1, '@loadDisplaygroupAfterSearch', 1);
// Click Next and check toast message
cy.get('.modal .modal-footer').contains('Next').click();
// Select daypart - custom
cy.get('#dayPartId').select('Custom');
cy.get('.starttime-control > .col-sm-10 > .input-group > .flatpickr-wrapper > .datePickerHelper')
.click() // Open the picker
.then(() => {
// Select today's date
cy.get('.flatpickr-calendar.open .flatpickr-days .dayContainer .today')
.click();
// Increment the hour (adjust time)
cy.get('.flatpickr-calendar.open .flatpickr-time :nth-child(3) .arrowUp')
.click();
// Close the picker by clicking outside
cy.get('body').click(0, 0);
});
cy.get('.endtime-control > .col-sm-10 > .input-group > .flatpickr-wrapper > .datePickerHelper')
.click() // Open the picker
.then(() => {
// Select today's date
cy.get('.flatpickr-calendar.open .flatpickr-days .dayContainer .today')
.click();
// Increment the hour (adjust time)
cy.get('.flatpickr-calendar.open .flatpickr-time :nth-child(3) .arrowUp')
.click()
.click();
// Close the picker by clicking outside
cy.get('body').click(0, 0);
});
cy.get('.modal .modal-footer').contains('Next').click();
cy.get('.modal-content [name="name"]').type('Custom - Overlay Event');
cy.get('.modal .modal-footer').contains('Finish').click();
});
});
});

View File

@@ -0,0 +1,136 @@
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
describe('Template Test Suite', function () {
let templateName = '';
// create template flow
function createTemplate(name) {
cy.visit('/template/view');
cy.contains('Add Template').click();
cy.get('#name').clear().type(name);
cy.get('#dialog_btn_2').should('be.visible').click();
cy.get('#layout-editor').should('be.visible');
cy.get('#backBtn').click({ force: true });
}
// delete template flow
function deleteATemplate(name) {
cy.get('div[title="Row Menu"] button.dropdown-toggle').click({ force: true });
cy.get('a.layout_button_delete[data-commit-method="delete"]').click({ force: true });
cy.get('#layoutDeleteForm').should('be.visible');
cy.contains('p', 'Are you sure you want to delete this item?').should('be.visible');
}
beforeEach(function () {
cy.login();
templateName = 'Template No. ' + Cypress._.random(0, 1e9);
});
// Display Template List
it('should display the template list', function () {
cy.intercept('GET', '**/template*').as('templateList');
cy.visit('/template/view');
cy.wait('@templateList').its('response.statusCode').should('eq', 200);
});
// Save Incomplete Form
it('should prevent saving incomplete template', function () {
cy.visit('/template/view');
cy.contains('Add Template').click();
cy.get('#dialog_btn_2').should('be.visible').click();
cy.contains('Layout Name must be between 1 and 100 characters').should('be.visible');
});
// Create a Template
it('should create a template', function () {
createTemplate(templateName);
cy.contains('td', templateName).should('be.visible');
});
// Duplicate Template
it('should not allow duplicate template name', function () {
createTemplate(templateName);
cy.contains('Add Template').click();
cy.get('#name').clear().type(templateName);
cy.get('#dialog_btn_2').should('be.visible').click();
cy.get('.modal-footer .form-error')
.contains(`You already own a Layout called '${templateName}'. Please choose another name.`)
.should('be.visible');
});
// Search and Delete a template
it('should search template and delete it', function () {
cy.intercept({
url: '/template?*',
query: { template: templateName },
}).as('displayTemplateAfterSearch');
createTemplate(templateName);
cy.get('#template').clear().type(templateName);
cy.wait('@displayTemplateAfterSearch');
cy.get('table tbody tr').should('have.length', 1);
cy.get('#templates tbody tr:nth-child(1) td:nth-child(1)').contains(templateName);
cy.get('#templates tbody tr')
.should('have.length', 1)
.first()
.should('contain.text', templateName);
// delete template = no
deleteATemplate(templateName);
cy.get('#dialog_btn_2').click({ force: true });
cy.contains(templateName).should('be.visible');
// delete template = yes
deleteATemplate(templateName);
cy.get('#dialog_btn_3').click({ force: true });
cy.get('#toast-container .toast-message').contains(`Deleted ${templateName}`).should('be.visible');
cy.contains(templateName).should('not.exist');
});
// Multiple deleting of templates
it('should delete multiple templates', function () {
createTemplate(templateName);
cy.get('button[data-toggle="selectAll"]').click();
cy.get('.dataTables_info button[data-toggle="dropdown"]').click();
cy.get('a[data-button-id="layout_button_delete"]').click();
cy.get('.modal-footer').contains('Save').click();
cy.get('.modal-body').contains(': Success');
cy.get('.modal-footer').contains('Close').click();
cy.contains('.dataTables_empty', 'No data available in table').should('be.visible');
});
// Search for non-existing template
it('should not return any entry for non-existing template', function () {
cy.visit('/template/view');
cy.get('#template').clear().type('This is a hardcoded template name just to make sure it doesnt exist in the record');
cy.contains('.dataTables_empty', 'No data available in table').should('be.visible');
});
});

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
describe('User Account Test Suite', function() {
beforeEach(function () {
cy.login();
cy.visit('/statusdashboard');
});
it('navigates to edit profile', function() {
cy.url().should('include', 'dashboard');
cy.get('img.nav-avatar').should('be.visible');
cy.get('#navbarUserMenu').click();
cy.get('div[aria-labelledby="navbarUserMenu"]')
.should('be.visible')
.contains('Edit Profile');
});
it('verifies all menu items are present and in order', function() {
cy.get('#navbarUserMenu').click();
cy.get('div[aria-labelledby="navbarUserMenu"] a')
.should('have.length', 6)
.then($items => {
const texts = [...$items].map(el => el.innerText.trim());
expect(texts).to.deep.equal([
'Preferences',
'Edit Profile',
'My Applications',
'Reshow welcome',
'About',
'Logout'
]);
});
});
it('validates edit profile', function() {
cy.get('#navbarUserMenu').click();
cy.get('div[aria-labelledby="navbarUserMenu"]')
.contains('Edit Profile')
.click();
cy.get('.modal-content').should('be.visible');
cy.contains('label', 'User Name').should('be.visible');
cy.contains('label', 'Password').should('be.visible');
cy.contains('label', 'New Password').should('be.visible');
cy.contains('label', 'Retype New Password').should('be.visible');
cy.contains('label', 'Email').should('be.visible');
cy.contains('label', 'Two Factor Authentication').should('be.visible');
// Ensure 2FA defaults to Off
cy.get('#twoFactorTypeId')
.should('be.visible')
.find('option:selected')
.should('have.text', 'Off');
});
});

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
describe('User Account Test Suite', function() {
beforeEach(function () {
cy.login();
cy.visit('/statusdashboard');
});
it('navigates to edit profile', function() {
cy.url().should('include', 'dashboard');
cy.get('img.nav-avatar').should('be.visible');
cy.get('#navbarUserMenu').click();
cy.get('div[aria-labelledby="navbarUserMenu"]')
.should('be.visible')
.contains('Edit Profile');
});
it('verifies all menu items are present and in order', function() {
cy.get('#navbarUserMenu').click();
cy.get('div[aria-labelledby="navbarUserMenu"] a')
.should('have.length', 6)
.then($items => {
const texts = [...$items].map(el => el.innerText.trim());
expect(texts).to.deep.equal([
'Preferences',
'Edit Profile',
'My Applications',
'Reshow welcome',
'About',
'Logout'
]);
});
});
it('validates edit profile', function() {
cy.get('#navbarUserMenu').click();
cy.get('div[aria-labelledby="navbarUserMenu"]')
.contains('Edit Profile')
.click();
cy.get('.modal-content').should('be.visible');
cy.contains('label', 'User Name').should('be.visible');
cy.contains('label', 'Password').should('be.visible');
cy.contains('label', 'New Password').should('be.visible');
cy.contains('label', 'Retype New Password').should('be.visible');
cy.contains('label', 'Email').should('be.visible');
cy.contains('label', 'Two Factor Authentication').should('be.visible');
// Ensure 2FA defaults to Off
cy.get('#twoFactorTypeId')
.should('be.visible')
.find('option:selected')
.should('have.text', 'Off');
});
});

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
describe('Dashboard', function() {
beforeEach(function() {
cy.login();
});
it('should be at the dashboard page', function() {
cy.visit('/statusdashboard');
cy.url().should('include', 'dashboard');
// Check for the dashboard elements
cy.contains('Bandwidth Usage');
cy.contains('Library Usage');
cy.contains('Display Activity');
cy.contains('Latest News');
});
});

37
cypress/e2e/login.cy.js Normal file
View File

@@ -0,0 +1,37 @@
describe('Login', function() {
it('should be able to login the default user', function () {
cy.visit('/login').then(() => {
cy.get('input#username')
.type('xibo_admin');
cy.get('input#password')
.type('password');
cy.get('button[type=submit]')
.click();
cy.url().should('include', 'dashboard');
cy.contains('xibo_admin');
});
});
it('should fail to login an invalid user', function () {
cy.visit('/login').then(() => {
cy.get('input#username')
.type('xibo_admin');
cy.get('input#password')
.type('wrongpassword');
cy.get('button[type=submit]')
.click();
cy.contains('Username or Password incorrect');
});
});
});

View File

@@ -0,0 +1,19 @@
describe('Unauthenticated CMS access', function () {
it('should visit the login page and check the version', function () {
cy.visit('/login').then(() => {
cy.url().should('include', '/login');
cy.contains('Version 4.');
});
});
it('should redirect to login when an authenticated page is requested', function() {
cy.visit('/logout').then(() => {
cy.visit('/layout/view').then(() => {
cy.url().should('include', '/login');
});
});
});
});