diff --git a/cypress.config.js b/cypress.config.js index ae6edec063..a01cd77cd1 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -1,7 +1,7 @@ /* - * Copyright (c) 2022 Xibo Signage Ltd + * Copyright (C) 2022-2023 Xibo Signage Ltd * - * Xibo - Digital Signage - http://www.xibo.org.uk + * Xibo - Digital Signage - https://xibosignage.com * * This file is part of Xibo. * @@ -32,6 +32,7 @@ module.exports = defineConfig({ client_secret: "Pk6DdDgu2HzSoepcMHRabY60lDEvQ9ucTejYvc5dOgNVSNaOJirCUM83oAzlwe0KBiGR2Nhi6ltclyNC1rmcq0CiJZXzE42KfeatQ4j9npr6nMIQAzMal8O8RiYrIoono306CfyvSSJRfVfKExIjj0ZyE4TUrtPezJbKmvkVDzh8aj3kbanDKatirhwpfqfVdfgsqVNjzIM9ZgKHnbrTX7nNULL3BtxxNGgDMuCuvKiJFrLSyIIz1F4SNrHwHz" }, e2e: { + experimentalSessionAndOrigin: true, // We've imported your old cypress plugins here. // You may want to clean this up later by importing these. setupNodeEvents(on, config) { diff --git a/cypress/e2e/campaigns.cy.js b/cypress/e2e/campaigns.cy.js index 938851871c..a0cffb33e6 100644 --- a/cypress/e2e/campaigns.cy.js +++ b/cypress/e2e/campaigns.cy.js @@ -19,38 +19,24 @@ * along with Xibo. If not, see . */ -describe('Campaigns', function () { +/* eslint-disable max-len */ +describe('Campaigns', function() { + const testRun = Cypress._.random(0, 1e9); - var testRun = ""; - - beforeEach(function () { + beforeEach(function() { cy.login(); - - testRun = Cypress._.random(0, 1e9); }); - /** - * Create a number of layouts - */ - function createTempLayouts(num) { - for(let index = 1; index <= num; index++) { - var rand = Cypress._.random(0, 1e9); - cy.createLayout(rand).as('testLayoutId' + index); - } - } - - /** - * Delete a number of layouts - */ - function deleteTempLayouts(num) { - for(let index = 1; index <= num;index++) { - cy.get('@testLayoutId' + index).then((id) => { - cy.deleteLayout(id); - }); - } - } - - it('should add an empty campaign', function() { + // 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('/layout?*').as('layoutLoad'); + cy.intercept('/user/pref').as('userPref'); + + // Intercept the POST request to get the campaign Id + cy.intercept('/campaign').as('postCampaign'); cy.visit('/campaign/view'); @@ -65,114 +51,106 @@ describe('Campaigns', function () { // 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(); + cy.wait('@layoutLoad'); + cy.wait('@userPref'); // Should have no layouts assigned cy.get('.modal #LayoutAssignSortable').children() - .should('have.length', 0); - }); - - it.skip('should assign layouts to an existing campaign', function() { - - // Create some layouts - createTempLayouts(2); - - // Create a new campaign and then assign some layouts to it - cy.createCampaign('Cypress Test Campaign ' + testRun).then((res) => { - - cy.server(); - cy.route('/campaign?draw=3&*').as('campaignGridLoad'); - - cy.visit('/campaign/view'); - - // Filter for the created campaign - cy.get('#Filter input[name="name"]') - .type('Cypress Test Campaign ' + testRun); - - // Should have no layouts assigned - cy.get('#campaigns tbody tr').should('have.length', 1); - cy.get('#campaigns tbody tr:nth-child(1) td:nth-child(5)').contains('0'); - - // Click on the first row element to open the edit modal - cy.get('#campaigns tr:first-child .dropdown-toggle').click(); - cy.get('#campaigns tr:first-child .campaign_button_edit').click(); - - // Switch to the layouts tab. - cy.contains('.modal .nav-tabs .nav-link', 'Layouts').click(); - - // Assign 2 layouts - cy.get('#layoutAssignments tr:nth-child(1) a.assignItem').click(); - cy.get('#layoutAssignments tr:nth-child(2) a.assignItem').click(); + .should('have.length', 0); + cy.wait('@layoutLoad'); + cy.wait('@userPref'); + + // 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').trigger('change'); + cy.wait('@layoutLoad'); + cy.wait('@userPref'); + + // Assign a layout + cy.get('#layoutAssignments tr:nth-child(1) a.assignItem').click(); + cy.wait('@layoutLoad'); + cy.wait('@userPref'); + 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); - // Save - cy.get('.bootbox .save-button').click(); + // 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'); + }); - // Wait for 4th campaign grid reload - cy.wait('@campaignGridLoad'); + 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'); - // 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'); + // Schedule the campaign + cy.scheduleCampaign(sessionCampaignId, displayName).then((res) => { + cy.displaySetStatus(displayName, 1); - // Delete temp layouts - //deleteTempLayouts(2); - }); - }); + // Go to display grid + cy.intercept('/display?draw=3&*').as('displayGridLoad'); - it('searches and delete existing campaign', function() { - - // Create a new campaign and then search for it and delete it - cy.createCampaign('Cypress Test Campaign ' + testRun).then((res) => { - cy.visit('/campaign/view'); + cy.visit('/display/view'); // Filter for the created campaign - cy.get('#Filter input[name="name"]') - .type('Cypress Test Campaign ' + testRun); - - // Click on the first row element to open the delete modal - cy.get('#campaigns tbody tr').should('have.length', 1); - cy.get('#campaigns tr:first-child .dropdown-toggle').click(); - cy.get('#campaigns tr:first-child .campaign_button_delete').click(); + cy.get('.FilterDiv input[name="display"]') + .type(displayName); - // Delete test campaign - cy.get('.bootbox .save-button').click(); + // Should have the display + cy.get('#displays tbody tr').should('have.length', 1); - // Check if campaign is deleted in toast message - cy.contains('Deleted Cypress Test Campaign ' + testRun); + // 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('selects multiple campaigns and delete them', function() { - - // Create a new campaign and then search for it and delete it - cy.createCampaign('Cypress Test Campaign ' + testRun).then((res) => { - - cy.server(); - cy.route('/campaign?draw=2&*').as('campaignGridLoad'); + 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'); - // Delete all test campaigns - cy.visit('/campaign/view'); + // Filter for the created campaign + cy.get('#Filter input[name="name"]') + .type('Cypress Test Campaign ' + testRun); - // Clear filter and search for text campaigns - cy.get('#Filter input[name="name"]') - .clear() - .type('Cypress Test Campaign'); + // Wait for 2nd campaign grid reload + cy.wait('@campaignGridLoad'); - // Wait for 2nd campaign grid reload - cy.wait('@campaignGridLoad'); + cy.get('#campaigns tbody tr').should('have.length', 1); - // Select all - cy.get('button[data-toggle="selectAll"]').click(); + cy.get('#campaigns tr:first-child .dropdown-toggle').click(); + cy.get('#campaigns tr:first-child .campaign_button_delete').click(); - // Delete all - cy.get('.dataTables_info button[data-toggle="dropdown"]').click(); - cy.get('.dataTables_info a[data-button-id="campaign_button_delete"]').click(); + // Delete the campaign + cy.get('.bootbox .save-button').click(); - cy.get('button.save-button').click(); + // Wait for the intercepted DELETE request to complete with status 200 + cy.wait('@deleteCampaign').its('response.statusCode').should('eq', 200); - // Modal should contain one successful delete at least - cy.get('.modal-body').contains(': Success'); + // check the display status + cy.displayStatusEquals('List Campaign Display 1', 3).then((res) => { + expect(res.body).to.be.true; }); }); }); diff --git a/cypress/e2e/displaygroups.cy.js b/cypress/e2e/displaygroups.cy.js index 76c5512b94..fece0620cc 100644 --- a/cypress/e2e/displaygroups.cy.js +++ b/cypress/e2e/displaygroups.cy.js @@ -1,7 +1,7 @@ /* - * Copyright (c) 2022 Xibo Signage Ltd + * Copyright (C) 2023 Xibo Signage Ltd * - * Xibo - Digital Signage - http://www.xibo.org.uk + * Xibo - Digital Signage - https://xibosignage.com * * This file is part of Xibo. * @@ -40,7 +40,7 @@ describe('Display Groups', function () { .type('Cypress Test Displaygroup ' + testRun + '_1'); // Add first by clicking next - cy.get('.modal #dialog_btn_3').click(); + cy.get('.modal').contains('Next').click(); // Check if displaygroup is added in toast message cy.contains('Added Cypress Test Displaygroup ' + testRun + '_1'); diff --git a/cypress/e2e/layout_clock.cy.js b/cypress/e2e/layout_clock.cy.js index 9b0dd1fd5a..e5837b35e1 100644 --- a/cypress/e2e/layout_clock.cy.js +++ b/cypress/e2e/layout_clock.cy.js @@ -46,16 +46,19 @@ describe('Layout Designer', function() { 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(); - cy.wait('@saveWidget'); // 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.get('#advancedTab input[name="duration"]').clear().type(12).trigger('change'); + 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-element').click({force: true}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 931ffc288f..ce4b6a2465 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -45,7 +45,8 @@ // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) Cypress.Commands.add('login', function(callbackRoute = '/login') { - cy.visit(callbackRoute).then(function() { + cy.session('saveSession', () => { + cy.visit(callbackRoute); cy.request({ method: 'POST', url: '/login', @@ -419,6 +420,60 @@ Cypress.Commands.add('goToLayoutAndLoadPrefs', function(layoutId) { cy.wait('@userPrefsLoad'); }); +// Schedule a layout +Cypress.Commands.add('scheduleCampaign', function(campaignId, displayName) { + cy.request({ + method: 'POST', + url: '/api/scheduleCampaign', + form: true, + headers: { + Authorization: 'Bearer ' + Cypress.env('accessToken'), + }, + body: { + campaignId: campaignId, + displayName: displayName, + }, + }).then((res) => { + return res.body.eventId; + }); +}); + +// Set Display Status +Cypress.Commands.add('displaySetStatus', function(displayName, statusId) { + cy.request({ + method: 'POST', + url: '/api/displaySetStatus', + form: true, + headers: { + Authorization: 'Bearer ' + Cypress.env('accessToken'), + }, + body: { + displayName: displayName, + statusId: statusId, + }, + }).then((res) => { + return res.body; + }); +}); + +// Check Display Status +Cypress.Commands.add('displayStatusEquals', function(displayName, statusId) { + cy.request({ + method: 'GET', + url: '/api/displayStatusEquals', + form: true, + headers: { + Authorization: 'Bearer ' + Cypress.env('accessToken'), + }, + body: { + displayName: displayName, + statusId: statusId, + }, + }).then((res) => { + return res; + }); +}); + /** * Force open toolbar menu * @param {number} menuIdx diff --git a/lib/Controller/CypressTest.php b/lib/Controller/CypressTest.php new file mode 100644 index 0000000000..1252ced913 --- /dev/null +++ b/lib/Controller/CypressTest.php @@ -0,0 +1,244 @@ +. + */ +namespace Xibo\Controller; + +use Carbon\Carbon; +use Psr\Http\Message\ResponseInterface; +use Slim\Http\Response as Response; +use Slim\Http\ServerRequest as Request; +use Xibo\Entity\Display; +use Xibo\Factory\CampaignFactory; +use Xibo\Factory\DayPartFactory; +use Xibo\Factory\DisplayFactory; +use Xibo\Factory\DisplayGroupFactory; +use Xibo\Factory\LayoutFactory; +use Xibo\Factory\ScheduleFactory; +use Xibo\Helper\Session; +use Xibo\Storage\StorageServiceInterface; +use Xibo\Support\Exception\ControllerNotImplemented; +use Xibo\Support\Exception\GeneralException; +use Xibo\Support\Exception\InvalidArgumentException; +use Xibo\Support\Exception\NotFoundException; + +/** + * Class CypressTest + * @package Xibo\Controller + */ +class CypressTest extends Base +{ + /** @var StorageServiceInterface */ + private $store; + + /** + * @var Session + */ + private $session; + + /** + * @var ScheduleFactory + */ + private $scheduleFactory; + + /** + * @var DisplayGroupFactory + */ + private $displayGroupFactory; + + /** + * @var CampaignFactory + */ + private $campaignFactory; + + /** @var DisplayFactory */ + private $displayFactory; + + /** @var LayoutFactory */ + private $layoutFactory; + + /** @var DayPartFactory */ + private $dayPartFactory; + + /** + * Set common dependencies. + * @param StorageServiceInterface $store + * @param Session $session + * @param ScheduleFactory $scheduleFactory + * @param DisplayGroupFactory $displayGroupFactory + * @param CampaignFactory $campaignFactory + * @param DisplayFactory $displayFactory + * @param LayoutFactory $layoutFactory + * @param DayPartFactory $dayPartFactory + */ + + public function __construct( + $store, + $session, + $scheduleFactory, + $displayGroupFactory, + $campaignFactory, + $displayFactory, + $layoutFactory, + $dayPartFactory + ) { + $this->store = $store; + $this->session = $session; + $this->scheduleFactory = $scheduleFactory; + $this->displayGroupFactory = $displayGroupFactory; + $this->campaignFactory = $campaignFactory; + $this->displayFactory = $displayFactory; + $this->layoutFactory = $layoutFactory; + $this->dayPartFactory = $dayPartFactory; + } + + /** + * @throws InvalidArgumentException + * @throws ControllerNotImplemented + * @throws NotFoundException + * @throws GeneralException + */ + public function scheduleCampaign(Request $request, Response $response): Response|ResponseInterface + { + $this->getLog()->debug('Add Schedule'); + $sanitizedParams = $this->getSanitizer($request->getParams()); + + $schedule = $this->scheduleFactory->createEmpty(); + $schedule->userId = $this->getUser()->userId; + $schedule->eventTypeId = 5; + $schedule->campaignId = $sanitizedParams->getInt('campaignId'); + $schedule->commandId = $sanitizedParams->getInt('commandId'); + $schedule->displayOrder = $sanitizedParams->getInt('displayOrder', ['default' => 0]); + $schedule->isPriority = $sanitizedParams->getInt('isPriority', ['default' => 0]); + $schedule->isGeoAware = $sanitizedParams->getCheckbox('isGeoAware'); + $schedule->actionType = $sanitizedParams->getString('actionType'); + $schedule->actionTriggerCode = $sanitizedParams->getString('actionTriggerCode'); + $schedule->actionLayoutCode = $sanitizedParams->getString('actionLayoutCode'); + $schedule->maxPlaysPerHour = $sanitizedParams->getInt('maxPlaysPerHour', ['default' => 0]); + $schedule->syncGroupId = $sanitizedParams->getInt('syncGroupId'); + + // Set the parentCampaignId for campaign events + if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$CAMPAIGN_EVENT) { + $schedule->parentCampaignId = $schedule->campaignId; + + // Make sure we're not directly scheduling an ad campaign + $campaign = $this->campaignFactory->getById($schedule->campaignId); + if ($campaign->type === 'ad') { + throw new InvalidArgumentException( + __('Direct scheduling of an Ad Campaign is not allowed'), + 'campaignId' + ); + } + } + + // Fields only collected for interrupt events + if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$INTERRUPT_EVENT) { + $schedule->shareOfVoice = $sanitizedParams->getInt('shareOfVoice', [ + 'throw' => function () { + new InvalidArgumentException( + __('Share of Voice must be a whole number between 0 and 3600'), + 'shareOfVoice' + ); + } + ]); + } else { + $schedule->shareOfVoice = null; + } + + $schedule->dayPartId = 2; + $schedule->syncTimezone = 0; + + $displays = $this->displayFactory->query(null, ['display' => $sanitizedParams->getString('displayName')]); + $display = $displays[0]; + $schedule->assignDisplayGroup($this->displayGroupFactory->getById($display->displayGroupId)); + + // Ready to do the add + $schedule->setDisplayNotifyService($this->displayFactory->getDisplayNotifyService()); + if ($schedule->campaignId != null) { + $schedule->setCampaignFactory($this->campaignFactory); + } + $schedule->save(); + + // Return + $this->getState()->hydrate([ + 'httpStatus' => 201, + 'message' => __('Added Event'), + 'id' => $schedule->eventId, + 'data' => $schedule + ]); + + return $this->render($request, $response); + } + + /** + * @throws NotFoundException + * @throws ControllerNotImplemented + * @throws GeneralException + */ + public function displaySetStatus(Request $request, Response $response): Response|ResponseInterface + { + $this->getLog()->debug('Set display status'); + $sanitizedParams = $this->getSanitizer($request->getParams()); + + $displays = $this->displayFactory->query(null, ['display' => $sanitizedParams->getString('displayName')]); + $display = $displays[0]; + + // Get the display + $status = $sanitizedParams->getInt('statusId'); + + // Set display status + $display->mediaInventoryStatus = $status; + + $this->store->update('UPDATE `display` SET MediaInventoryStatus = :status, auditingUntil = :auditingUntil + WHERE displayId = :displayId', [ + 'displayId' => $display->displayId, + 'auditingUntil' => Carbon::now()->addSeconds(86400)->format('U'), + 'status' => Display::$STATUS_DONE + ]); + $this->store->commitIfNecessary(); + $this->store->close(); + + return $this->render($request, $response); + } + + /** + * @throws NotFoundException + * @throws ControllerNotImplemented + * @throws GeneralException + */ + public function displayStatusEquals(Request $request, Response $response): Response|ResponseInterface + { + $this->getLog()->debug('Check display status'); + $sanitizedParams = $this->getSanitizer($request->getParams()); + + // Get the display + $displays = $this->displayFactory->query(null, ['display' => $sanitizedParams->getString('displayName')]); + $display = $displays[0]; + $status = $sanitizedParams->getInt('statusId'); + + // Check display status + $this->getState()->hydrate([ + 'httpStatus' => 201, + 'data' => $display->mediaInventoryStatus === $status + ]); + + return $this->render($request, $response); + } +} diff --git a/lib/Dependencies/Controllers.php b/lib/Dependencies/Controllers.php index 800127ecce..6e5da7df59 100644 --- a/lib/Dependencies/Controllers.php +++ b/lib/Dependencies/Controllers.php @@ -478,6 +478,20 @@ public static function registerControllersWithDi() $controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService')); return $controller; }, + '\Xibo\Controller\CypressTest' => function (ContainerInterface $c) { + $controller = new \Xibo\Controller\CypressTest( + $c->get('store'), + $c->get('session'), + $c->get('scheduleFactory'), + $c->get('displayGroupFactory'), + $c->get('campaignFactory'), + $c->get('displayFactory'), + $c->get('layoutFactory'), + $c->get('dayPartFactory') + ); + $controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService')); + return $controller; + }, '\Xibo\Controller\Sessions' => function (ContainerInterface $c) { $controller = new \Xibo\Controller\Sessions( $c->get('store'), diff --git a/lib/XTR/SeedDatabaseTask.php b/lib/XTR/SeedDatabaseTask.php index 499886b7fb..b696998c18 100644 --- a/lib/XTR/SeedDatabaseTask.php +++ b/lib/XTR/SeedDatabaseTask.php @@ -188,6 +188,7 @@ private function createDisplays(): void $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], // 6 displays for xmds 'phpunitv7' => ['license' => 'PHPUnit7', 'licensed' => true, 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4], diff --git a/lib/routes.php b/lib/routes.php index 2bd68d2a28..cb5288c70f 100644 --- a/lib/routes.php +++ b/lib/routes.php @@ -96,6 +96,10 @@ ->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-list-campaign-layout-1.zip b/tests/resources/seeds/layouts/export-list-campaign-layout-1.zip new file mode 100644 index 0000000000..fb9a68a621 Binary files /dev/null and b/tests/resources/seeds/layouts/export-list-campaign-layout-1.zip differ diff --git a/tests/resources/seeds/layouts/export-list-campaign-layout-2.zip b/tests/resources/seeds/layouts/export-list-campaign-layout-2.zip new file mode 100644 index 0000000000..cda32e193b Binary files /dev/null and b/tests/resources/seeds/layouts/export-list-campaign-layout-2.zip differ