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