mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 10:53:51 +00:00
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:
@@ -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'
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) => { ... })
|
||||
@@ -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
@@ -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}"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
@@ -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();
|
||||
@@ -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';
|
||||
@@ -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')
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user