V2 message manager (#234)

* refactor: server run script
* refactor: extract message control feature
* refactor: socket usage for message control feature
* refactor: migrate e2e testing to playwright
* chore: add feature tests
* refactor: add validation on socket controller
* chore: jest tests server logic
* chore: update tests
* chore: run playwright tests on pull_requests
This commit is contained in:
Carlos Valente
2022-10-29 20:40:38 +02:00
committed by GitHub
parent 918f8517d2
commit a3f14182a4
26 changed files with 476 additions and 1048 deletions
+16 -7
View File
@@ -3,7 +3,7 @@
name: ontime_test_CI
on: [push, pull_request]
on: [pull_request]
jobs:
build:
@@ -43,12 +43,21 @@ jobs:
run: yarn install && yarn setdb
working-directory: ./server
- name: Electron - Run tests
- name: Server - run tests
run: yarn test
working-directory: ./server
- name: Cypress run
uses: cypress-io/github-action@v2
with:
working-directory: ./server
start: yarn cypress
# - name: Install Playwright Browsers
# run: npx playwright install --with-deps
# working-directory: ./server
#
# - name: Run Playwright tests
# run: yarn e2e
# working-directory: ./server
#
# - uses: actions/upload-artifact@v3
# if: always()
# with:
# name: playwright-report
# path: playwright-report/
# retention-days: 7
@@ -12,7 +12,6 @@ export default function TooltipActionBtn(props: TooltipActionBtnProps) {
<Tooltip label={tooltip} openDelay={openDelay}>
<IconButton
{...rest}
aria-label={tooltip}
size={size}
icon={icon}
onClick={clickHandler}
@@ -24,7 +24,7 @@ export default function InputRow(props: InputRowProps) {
return (
<div className={`${visible ? style.inputRowActive : ''}`}>
<span className={style.label}>{label}</span>
<label className={style.label}>{label}</label>
<div className={style.inputItems}>
<Input
size='sm'
@@ -36,7 +36,7 @@ export default function InputRow(props: InputRowProps) {
<TooltipActionBtn
clickHandler={() => actionHandler('update', { field: 'isPublic', value: !visible })}
tooltip={visible ? 'Make invisible' : 'Make visible'}
aria-label='Toggle tooltip visibility'
aria-label={`Toggle ${label}`}
openDelay={tooltipDelayMid}
icon={<IoSunny size='18px' />}
colorScheme='blue'
+7 -42
View File
@@ -2,6 +2,7 @@
import { useEffect, useMemo, useState } from 'react';
import { useSocket } from '../../common/context/socketContext';
import { useMessageControlProvider } from '../../common/hooks/useSocketProvider';
import useSubscription from '../../common/hooks/useSubscription';
import useEvent from '../../common/hooks-query/useEvent';
import useEventsList from '../../common/hooks-query/useEventsList';
@@ -12,20 +13,9 @@ const withSocket = (Component) => {
const { data: eventsData } = useEventsList();
const { data: genData } = useEvent();
const { data: viewSettings } = useViewSettings();
const { data: messages } = useMessageControlProvider();
const socket = useSocket();
const [pres, setPres] = useState({
text: '',
visible: false,
});
const [publ, setPubl] = useState({
text: '',
visible: false,
});
const [lower, setLower] = useState({
text: '',
visible: false,
});
const [publicSelectedId, setPublicSelectedId] = useState(null);
const [timer] = useSubscription('timer', {
@@ -61,35 +51,10 @@ const withSocket = (Component) => {
if (!socket) {
return;
}
// Handle timer messages
socket.on('messages-timer', (data) => {
setPres({ ...data });
});
// Handle public messages
socket.on('messages-public', (data) => {
setPubl({ ...data });
});
// Handle lower third messages
socket.on('messages-lower', (data) => {
setLower({ ...data });
});
// todo: remove
socket.on('publicselected-id', (data) => {
setPublicSelectedId(data);
});
// Ask for up to date data
socket.emit('get-messages');
// Clear listeners
return () => {
socket.off('messages-public');
socket.off('messages-timer');
socket.off('messages-lower');
};
}, [socket]);
@@ -161,9 +126,9 @@ const withSocket = (Component) => {
return (
<Component
{...props}
pres={pres}
publ={publ}
lower={lower}
pres={messages.presenter}
publ={messages.public}
lower={messages.lower}
title={titleManager}
publicTitle={publicTitleManager}
time={TimeManagerType}
@@ -174,7 +139,7 @@ const withSocket = (Component) => {
viewSettings={viewSettings}
nextId={nextId}
general={genData}
onAir={onAir}
onAir={messages.onAir}
/>
);
};
@@ -94,7 +94,12 @@ export default function StudioClock(props) {
</div>
</div>
<div className='schedule-container'>
<div className={onAir ? 'onAir' : 'onAir onAir--idle'}>ON AIR</div>
<div
className={onAir ? 'onAir' : 'onAir onAir--idle'}
data-testid={onAir ? 'on-air-enabled' : 'on-air-disabled'}
>
ON AIR
</div>
<div className='schedule'>
<ul>
{schedule.map((s) => (
+4
View File
@@ -0,0 +1,4 @@
node_modules/
/test-results/
/playwright-report/
/playwright/.cache/
-5
View File
@@ -1,5 +0,0 @@
{
"viewportWidth": 1920,
"viewportHeight": 1080,
"video": false
}
@@ -1,172 +0,0 @@
// go through routes and make sure things render as expected
describe('validate routes', () => {
describe('viewer routes', () => {
it('default to stage timer', () => {
cy.visit('http://localhost:4001/');
cy.get('[data-testid="timer-view"]').should('exist');
cy.contains('Time Now');
});
it('renders stage timer routes', () => {
cy.visit('http://localhost:4001/timer');
cy.get('[data-testid="timer-view"]').should('exist');
cy.contains('Time Now');
cy.visit('http://localhost:4001/presenter');
cy.get('[data-testid="timer-view"]').should('exist');
cy.contains('Time Now');
cy.visit('http://localhost:4001/speaker');
cy.get('[data-testid="timer-view"]').should('exist');
cy.contains('Time Now');
cy.visit('http://localhost:4001/stage');
cy.get('[data-testid="timer-view"]').should('exist');
cy.contains('Time Now');
});
it('renders backstage routes', () => {
cy.visit('http://localhost:4001/backstage');
cy.get('[data-testid="backstage-view"]').should('exist');
cy.contains('Today');
cy.contains('Time Now');
cy.contains('Info');
cy.visit('http://localhost:4001/sm');
cy.get('[data-testid="backstage-view"]').should('exist');
cy.contains('Today');
cy.contains('Time Now');
cy.contains('Info');
});
it('renders public view', () => {
cy.visit('http://localhost:4001/public');
cy.get('[data-testid="public-view"]').should('exist');
cy.contains('Today');
cy.contains('Time Now');
cy.contains('Info');
});
it('renders pip view', () => {
cy.visit('http://localhost:4001/pip');
cy.get('[data-testid="pip-view"]').should('exist');
cy.contains('Today');
});
it('renders lower third with no errors', () => {
cy.visit('http://localhost:4001/lower');
cy.get('[data-testid="error-container"]').should('not.exist');
});
it('renders lower third with no errors', () => {
cy.visit('http://localhost:4001/lower');
cy.get('[data-testid="error-container"]').should('not.exist');
});
it('renders studio clock', () => {
cy.visit('http://localhost:4001/studio');
cy.get('[data-testid="studio-view"]').should('exist');
cy.contains('ON AIR');
});
it('renders countdown selection', () => {
cy.visit('http://localhost:4001/countdown');
cy.get('[data-testid="countdown-view"]').should('exist');
cy.get('[data-testid="countdown-select"]').should('exist');
cy.contains('Select an event to follow');
});
it('renders countdown with event', () => {
cy.visit('http://localhost:4001/countdown?event=1');
cy.get('[data-testid="countdown-view"]').should('exist');
cy.get('[data-testid="countdown-event"]').should('exist');
cy.contains('Time Now');
cy.contains('Start Time');
cy.contains('End Time');
});
});
describe('clock view timer', () => {
it('renders base route', () => {
cy.visit('http://localhost:4001/clock');
cy.get('[data-testid="clock-view"]').should('exist');
cy.get('.App').should('not.contain', 'Time Now');
});
});
describe('minimal timer and options', () => {
it('renders base route', () => {
cy.visit('http://localhost:4001/minimal');
cy.get('[data-testid="minimal-timer"]').should('exist');
cy.get('.App').should('not.contain', 'Time Now');
});
it('renders with defined options', () => {
cy.visit(
'http://localhost:4001/minimal?font=arial=1&size=1.5&text=0f0&key=ff0&hideovertime=true&alignx=start&aligny=end&offsetx=100&offsety=-100&hidemessages=true'
);
cy.get('[data-testid="minimal-timer"]').should('exist');
cy.get('.App').should('not.contain', 'Time Now');
});
it('hides nav on given option', () => {
cy.visit('http://localhost:4001/minimal?hidenav=true');
cy.get('[data-testid="minimal-timer"]').should('exist');
cy.get('.App').should('not.contain', 'Time Now');
cy.get('[data-testid="nav-logo"]').should('not.exist');
});
});
describe('editor routes', () => {
it('app editor', () => {
cy.visit('http://localhost:4001/editor');
cy.get('[data-testid="event-editor"]').should('exist');
cy.get('[data-testid="panel-event-list"]').should('exist');
cy.get('[data-testid="panel-timer-control"]').should('exist');
cy.get('[data-testid="panel-messages-control"]').should('exist');
cy.get('[data-testid="panel-info"]').should('exist');
});
it('self contained eventlist', () => {
cy.visit('http://localhost:4001/eventlist');
cy.get('[data-testid="panel-event-list"]').should('exist');
cy.get('[data-testid="panel-timer-control"]').should('not.exist');
cy.get('[data-testid="panel-messages-control"]').should('not.exist');
cy.get('[data-testid="panel-info"]').should('not.exist');
});
it('self contained timercontrol', () => {
cy.visit('http://localhost:4001/timercontrol');
cy.get('[data-testid="panel-event-list"]').should('not.exist');
cy.get('[data-testid="panel-timer-control"]').should('exist');
cy.get('[data-testid="panel-messages-control"]').should('not.exist');
cy.get('[data-testid="panel-info"]').should('not.exist');
});
it('self contained messagecontrol', () => {
cy.visit('http://localhost:4001/messagecontrol');
cy.get('[data-testid="panel-event-list"]').should('not.exist');
cy.get('[data-testid="panel-timer-control"]').should('not.exist');
cy.get('[data-testid="panel-messages-control"]').should('exist');
cy.get('[data-testid="panel-info"]').should('not.exist');
});
it('self contained info', () => {
cy.visit('http://localhost:4001/info');
cy.get('[data-testid="panel-event-list"]').should('not.exist');
cy.get('[data-testid="panel-timer-control"]').should('not.exist');
cy.get('[data-testid="panel-messages-control"]').should('not.exist');
cy.get('[data-testid="panel-info"]').should('exist');
});
});
it('table routes', () => {
cy.visit('http://localhost:4001/table');
cy.contains('Running Timer');
cy.get('[data-testid="cuesheet"]').should('exist');
cy.visit('http://localhost:4001/cuesheet');
cy.contains('Running Timer');
cy.get('[data-testid="cuesheet"]').should('exist');
cy.visit('http://localhost:4001/cuelist');
cy.contains('Running Timer');
cy.get('[data-testid="cuesheet"]').should('exist');
});
});
-22
View File
@@ -1,22 +0,0 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
-25
View File
@@ -1,25 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
-20
View File
@@ -1,20 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
+9 -11
View File
@@ -12,7 +12,7 @@
"license": "AGPL-3.0-only",
"main": "main.js",
"devDependencies": {
"cypress": "^9.6.0",
"@playwright/test": "^1.27.1",
"electron": "^21.1.0",
"electron-builder": "^23.6.0",
"eslint": "^8.25.0",
@@ -32,9 +32,8 @@
"prep": "yarn clean && yarn setdb",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"start": "NODE_ENV=development electron .",
"start:server": "nodemon --experimental-modules --es-module-specifier-resolution=node src/app.js",
"cy:run": "cypress run",
"cypress": "start-server-and-test start http://localhost:4001 cy:run",
"start:server": "nodemon --experimental-modules --es-module-specifier-resolution=node src/run.js",
"e2e": "set IS_TEST=true && playwright test",
"pack": "electron-builder --dir",
"dist": "electron-builder",
"dist-win": "electron-builder --publish=never --x64 --win",
@@ -44,10 +43,7 @@
},
"jest": {
"testEnvironment": "node",
"testPathIgnorePatterns": [
"dist",
"cypress"
]
"rootDir": "src"
},
"build": {
"productName": "ontime",
@@ -88,7 +84,7 @@
"!**/{yarn.lock,yarn-error.log}",
"!**/{test,tests,__test__,__tests__}",
"!**/{mock,mocks,__mock__,__mocks__}",
"!*{.spec.js,*.test.js}"
"!*{.spec.js,*.test.js,*.spec.ts,.test.ts}"
],
"directories": {
"buildResources": "./assets/"
@@ -101,7 +97,8 @@
"**/*",
"!**/{yarn.lock,yarn-error.log}",
"!**/{test,tests,__test__,__tests__}",
"!**/{mock,mocks,__mock__,__mocks__}"
"!**/{mock,mocks,__mock__,__mocks__}",
"!*{.spec.js,*.test.js,*.spec.ts,.test.ts}"
]
},
{
@@ -111,7 +108,8 @@
"**/*",
"!**/{yarn.lock,yarn-error.log}",
"!**/{test,tests,__test__,__tests__}",
"!**/{mock,mocks,__mock__,__mocks__}"
"!**/{mock,mocks,__mock__,__mocks__}",
"!*{.spec.js,*.test.js,*.spec.ts,.test.ts}"
]
}
]
+84
View File
@@ -0,0 +1,84 @@
import type { PlaywrightTestConfig } from '@playwright/test';
import { devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: './tests',
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
screenshot: 'only-on-failure', // Capture screenshot after each test failure
viewport: { width: 1920, height: 1080 },
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:4001',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
testMatch: /.*.spec.ts/,
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
testMatch: /.*.spec.ts/,
use: {
...devices['Desktop Safari'],
},
},
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
webServer: {
command: 'yarn start:server',
timeout: 120 * 1000,
port: 4001,
},
};
export default config;
+13 -3
View File
@@ -31,6 +31,7 @@ import { DataProvider } from './classes/data-provider/DataProvider.js';
const env = process.env.NODE_ENV || 'production';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const isTest = process.env.IS_TEST;
export const { db, data } = await loadDb(__dirname);
console.log(`Starting ontime version ${process.env.npm_package_version}`);
@@ -60,12 +61,21 @@ app.use('/playback', playbackRouter);
// serve static - css
app.use('/external', express.static(join(__dirname, 'external')));
// serve static - react
app.use(express.static(join(__dirname, env === 'production' ? '../' : '../../', 'client/build')));
// serve static - react, in test mode we fetch the react app from module
const resolvedPath = () => {
const sameModule = '../';
const siblingModule = '../../';
if (env === 'production' && !isTest) {
return sameModule;
}
return siblingModule;
};
app.use(express.static(join(__dirname, resolvedPath(), 'client/build')));
app.get('*', (req, res) => {
res.sendFile(
resolve(__dirname, env === 'production' ? '../' : '../../', 'client', 'build', 'index.html'),
resolve(__dirname, resolvedPath(), 'client', 'build', 'index.html'),
);
});
@@ -0,0 +1,102 @@
let instance;
export class MessageManager {
constructor() {
if (instance) {
throw new Error('There can be only one');
}
instance = this;
this.socket = null;
this.presenter = {
text: '',
visible: false,
};
this.public = {
text: '',
visible: false,
};
this.lower = {
text: '',
visible: false,
};
this.onAir = false;
}
/**
* @description sets message on stage timer screen
* @param payload {string}
*/
setTimerText(payload) {
this.presenter.text = payload;
return this.getAll();
}
/**
* @description sets message visibility on stage timer screen
* @param status {boolean}
*/
setTimerVisibility(status) {
this.presenter.visible = status;
return this.getAll();
}
/**
* @description sets message on public screen
* @param payload {string}
*/
setPublicText(payload) {
this.public.text = payload;
return this.getAll();
}
/**
* @description sets message visibility on public screen
* @param status {boolean}
*/
setPublicVisibility(status) {
this.public.visible = status;
return this.getAll();
}
/**
* @description sets message on lower third screen
* @param payload {string}
*/
setLowerText(payload) {
this.lower.text = payload;
return this.getAll();
}
/**
* @description sets message visibility on lower third screen
* @param status {boolean}
*/
setLowerVisibility(status) {
this.lower.visible = status;
return this.getAll();
}
/**
* @description set state of onAir
* @param status {boolean}
*/
setOnAir(status) {
this.onAir = status;
return this.getAll();
}
/**
* @description Returns feature data
*/
getAll() {
return {
presenter: this.presenter,
public: this.public,
lower: this.lower,
onAir: this.onAir,
};
}
}
export const messageManager = new MessageManager();
+49 -28
View File
@@ -1,8 +1,12 @@
import { Server } from 'socket.io';
import getRandomName from '../../utils/getRandomName.js';
import { generateId } from '../../utils/generate_id.js';
import { stringFromMillis } from '../../utils/time.js';
import { Timer } from '../timer/Timer.js';
import { messageManager } from '../message-manager/MessageManager.js';
import { ADDRESS_MESSAGE_CONTROL } from './socketConfig.js';
class SocketController {
constructor() {
@@ -167,17 +171,6 @@ class SocketController {
global.timer.increment(delayTime * 1000 * 60);
});
socket.on('set-onAir', (data) => {
try {
const d = JSON.parse(data);
d ? global.timer.trigger('onAir') : global.timer.trigger('offAir');
} catch (error) {
this.error('RX', `Failed to parse message ${data}`);
}
this.send('onAir', global.timer.onAir);
global.timer._broadcastFeatureMessageControl();
});
/*******************************************/
// general playback state, useful for external sync
socket.on('ontime-poll', () => {
@@ -249,41 +242,71 @@ class SocketController {
/*** ------------------------- ***/
/***********************************/
/*******************************************/
// On Air
socket.on('set-onAir', (data) => {
if (typeof data === 'boolean') {
try {
const featureData = messageManager.setOnAir(data);
this.info('PLAYBACK', featureData.onAir ? 'Going On Air' : 'Going Off Air');
this.socket.emit(ADDRESS_MESSAGE_CONTROL, featureData);
} catch (error) {
this.error('RX', `Failed to parse message ${data} : ${error}`);
}
}
this.send('onAir', messageManager.onAir);
});
// Presenter message
socket.on('set-timer-message-text', (data) => {
global.timer._setTitles('set-timer-text', data);
global.timer._broadcastFeatureMessageControl();
if (typeof data !== 'string') {
return;
}
const featureData = messageManager.setTimerText(data);
this.socket.emit(ADDRESS_MESSAGE_CONTROL, featureData);
});
socket.on('set-timer-message-visible', (data) => {
global.timer._setTitles('set-timer-visible', data);
global.timer._broadcastFeatureMessageControl();
if (typeof data !== 'boolean') {
return;
}
const featureData = messageManager.setTimerVisibility(data);
this.socket.emit(ADDRESS_MESSAGE_CONTROL, featureData);
});
/*******************************************/
// Public message
socket.on('set-public-message-text', (data) => {
global.timer._setTitles('set-public-text', data);
global.timer._broadcastFeatureMessageControl();
if (typeof data !== 'string') {
return;
}
const featureData = messageManager.setPublicText(data);
this.socket.emit(ADDRESS_MESSAGE_CONTROL, featureData);
});
socket.on('set-public-message-visible', (data) => {
global.timer._setTitles('set-public-visible', data);
global.timer._broadcastFeatureMessageControl();
if (typeof data !== 'boolean') {
return;
}
const featureData = messageManager.setPublicVisibility(data);
this.socket.emit(ADDRESS_MESSAGE_CONTROL, featureData);
});
/*******************************************/
// Lower third message
socket.on('set-lower-message-text', (data) => {
global.timer._setTitles('set-lower-text', data);
global.timer._broadcastFeatureMessageControl();
if (typeof data !== 'string') {
return;
}
const featureData = messageManager.setLowerText(data);
this.socket.emit(ADDRESS_MESSAGE_CONTROL, featureData);
});
socket.on('set-lower-message-visible', (data) => {
global.timer._setTitles('set-lower-visible', data);
global.timer._broadcastFeatureMessageControl();
if (typeof data !== 'boolean') {
return;
}
const featureData = messageManager.setLowerVisibility(data);
this.socket.emit(ADDRESS_MESSAGE_CONTROL, featureData);
});
/* MOLECULAR ENDPOINTS
@@ -303,7 +326,8 @@ class SocketController {
// 2. MESSAGE CONTROL
socket.on('get-ontime-feat-messagecontrol', () => {
global.timer._broadcastFeatureMessageControl();
const featureData = messageManager.getAll();
this.socket.emit(ADDRESS_MESSAGE_CONTROL, featureData);
});
// 3. PLAYBACK CONTROL
@@ -337,9 +361,6 @@ class SocketController {
/**
* Logger logic
* -------------
*
* This should be separate of event timer, left here for convenience
*
*/
/**
@@ -0,0 +1 @@
export const ADDRESS_MESSAGE_CONTROL = 'ontime-feat-messagecontrol';
+1 -100
View File
@@ -47,26 +47,11 @@ export class EventTimer extends Timer {
// HTTP Client Object
this.http = null;
this._interval = null;
this.presenter = {
text: '',
visible: false,
};
this.public = {
text: '',
visible: false,
};
this.lower = {
text: '',
visible: false,
};
// call general title reset
this._resetSelection();
this._eventlist = [];
this.onAir = false;
// set recurrent emits
this._interval = setInterval(() => this.runCycle(), timerConfig?.refresh || 1000);
@@ -154,20 +139,6 @@ export class EventTimer extends Timer {
this.socket.send('ontime-feat-eventlist', featureData);
}
/**
* @description Broadcast data for Message Control feature
* @private
*/
_broadcastFeatureMessageControl() {
const featureData = {
presenter: this.presenter,
public: this.public,
lower: this.lower,
onAir: this.onAir,
};
this.socket.send('ontime-feat-messagecontrol', featureData);
}
/**
* @description Broadcast data for Playback Control feature
* @private
@@ -213,7 +184,6 @@ export class EventTimer extends Timer {
broadcastState() {
// feature sync
this._broadcastFeatureEventList();
this._broadcastFeatureMessageControl();
this._broadcastFeaturePlaybackControl();
this._broadcastFeatureInfo();
this._broadcastFeatureCuesheet();
@@ -234,7 +204,6 @@ export class EventTimer extends Timer {
this.socket.send('publicnext-id', this.nextPublicEventId);
this.socket.send('titles', this.titles);
this.socket.send('publictitles', this.titlesPublic);
this.socket.send('onAir', this.onAir);
}
/**
@@ -347,18 +316,6 @@ export class EventTimer extends Timer {
this.reload();
break;
}
case 'onAir': {
// Call action
this.socket.info('PLAYBACK', 'Going On Air');
this.setonAir(true);
break;
}
case 'offAir': {
// Call action and force update
this.socket.info('PLAYBACK', 'Going Off Air');
this.setonAir(false);
break;
}
default: {
// Error, disable flag
this.socket.error('RX', `Unhandled action triggered ${action}`);
@@ -572,52 +529,6 @@ export class EventTimer extends Timer {
}
}
/**
* Set titles and broadcast change
* @param {string} action
* @param {any} payload
* @private
*/
_setTitles(action, payload) {
switch (action) {
/*******************************************/
// Presenter message
case 'set-timer-text':
this.presenter.text = payload;
this.socket.send('messages-timer', this.presenter);
break;
case 'set-timer-visible':
this.presenter.visible = payload;
this.socket.send('messages-timer', this.presenter);
break;
/*******************************************/
// Public message
case 'set-public-text':
this.public.text = payload;
this.socket.send('messages-public', this.public);
break;
case 'set-public-visible':
this.public.visible = payload;
this.socket.send('messages-public', this.public);
break;
/*******************************************/
// Lower third message
case 'set-lower-text':
this.lower.text = payload;
this.socket.send('messages-lower', this.lower);
break;
case 'set-lower-visible':
this.lower.visible = payload;
this.socket.send('messages-lower', this.lower);
break;
default:
break;
}
}
/**
* Deletes running event list from object
*/
@@ -1112,16 +1023,6 @@ export class EventTimer extends Timer {
this.nextPublicEventId = null;
}
/**
* @description Set onAir property of timer
* @param {boolean} onAir - whether flag is active
*/
setonAir(onAir) {
this.onAir = onAir;
// broadcast change
this.socket.send('onAir', onAir);
}
/**
* @description start timer
*/
@@ -36,10 +36,6 @@ test('object instantiates correctly', async () => {
expect(t.osc).toBeNull();
expect(t.http).toBeNull();
expect(t._interval).not.toBeNull();
expect(t.presenter).toStrictEqual({ text: '', visible: false });
expect(t.public).toStrictEqual({ text: '', visible: false });
expect(t.lower).toStrictEqual({ text: '', visible: false });
expect(t.lower).toStrictEqual({ text: '', visible: false });
const expectTitlesPublic = {
titleNow: null,
@@ -90,12 +86,6 @@ describe('test triggers behaviour', () => {
expect(t.trigger('previous')).toBeFalsy();
expect(t.trigger('next')).toBeFalsy();
expect(t.trigger('reload')).toBeFalsy();
expect(t.onAir).toBeFalsy();
expect(t.trigger('onAir')).toBeTruthy();
expect(t.onAir).toBeTruthy();
expect(t.trigger('offAir')).toBeTruthy();
expect(t.onAir).toBeFalsy();
done();
});
@@ -4,18 +4,6 @@ export const pbGet = async (req, res) => {
res.send({ playback: global.timer.state });
};
// Create controller for GET request to '/playback/onAir'
// Turns onAir flag to true
export const onAir = async (req, res) => {
global.timer.trigger('onAir') ? res.sendStatus(200) : res.sendStatus(400);
};
// Create controller for GET request to '/playback/onAir'
// Turns onAir flag to true
export const offAir = async (req, res) => {
global.timer.trigger('offAir') ? res.sendStatus(200) : res.sendStatus(400);
};
// Create controller for GET request to '/playback/start'
// Starts timer object
export const pbStart = async (req, res) => {
@@ -19,24 +19,6 @@ describe('When a GET request request is sent', () => {
});
describe('When a POST state change is sent', () => {
test('POST /playback/onAir returns 200', async () => {
await supertest(server)
.post('/playback/onAir')
.expect(200)
.then((response) => {
expect(response.text.includes('<!doctype html>')).toBe(false);
});
});
test('POST /playback/offAir returns 200', async () => {
await supertest(server)
.post('/playback/offAir')
.expect(200)
.then((response) => {
expect(response.text.includes('<!doctype html>')).toBe(false);
});
});
test('POST /playback/start returns 200', async () => {
await supertest(server)
.post('/playback/start')
+9 -17
View File
@@ -1,31 +1,23 @@
import express from 'express';
export const router = express.Router();
// import playback controllers
import {
pbGet,
onAir,
offAir,
pbStart,
pbPause,
pbStop,
pbRoll,
pbPrevious,
pbNext,
pbLoad,
pbUnload,
pbNext,
pbPause,
pbPrevious,
pbReload,
pbRoll,
pbStart,
pbStop,
pbUnload,
} from '../controllers/playbackController.js';
export const router = express.Router();
// create route between controller and '/playback/' endpoint
router.get('/', pbGet);
// create route between controller and '/playback/onAir' endpoint
router.post('/onAir', onAir);
// create route between controller and '/playback/offAir' endpoint
router.post('/offAir', offAir);
// create route between controller and '/playback/start' endpoint
router.post('/start', pbStart);
@@ -0,0 +1,38 @@
import { expect, test } from '@playwright/test';
test('test', async ({ context }) => {
const editorPage = await context.newPage();
const featurePage = await context.newPage();
await editorPage.goto('http://localhost:4001/messagecontrol');
// stage timer message
await editorPage.getByPlaceholder('Shown in stage timer').click();
await editorPage.getByPlaceholder('Shown in stage timer').fill('testing');
await editorPage.getByRole('button', { name: 'toggle timer screen message' }).click();
await featurePage.goto('http://localhost:4001/timer');
await featurePage.getByText('testing').click();
// public screen message
await editorPage.getByPlaceholder('Shown in public screens').click();
await editorPage.getByPlaceholder('Shown in public screens').fill('testing public');
await editorPage.getByRole('button', { name: /toggle public screen message/i }).click();
await featurePage.goto('http://localhost:4001/public');
await featurePage.getByText('testing public').click();
// lower third message
await editorPage.getByPlaceholder('Shown in lower third').click();
await editorPage.getByPlaceholder('Shown in lower third').fill('testing lower');
await editorPage.getByRole('button', { name: /toggle lower third message/i }).click();
await featurePage.goto('http://localhost:4001/lower');
await featurePage.getByText('testing lower').click();
// on air state
await editorPage.getByRole('button', { name: 'Toggle On Air' }).click();
await featurePage.goto('http://localhost:4001/studio');
await featurePage.getByText('ON AIR').click();
const onAirActive = await featurePage.locator('data-testid=on-air-enabled');
await expect(onAirActive).toHaveText(/on air/i);
});
+14
View File
@@ -0,0 +1,14 @@
import { expect, test } from '@playwright/test';
test.describe('minimal view behaviour can be changed through params', () => {
test.use({ viewport: { width: 1920, height: 1080 } });
test('without overloads', async ({ page }) => {
await page.goto('http://localhost:4001/minimal');
await page.getByTestId('minimal-timer').click();
});
test('hide nav', async ({ page }) => {
await page.goto('http://localhost:4001/minimal?hidenav=true');
const navBar = await page.locator('data-test-id=nav-logo');
await expect(navBar).toHaveCount(0);
});
});
+78
View File
@@ -0,0 +1,78 @@
import { expect, test } from '@playwright/test';
test.describe('pages routes are available', () => {
test.use({ viewport: { width: 1920, height: 1080 } });
test.describe('backstage views', () => {
test('editor', async ({ page }) => {
await page.goto('http://localhost:4001/editor');
await expect(page).toHaveTitle(/ontime/);
await page.getByTestId('event-editor').click();
await page.getByTestId('panel-event-list').click();
await page.getByTestId('panel-timer-control').click();
await page.getByTestId('panel-messages-control').click();
await page.getByTestId('panel-info').click();
});
test('cuesheet', async ({ page }) => {
await page.goto('http://localhost:4001/cuesheet');
await expect(page).toHaveTitle(/ontime/);
await page.getByTestId('cuesheet').click();
});
});
test.describe('detached views', () => {
test('rundown', async ({ page }) => {
await page.goto('http://localhost:4001/eventlist');
await page.getByTestId('panel-event-list').click();
});
test('timer control', async ({ page }) => {
await page.goto('http://localhost:4001/timercontrol');
await page.getByTestId('panel-timer-control').click();
});
test('message control', async ({ page }) => {
await page.goto('http://localhost:4001/messagecontrol');
await page.getByTestId('panel-messages-control').click();
});
test('info', async ({ page }) => {
await page.goto('http://localhost:4001/info');
await page.getByTestId('panel-info').click();
});
});
test.describe('client routes', () => {
test('stage timer', async ({ page }) => {
await page.goto('http://localhost:4001/timer');
await page.getByTestId('timer-view').click();
});
test('clock', async ({ page }) => {
await page.goto('http://localhost:4001/clock');
await page.getByTestId('clock-view').click();
});
test('minimal timer', async ({ page }) => {
await page.goto('http://localhost:4001/minimal');
await page.getByTestId('minimal-timer').click();
});
test('backstage', async ({ page }) => {
await page.goto('http://localhost:4001/backstage');
await page.getByTestId('backstage-view').click();
});
test('public', async ({ page }) => {
await page.goto('http://localhost:4001/public');
await page.getByTestId('public-view').click();
});
test('pip', async ({ page }) => {
await page.goto('http://localhost:4001/pip');
await page.getByTestId('pip-view').click();
});
test('studio clock', async ({ page }) => {
await page.goto('http://localhost:4001/studio');
await page.getByTestId('studio-view').click();
});
test('lower', async ({ page }) => {
await page.goto('http://localhost:4001/lower');
const errorBoundary = await page.locator('data-test-id=error-container');
await expect(errorBoundary).toHaveCount(0);
});
});
});
+43 -552
View File
File diff suppressed because it is too large Load Diff