diff --git a/Dockerfile b/Dockerfile index 2832219d0e..e4c423699e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -209,7 +209,8 @@ RUN rm /var/www/cms/composer.* && \ rm /var/www/cms/cypress.config.js && \ rm -r /var/www/cms/cypress && \ rm -r /var/www/cms/ui && \ - rm /var/www/cms/webpack.config.js + rm /var/www/cms/webpack.config.js && \ + rm /var/www/cms/lib/route-cypress.php # Map a volumes to this folder. # Our CMS files, library, cache and backups will be in here. diff --git a/cypress/e2e/schedule.cy.js b/cypress/e2e/schedule.cy.js new file mode 100644 index 0000000000..962025ae68 --- /dev/null +++ b/cypress/e2e/schedule.cy.js @@ -0,0 +1,200 @@ +/* + * 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 . + */ + + +/* eslint-disable max-len */ +describe('Campaigns', function() { + 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'); + }); + }); + + it('should schedule an event campaign/layout/command/overlay layout that has no priority, no recurrence', function() { + + cy.intercept('/displaygroup?*').as('loadDisplaygroups'); + cy.intercept('/campaign?type=list*').as('loadListCampaigns'); + cy.intercept('/campaign?isLayoutSpecific=-1*').as('loadLayoutSpecificCampaign'); + cy.intercept('/display?start=*').as('loadDisplays'); + cy.intercept('/schedule?draw=4&*').as('scheduleGridLoad'); + cy.intercept('/layout?*').as('layoutLoad'); + cy.intercept('/user/pref').as('userPref'); + + cy.createCampaign('Campaign for Schedule 1'); + cy.createCommand('Set Timezone', 'Set timezone', 'TIMEZONE'); + + // Intercept the POST request to get the schedule Id + cy.intercept('/schedule').as('postCampaign'); + + cy.visit('/schedule/view'); + cy.contains('Add Event').click(); + + cy.get('.col-sm-10 > #eventTypeId').select('Campaign', {force: true}); + cy.get(':nth-child(3) > .col-sm-10 > .select2 > .selection > .select2-selection > .select2-selection__rendered') + .type('List Campaign Display 1'); + // Wait for Display to load + cy.wait('@loadDisplaygroups'); + cy.get('.select2-container--open').contains('List Campaign Display 1'); + cy.get('.select2-container--open .select2-dropdown .select2-results > ul > li').should('have.length', 2); + cy.get('#select2-displayGroupIds-results > li > ul > li:first').contains('List Campaign Display 1').click(); + // cy.get('[name="dayPartId"]').select('Daypart 11-14', {force: true}); + cy.get('[name="dayPartId"]').select('Always', {force: true}); + + // Select Campaign + cy.get('.layout-control > .col-sm-10 > .select2 > .selection > .select2-selection') + .type('Campaign for Schedule 1'); + // Wait for Campaign to load + cy.wait('@loadListCampaigns'); + cy.get('.select2-container--open').contains('Campaign for Schedule 1'); + // cy.get('.select2-container--open .select2-dropdown .select2-results > ul > li').should('have.length', 1); + cy.get('.select2-container--open .select2-results > ul > li').should('have.length', 1); + cy.get('.select2-container--open .select2-results > ul > li:first').contains('Campaign for Schedule 1').click(); + cy.get('.modal .modal-footer').contains('Next').click(); + + // Check toast message + cy.contains('Added Event'); + // --------- + + // Layout + cy.get('.col-sm-10 > #eventTypeId').select('Layout', {force: true}); + // Select Layout + cy.get('.layout-control > .col-sm-10 > .select2 > .selection > .select2-selection') + .type('Layout for Schedule 1'); + // Wait for Campaign to load + cy.wait('@loadListCampaigns'); + cy.get('.select2-container--open').contains('Layout for Schedule 1'); + cy.get('.select2-container--open .select2-dropdown .select2-results > ul > li').should('have.length', 1); + cy.get('.select2-container--open .select2-results > ul > li').should('have.length', 1); + cy.get('.select2-container--open .select2-results > ul > li:first').contains('Layout for Schedule 1').click(); + cy.get('.modal .modal-footer').contains('Next').click(); + + // --------- + + // Create Command Schedule + cy.get('.col-sm-10 > #eventTypeId').select('Command', {force: true}); + + cy.get('.starttime-control > .col-sm-10 > .input-group > .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('[name="commandId"]').select('Set Timezone', {force: true}); + cy.get('.modal .modal-footer').contains('Next').click(); + + // --------- + // Create Overlay Layout Schedule + cy.get('.col-sm-10 > #eventTypeId').select('Overlay Layout', {force: true}); + cy.get('[name="dayPartId"]').select('Always', {force: true}); + + // Select Layout + cy.get('.layout-control > .col-sm-10 > .select2 > .selection > .select2-selection').type('Layout for Schedule 1'); + // Wait for Display to load + cy.wait('@loadListCampaigns'); + cy.get('.select2-container--open').contains('Layout for Schedule 1'); + // cy.get('.select2-container--open .select2-dropdown .select2-results > ul > li').should('have.length', 1); + cy.get('.select2-container--open .select2-results > ul > li').should('have.length', 1); + cy.get('.select2-container--open .select2-results > ul > li:first').contains('Layout for Schedule 1').click(); + + cy.get(':nth-child(3) > .col-sm-10 > .select2 > .selection > .select2-selection > .select2-selection__rendered') + .type('List Campaign Display 1'); + // Wait for Display to load + cy.wait('@loadDisplaygroups'); + cy.get('.select2-container--open').contains('List Campaign Display 1'); + cy.get('.select2-container--open .select2-dropdown .select2-results > ul > li').should('have.length', 2); + cy.get('#select2-displayGroupIds-results > li > ul > li:first').contains('List Campaign Display 1').click(); + + cy.get('.modal .modal-footer').contains('Save').click(); + + // ------ + // Check if schedule creation was successful + cy.visit('/schedule/view'); + + cy.get('#DisplayList + span .select2-selection').click(); + cy.wait('@loadDisplays'); + // Type the display name + cy.get('.select2-container--open input[type="search"]').type('List Campaign Display 1'); + + // Wait for Display to load + cy.wait('@loadDisplays'); + cy.get('.select2-container--open').contains('List Campaign Display 1'); + cy.get('.select2-container--open .select2-results > ul > li').should('have.length', 1); + cy.get('.select2-container--open .select2-results > ul > li:first').contains('List Campaign Display 1').click(); + + cy.get('#schedule-grid').contains('Campaign for Schedule 1'); + cy.get('#schedule-grid').contains('Layout for Schedule 1'); + }); + + it('should edit a scheduled event', function() { + cy.intercept('/user/pref').as('userPref'); + cy.intercept('/schedule?draw=*').as('scheduleGridLoad'); + + cy.intercept('/displaygroup?*').as('loadDisplaygroups'); + cy.intercept('/campaign?isLayoutSpecific=-1*').as('loadLayoutSpecificCampaign'); + + cy.visit('/schedule/view'); + + // --------- + // Edit a schedule - add another display + cy.get('#campaignIdFilter + span .select2-selection').click(); + cy.wait('@loadLayoutSpecificCampaign'); + cy.get('.select2-container--open input[type="search"]').type('Layout for Schedule 1'); // Type the layout name + cy.wait('@loadLayoutSpecificCampaign'); + cy.get('.select2-container--open').contains('Layout for Schedule 1'); + cy.get('.select2-container--open .select2-results > ul > li').should('have.length', 1); + cy.get('.select2-container--open .select2-results > ul > li:first').contains('Layout for Schedule 1').click(); + + // Should have 1 + cy.get('#schedule-grid tbody tr').should('have.length', 2); + cy.get('#schedule-grid tr:first-child .dropdown-toggle').click(); + cy.get('#schedule-grid tr:first-child .schedule_button_edit').click(); + + cy.get(':nth-child(3) > .col-sm-10 > .select2 > .selection > .select2-selection > .select2-selection__rendered') + .type('List Campaign Display 2'); + // Wait for Display to load + cy.wait('@loadDisplaygroups'); + cy.get('.select2-container--open').contains('List Campaign Display 2'); + cy.get('.select2-container--open .select2-dropdown .select2-results > ul > li').should('have.length', 2); + cy.get('#select2-displayGroupIds-results > li > ul > li:first').contains('List Campaign Display 2').click(); + cy.get('.modal .modal-footer').contains('Save').click(); + cy.get('#schedule-grid tbody').contains('2'); + + // --------- + // Delete the schedule + // cy.get('#schedule-grid tbody tr').should('have.length', 2); + // cy.wait('@scheduleGridLoad'); + // cy.wait('@userPref'); + // cy.wait('@scheduleGridLoad'); + // cy.get('#schedule-grid tr:first-child .dropdown-toggle').click(); + // cy.get('#schedule-grid tr:first-child .schedule_button_delete').click(); + // cy.get('.bootbox .save-button').click(); + // + // // Validate the schedule no longer exist + // cy.get('#schedule-grid tbody tr').should('have.length', 1); + }); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index ce4b6a2465..0a3e6af25f 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -438,6 +438,42 @@ Cypress.Commands.add('scheduleCampaign', function(campaignId, displayName) { }); }); +// Create a campaign +Cypress.Commands.add('createCampaign', function(campaignName) { + cy.request({ + method: 'POST', + url: '/api/createCampaign', + form: true, + headers: { + Authorization: 'Bearer ' + Cypress.env('accessToken'), + }, + body: { + name: campaignName, + }, + }).then((res) => { + return res.body.campaignId; + }); +}); + +// Create a command +Cypress.Commands.add('createCommand', function(name, description, code) { + cy.request({ + method: 'POST', + url: '/api/createCommand', + form: true, + headers: { + Authorization: 'Bearer ' + Cypress.env('accessToken'), + }, + body: { + command: name, + description: description, + code: code, + }, + }).then((res) => { + return res.body.commandId; + }); +}); + // Set Display Status Cypress.Commands.add('displaySetStatus', function(displayName, statusId) { cy.request({ diff --git a/lib/Controller/CypressTest.php b/lib/Controller/CypressTest.php index 1252ced913..99f9c8c2fd 100644 --- a/lib/Controller/CypressTest.php +++ b/lib/Controller/CypressTest.php @@ -27,9 +27,11 @@ use Slim\Http\ServerRequest as Request; use Xibo\Entity\Display; use Xibo\Factory\CampaignFactory; +use Xibo\Factory\CommandFactory; use Xibo\Factory\DayPartFactory; use Xibo\Factory\DisplayFactory; use Xibo\Factory\DisplayGroupFactory; +use Xibo\Factory\FolderFactory; use Xibo\Factory\LayoutFactory; use Xibo\Factory\ScheduleFactory; use Xibo\Helper\Session; @@ -58,6 +60,13 @@ class CypressTest extends Base */ private $scheduleFactory; + /** @var FolderFactory */ + private $folderFactory; + /** + * @var CommandFactory + */ + private $commandFactory; + /** * @var DisplayGroupFactory */ @@ -97,7 +106,9 @@ public function __construct( $campaignFactory, $displayFactory, $layoutFactory, - $dayPartFactory + $dayPartFactory, + $folderFactory, + $commandFactory ) { $this->store = $store; $this->session = $session; @@ -107,8 +118,12 @@ public function __construct( $this->displayFactory = $displayFactory; $this->layoutFactory = $layoutFactory; $this->dayPartFactory = $dayPartFactory; + $this->folderFactory = $folderFactory; + $this->commandFactory = $commandFactory; } + // + /** * @throws InvalidArgumentException * @throws ControllerNotImplemented @@ -241,4 +256,90 @@ public function displayStatusEquals(Request $request, Response $response): Respo return $this->render($request, $response); } + + // + + public function createCommand(Request $request, Response $response): Response|ResponseInterface + { + $sanitizedParams = $this->getSanitizer($request->getParams()); + + $command = $this->commandFactory->create(); + $command->command = $sanitizedParams->getString('command'); + $command->description = $sanitizedParams->getString('description'); + $command->code = $sanitizedParams->getString('code'); + $command->userId = $this->getUser()->userId; + $command->commandString = $sanitizedParams->getString('commandString'); + $command->validationString = $sanitizedParams->getString('validationString'); + $availableOn = $sanitizedParams->getArray('availableOn'); + if (empty($availableOn)) { + $command->availableOn = null; + } else { + $command->availableOn = implode(',', $availableOn); + } + $command->save(); + + // Return + $this->getState()->hydrate([ + 'httpStatus' => 201, + 'message' => sprintf(__('Added %s'), $command->command), + 'id' => $command->commandId, + 'data' => $command + ]); + + return $this->render($request, $response); + } + + /** + * @throws InvalidArgumentException + * @throws ControllerNotImplemented + * @throws NotFoundException + * @throws GeneralException + */ + public function createCampaign(Request $request, Response $response): Response|ResponseInterface + { + $this->getLog()->debug('Creating campaign'); + $sanitizedParams = $this->getSanitizer($request->getParams()); + + $folder = $this->folderFactory->getById($this->getUser()->homeFolderId, 0); + + // Create Campaign + $campaign = $this->campaignFactory->create( + 'list', + $sanitizedParams->getString('name'), + $this->getUser()->userId, + $folder->getId() + ); + + // Cycle based playback + if ($campaign->type === 'list') { + $campaign->cyclePlaybackEnabled = $sanitizedParams->getCheckbox('cyclePlaybackEnabled'); + $campaign->playCount = ($campaign->cyclePlaybackEnabled) ? $sanitizedParams->getInt('playCount') : null; + + // For compatibility with existing API implementations we set a default here. + $campaign->listPlayOrder = ($campaign->cyclePlaybackEnabled) + ? 'block' + : $sanitizedParams->getString('listPlayOrder', ['default' => 'round']); + } else if ($campaign->type === 'ad') { + $campaign->targetType = $sanitizedParams->getString('targetType'); + $campaign->target = $sanitizedParams->getInt('target'); + $campaign->listPlayOrder = 'round'; + } + + // All done, save. + $campaign->save(); + + // Return + $this->getState()->hydrate([ + 'httpStatus' => 201, + 'message' => __('Added campaign'), + 'id' => $campaign->campaignId, + 'data' => $campaign + ]); + + return $this->render($request, $response); + } + + // + + // } diff --git a/lib/Dependencies/Controllers.php b/lib/Dependencies/Controllers.php index 18de95b4ba..672dfbb3ad 100644 --- a/lib/Dependencies/Controllers.php +++ b/lib/Dependencies/Controllers.php @@ -486,7 +486,9 @@ public static function registerControllersWithDi() $c->get('campaignFactory'), $c->get('displayFactory'), $c->get('layoutFactory'), - $c->get('dayPartFactory') + $c->get('dayPartFactory'), + $c->get('folderFactory'), + $c->get('commandFactory') ); $controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService')); return $controller; diff --git a/lib/XTR/SeedDatabaseTask.php b/lib/XTR/SeedDatabaseTask.php index b696998c18..c268f0ce5e 100644 --- a/lib/XTR/SeedDatabaseTask.php +++ b/lib/XTR/SeedDatabaseTask.php @@ -186,9 +186,14 @@ private function createDisplays(): void { // Create Displays $displays = [ - 'POP Display 1' => ['license' => Random::generateString(12, 'seed'), 'licensed' => false, 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4], - 'POP Display 2' => ['license' => Random::generateString(12, 'seed'), 'licensed' => false, 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4], - 'List Campaign Display 1' => ['license' => Random::generateString(12, 'seed'), 'licensed' => true, 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4], + 'POP Display 1' => ['license' => Random::generateString(12, 'seed'), 'licensed' => false, + 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4], + 'POP Display 2' => ['license' => Random::generateString(12, 'seed'), 'licensed' => false, + 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4], + 'List Campaign Display 1' => ['license' => Random::generateString(12, 'seed'), 'licensed' => true, + 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4], + 'List Campaign Display 2' => ['license' => Random::generateString(12, 'seed'), 'licensed' => true, + 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4], // 6 displays for xmds 'phpunitv7' => ['license' => 'PHPUnit7', 'licensed' => true, 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4], diff --git a/lib/routes-cypress.php b/lib/routes-cypress.php new file mode 100644 index 0000000000..072bb41ebe --- /dev/null +++ b/lib/routes-cypress.php @@ -0,0 +1,43 @@ +. + */ + +use Slim\Routing\RouteCollectorProxy; +use Xibo\Middleware\FeatureAuth; +use Xibo\Middleware\LayoutLock; +use Xibo\Middleware\SuperAdminAuth; + +defined('XIBO') or die('Sorry, you are not allowed to directly access this page.'); + + +/** + * Cypress endpoints + * @SWG\Tag( + * name="cypress", + * description="Cypress endpoints for tests" + * ) + */ + +$app->post('/createCommand', ['\Xibo\Controller\CypressTest','createCommand']); +$app->post('/createCampaign', ['\Xibo\Controller\CypressTest','createCampaign']); +$app->post('/scheduleCampaign', ['\Xibo\Controller\CypressTest','scheduleCampaign']); +$app->post('/displaySetStatus', ['\Xibo\Controller\CypressTest','displaySetStatus']); +$app->get('/displayStatusEquals', ['\Xibo\Controller\CypressTest','displayStatusEquals']); diff --git a/lib/routes.php b/lib/routes.php index cb5288c70f..5345daf70f 100644 --- a/lib/routes.php +++ b/lib/routes.php @@ -27,6 +27,10 @@ defined('XIBO') or die('Sorry, you are not allowed to directly access this page.'); +if (file_exists(PROJECT_ROOT . '/lib/routes-cypress.php')) { + include(PROJECT_ROOT . '/lib/routes-cypress.php'); +} + /** * @SWG\Swagger( * basePath="/api", @@ -96,10 +100,6 @@ ->add(new FeatureAuth($app->getContainer(), ['schedule.add'])) ->setName('schedule.add'); -$app->post('/scheduleCampaign', ['\Xibo\Controller\CypressTest','scheduleCampaign'])->setName('cypresstest.scheduleCampaign'); -$app->post('/displaySetStatus', ['\Xibo\Controller\CypressTest','displaySetStatus']); -$app->get('/displayStatusEquals', ['\Xibo\Controller\CypressTest','displayStatusEquals']); - $app->group('', function(RouteCollectorProxy $group) { $group->put('/schedule/{id}', ['\Xibo\Controller\Schedule','edit']) ->setName('schedule.edit'); diff --git a/tests/resources/seeds/layouts/export-layout-for-schedule-1.zip b/tests/resources/seeds/layouts/export-layout-for-schedule-1.zip new file mode 100644 index 0000000000..23524d5b3d Binary files /dev/null and b/tests/resources/seeds/layouts/export-layout-for-schedule-1.zip differ