Initial Upload
This commit is contained in:
2
CONTRIBUTING.md
Normal file
2
CONTRIBUTING.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Contributing
|
||||
Please refer to our parent repository's [CONTRIBUTING](https://github.com/xibosignage/xibo/blob/master/CONTRIBUTING.md).
|
||||
245
Dockerfile
Normal file
245
Dockerfile
Normal file
@@ -0,0 +1,245 @@
|
||||
# Multi-stage build
|
||||
# Stage 1
|
||||
# Run composer
|
||||
FROM composer as composer
|
||||
COPY ./composer.json /app
|
||||
COPY ./composer.lock /app
|
||||
|
||||
RUN composer install --no-interaction --no-dev --optimize-autoloader
|
||||
|
||||
# Tidy up
|
||||
# remove non-required vendor files
|
||||
WORKDIR /app/vendor
|
||||
RUN find -type d -name '.git' -exec rm -r {} + && \
|
||||
find -path ./twig/twig/lib/Twig -prune -type d -name 'Test' -exec rm -r {} + && \
|
||||
find -type d -name 'tests' -depth -exec rm -r {} + && \
|
||||
find -type d -name 'benchmarks' -depth -exec rm -r {} + && \
|
||||
find -type d -name 'smoketests' -depth -exec rm -r {} + && \
|
||||
find -type d -name 'demo' -depth -exec rm -r {} + && \
|
||||
find -type d -name 'doc' -depth -exec rm -r {} + && \
|
||||
find -type d -name 'docs' -depth -exec rm -r {} + && \
|
||||
find -type d -name 'examples' -depth -exec rm -r {} + && \
|
||||
find -type f -name 'phpunit.xml' -exec rm -r {} + && \
|
||||
find -type f -name '*.md' -exec rm -r {} +
|
||||
|
||||
|
||||
# Stage 2
|
||||
# Run webpack
|
||||
FROM node:22 AS webpack
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json and the webpack config file
|
||||
COPY webpack.config.js .
|
||||
COPY package.json .
|
||||
COPY package-lock.json .
|
||||
|
||||
# Install npm packages
|
||||
RUN npm install
|
||||
|
||||
# Copy ui folder
|
||||
COPY ./ui ./ui
|
||||
|
||||
# Copy modules source folder
|
||||
COPY ./modules/src ./modules/src
|
||||
COPY ./modules/vendor ./modules/vendor
|
||||
|
||||
# Build webpack
|
||||
RUN npm run publish
|
||||
|
||||
# Stage 3
|
||||
# Build the CMS container
|
||||
FROM debian:bullseye-slim
|
||||
MAINTAINER Xibo Signage <support@xibosignage.com>
|
||||
LABEL org.opencontainers.image.authors="support@xibosignage.com"
|
||||
|
||||
# Install apache, PHP, and supplimentary programs.
|
||||
RUN apt update && \
|
||||
apt install -y software-properties-common lsb-release ca-certificates curl && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime
|
||||
|
||||
# Add sury.org PHP Repository
|
||||
RUN curl -sSLo /usr/share/keyrings/deb.sury.org-php.gpg https://packages.sury.org/php/apt.gpg && \
|
||||
sh -c 'echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list'
|
||||
|
||||
RUN LC_ALL=C.UTF-8 DEBIAN_FRONTEND=noninteractive apt update && apt upgrade -y && apt install -y \
|
||||
tar \
|
||||
bash \
|
||||
curl \
|
||||
apache2 \
|
||||
libapache2-mod-xsendfile \
|
||||
netcat \
|
||||
iputils-ping \
|
||||
gnupg \
|
||||
php8.4 \
|
||||
libapache2-mod-php8.4 \
|
||||
php8.4-gd \
|
||||
php8.4-dom \
|
||||
php8.4-pdo \
|
||||
php8.4-zip \
|
||||
php8.4-mysql \
|
||||
php8.4-gettext \
|
||||
php8.4-soap \
|
||||
php8.4-iconv \
|
||||
php8.4-curl \
|
||||
php8.4-ctype \
|
||||
php8.4-fileinfo \
|
||||
php8.4-xml \
|
||||
php8.4-simplexml \
|
||||
php8.4-mbstring \
|
||||
php8.4-memcached \
|
||||
php8.4-phar \
|
||||
php8.4-opcache \
|
||||
php8.4-mongodb \
|
||||
php8.4-gnupg \
|
||||
tzdata \
|
||||
msmtp \
|
||||
openssl \
|
||||
cron \
|
||||
default-mysql-client \
|
||||
&& dpkg-reconfigure --frontend noninteractive tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN update-alternatives --set php /usr/bin/php8.4
|
||||
|
||||
# Enable Apache module
|
||||
RUN a2enmod rewrite \
|
||||
&& a2enmod headers \
|
||||
&& a2enmod proxy \
|
||||
&& a2enmod proxy_http \
|
||||
&& a2enmod proxy_wstunnel
|
||||
|
||||
# Add all necessary config files in one layer
|
||||
ADD docker/ /
|
||||
|
||||
# Update the PHP.ini file
|
||||
RUN sed -i "s/error_reporting = .*$/error_reporting = E_ERROR | E_WARNING | E_PARSE/" /etc/php/8.4/apache2/php.ini && \
|
||||
sed -i "s/session.gc_probability = .*$/session.gc_probability = 1/" /etc/php/8.4/apache2/php.ini && \
|
||||
sed -i "s/session.gc_divisor = .*$/session.gc_divisor = 100/" /etc/php/8.4/apache2/php.ini && \
|
||||
sed -i "s/allow_url_fopen = .*$/allow_url_fopen = Off/" /etc/php/8.4/apache2/php.ini && \
|
||||
sed -i "s/expose_php = .*$/expose_php = Off/" /etc/php/8.4/apache2/php.ini && \
|
||||
sed -i "s/error_reporting = .*$/error_reporting = E_ERROR | E_WARNING | E_PARSE/" /etc/php/8.4/cli/php.ini && \
|
||||
sed -i "s/session.gc_probability = .*$/session.gc_probability = 1/" /etc/php/8.4/cli/php.ini && \
|
||||
sed -i "s/session.gc_divisor = .*$/session.gc_divisor = 100/" /etc/php/8.4/cli/php.ini && \
|
||||
sed -i "s/allow_url_fopen = .*$/allow_url_fopen = Off/" /etc/php/8.4/cli/php.ini && \
|
||||
sed -i "s/expose_php = .*$/expose_php = Off/" /etc/php/8.4/cli/php.ini
|
||||
|
||||
# Capture the git commit for this build if we provide one
|
||||
ARG GIT_COMMIT=prod
|
||||
|
||||
# Setup persistent environment variables
|
||||
ENV CMS_DEV_MODE=false \
|
||||
INSTALL_TYPE=docker \
|
||||
XMR_HOST=xmr \
|
||||
CMS_SERVER_NAME=localhost \
|
||||
MYSQL_HOST=mysql \
|
||||
MYSQL_USER=cms \
|
||||
MYSQL_PASSWORD=none \
|
||||
MYSQL_PORT=3306 \
|
||||
MYSQL_DATABASE=cms \
|
||||
MYSQL_BACKUP_ENABLED=true \
|
||||
MYSQL_ATTR_SSL_CA=none \
|
||||
MYSQL_ATTR_SSL_VERIFY_SERVER_CERT=true \
|
||||
CMS_SMTP_SERVER=smtp.gmail.com:587 \
|
||||
CMS_SMTP_USERNAME=none \
|
||||
CMS_SMTP_PASSWORD=none \
|
||||
CMS_SMTP_USE_TLS=YES \
|
||||
CMS_SMTP_USE_STARTTLS=YES \
|
||||
CMS_SMTP_REWRITE_DOMAIN=gmail.com \
|
||||
CMS_SMTP_HOSTNAME=none \
|
||||
CMS_SMTP_FROM_LINE_OVERRIDE=YES \
|
||||
CMS_SMTP_FROM=none \
|
||||
CMS_ALIAS=none \
|
||||
CMS_PHP_SESSION_GC_MAXLIFETIME=1440 \
|
||||
CMS_PHP_POST_MAX_SIZE=2G \
|
||||
CMS_PHP_UPLOAD_MAX_FILESIZE=2G \
|
||||
CMS_PHP_MAX_EXECUTION_TIME=300 \
|
||||
CMS_PHP_MEMORY_LIMIT=256M \
|
||||
CMS_PHP_CLI_MAX_EXECUTION_TIME=0 \
|
||||
CMS_PHP_CLI_MEMORY_LIMIT=256M \
|
||||
CMS_PHP_COOKIE_SECURE=Off \
|
||||
CMS_PHP_COOKIE_HTTP_ONLY=On \
|
||||
CMS_PHP_COOKIE_SAMESITE=Lax \
|
||||
CMS_APACHE_START_SERVERS=2 \
|
||||
CMS_APACHE_MIN_SPARE_SERVERS=5 \
|
||||
CMS_APACHE_MAX_SPARE_SERVERS=10 \
|
||||
CMS_APACHE_MAX_REQUEST_WORKERS=60 \
|
||||
CMS_APACHE_MAX_CONNECTIONS_PER_CHILD=300 \
|
||||
CMS_APACHE_TIMEOUT=30 \
|
||||
CMS_APACHE_OPTIONS_INDEXES=false \
|
||||
CMS_QUICK_CHART_URL=http://cms-quickchart:3400 \
|
||||
CMS_APACHE_SERVER_TOKENS=OS \
|
||||
CMS_APACHE_LOG_REQUEST_TIME=false \
|
||||
CMS_USE_MEMCACHED=false \
|
||||
MEMCACHED_HOST=memcached \
|
||||
MEMCACHED_PORT=11211 \
|
||||
CMS_USAGE_REPORT=true \
|
||||
XTR_ENABLED=true \
|
||||
GIT_COMMIT=$GIT_COMMIT \
|
||||
GNUPGHOME=/var/www/.gnupg
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
# Map the source files into /var/www/cms
|
||||
RUN mkdir -p /var/www/cms
|
||||
|
||||
# Composer generated vendor files
|
||||
COPY --from=composer /app /var/www/cms
|
||||
|
||||
# Copy dist built webpack app folder to web
|
||||
COPY --from=webpack /app/web/dist /var/www/cms/web/dist
|
||||
|
||||
# Copy modules built webpack app folder to cms modules
|
||||
COPY --from=webpack /app/modules /var/www/cms/modules
|
||||
|
||||
# All other files (.dockerignore excludes many things, but we tidy up the rest below)
|
||||
COPY --chown=www-data:www-data . /var/www/cms
|
||||
|
||||
# OpenOOH specification
|
||||
RUN mkdir /var/www/cms/openooh \
|
||||
&& curl -o /var/www/cms/openooh/specification.json https://raw.githubusercontent.com/openooh/venue-taxonomy/main/specification.json
|
||||
|
||||
# Help Links
|
||||
RUN curl -o /var/www/cms/help-links.yaml https://raw.githubusercontent.com/xibosignage/xibo-manual/master/help-links.yaml || true
|
||||
|
||||
# Git commit fallback
|
||||
RUN echo $GIT_COMMIT > /var/www/cms/commit.sha
|
||||
|
||||
# Tidy up
|
||||
RUN rm /var/www/cms/composer.* && \
|
||||
rm -r /var/www/cms/docker && \
|
||||
rm -r /var/www/cms/tests && \
|
||||
rm /var/www/cms/.dockerignore && \
|
||||
rm /var/www/cms/phpunit.xml && \
|
||||
rm /var/www/cms/package.json && \
|
||||
rm /var/www/cms/package-lock.json && \
|
||||
rm /var/www/cms/cypress.config.js && \
|
||||
rm -r /var/www/cms/cypress && \
|
||||
rm -r /var/www/cms/ui && \
|
||||
rm /var/www/cms/webpack.config.js && \
|
||||
rm /var/www/cms/lib/routes-cypress.php
|
||||
|
||||
# Map a volumes to this folder.
|
||||
# Our CMS files, library, cache and backups will be in here.
|
||||
RUN mkdir -p /var/www/cms/library/temp && \
|
||||
mkdir -p /var/www/backup && \
|
||||
mkdir -p /var/www/cms/cache && \
|
||||
mkdir -p /var/www/cms/web/userscripts && \
|
||||
chown -R www-data:www-data /var/www/cms && \
|
||||
chmod +x /entrypoint.sh /usr/local/bin/httpd-foreground /usr/local/bin/wait-for-command.sh \
|
||||
/etc/periodic/15min/cms-db-backup && \
|
||||
mkdir -p /run/apache2 && \
|
||||
ln -sf /usr/bin/msmtp /usr/sbin/sendmail && \
|
||||
chmod 777 /tmp && \
|
||||
chown -R www-data:www-data /var/www/.gnupg
|
||||
|
||||
# Expose volume mount points
|
||||
VOLUME /var/www/cms/library
|
||||
VOLUME /var/www/cms/custom
|
||||
VOLUME /var/www/cms/web/theme/custom
|
||||
VOLUME /var/www/backup
|
||||
VOLUME /var/www/cms/web/userscripts
|
||||
VOLUME /var/www/cms/ca-certs
|
||||
|
||||
CMD ["/entrypoint.sh"]
|
||||
192
Dockerfile.ci
Normal file
192
Dockerfile.ci
Normal file
@@ -0,0 +1,192 @@
|
||||
# A production style container which has the source mapped in from PWD
|
||||
# Multi-stage build
|
||||
# Stage 1
|
||||
# Run composer
|
||||
FROM composer as composer
|
||||
COPY ./composer.json /app
|
||||
COPY ./composer.lock /app
|
||||
|
||||
RUN composer install --no-interaction
|
||||
|
||||
# Stage 2
|
||||
# Run webpack
|
||||
FROM node:22 AS webpack
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json and the webpack config file
|
||||
COPY webpack.config.js .
|
||||
COPY package.json .
|
||||
COPY package-lock.json .
|
||||
|
||||
# Install npm packages
|
||||
RUN npm install
|
||||
|
||||
# Copy ui folder
|
||||
COPY ./ui ./ui
|
||||
|
||||
# Copy modules source folder
|
||||
COPY ./modules/src ./modules/src
|
||||
COPY ./modules/vendor ./modules/vendor
|
||||
|
||||
# Build webpack
|
||||
RUN npm run publish
|
||||
|
||||
# Stage 1
|
||||
# Build the CMS container
|
||||
FROM debian:bullseye-slim
|
||||
MAINTAINER Xibo Signage <support@xibosignage.com>
|
||||
LABEL org.opencontainers.image.authors="support@xibosignage.com"
|
||||
|
||||
# Install apache, PHP, and supplimentary programs.
|
||||
RUN apt update && \
|
||||
apt install -y software-properties-common lsb-release ca-certificates curl && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime
|
||||
|
||||
# Add sury.org PHP Repository
|
||||
RUN curl -sSLo /usr/share/keyrings/deb.sury.org-php.gpg https://packages.sury.org/php/apt.gpg && \
|
||||
sh -c 'echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list'
|
||||
|
||||
RUN LC_ALL=C.UTF-8 DEBIAN_FRONTEND=noninteractive apt update && apt upgrade -y && apt install -y \
|
||||
tar \
|
||||
bash \
|
||||
curl \
|
||||
apache2 \
|
||||
libapache2-mod-xsendfile \
|
||||
netcat \
|
||||
iputils-ping \
|
||||
gnupg \
|
||||
php8.4 \
|
||||
libapache2-mod-php8.4 \
|
||||
php8.4-gd \
|
||||
php8.4-dom \
|
||||
php8.4-pdo \
|
||||
php8.4-zip \
|
||||
php8.4-mysql \
|
||||
php8.4-gettext \
|
||||
php8.4-soap \
|
||||
php8.4-iconv \
|
||||
php8.4-curl \
|
||||
php8.4-ctype \
|
||||
php8.4-fileinfo \
|
||||
php8.4-xml \
|
||||
php8.4-simplexml \
|
||||
php8.4-mbstring \
|
||||
php8.4-memcached \
|
||||
php8.4-phar \
|
||||
php8.4-opcache \
|
||||
php8.4-mongodb \
|
||||
php8.4-gnupg \
|
||||
tzdata \
|
||||
msmtp \
|
||||
openssl \
|
||||
cron \
|
||||
default-mysql-client \
|
||||
&& dpkg-reconfigure --frontend noninteractive tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN update-alternatives --set php /usr/bin/php8.4
|
||||
|
||||
# Enable Apache module
|
||||
RUN a2enmod rewrite \
|
||||
&& a2enmod headers \
|
||||
&& a2enmod proxy \
|
||||
&& a2enmod proxy_http \
|
||||
&& a2enmod proxy_wstunnel
|
||||
|
||||
# Add all necessary config files in one layer
|
||||
ADD docker/ /
|
||||
|
||||
# Adjust file permissions as appropriate
|
||||
RUN chmod +x /entrypoint.sh /usr/local/bin/httpd-foreground /usr/local/bin/wait-for-command.sh \
|
||||
/etc/periodic/15min/cms-db-backup && \
|
||||
chmod 777 /tmp && \
|
||||
chown -R www-data:www-data /var/www/.gnupg
|
||||
|
||||
# Update the PHP.ini file
|
||||
RUN sed -i "s/error_reporting = .*$/error_reporting = E_ERROR | E_WARNING | E_PARSE/" /etc/php/8.4/apache2/php.ini && \
|
||||
sed -i "s/session.gc_probability = .*$/session.gc_probability = 1/" /etc/php/8.4/apache2/php.ini && \
|
||||
sed -i "s/session.gc_divisor = .*$/session.gc_divisor = 100/" /etc/php/8.4/apache2/php.ini && \
|
||||
sed -i "s/allow_url_fopen = .*$/allow_url_fopen = Off/" /etc/php/8.4/apache2/php.ini && \
|
||||
sed -i "s/expose_php = .*$/expose_php = Off/" /etc/php/8.4/apache2/php.ini && \
|
||||
sed -i "s/error_reporting = .*$/error_reporting = E_ERROR | E_WARNING | E_PARSE/" /etc/php/8.4/cli/php.ini && \
|
||||
sed -i "s/session.gc_probability = .*$/session.gc_probability = 1/" /etc/php/8.4/cli/php.ini && \
|
||||
sed -i "s/session.gc_divisor = .*$/session.gc_divisor = 100/" /etc/php/8.4/cli/php.ini && \
|
||||
sed -i "s/allow_url_fopen = .*$/allow_url_fopen = Off/" /etc/php/8.4/cli/php.ini && \
|
||||
sed -i "s/expose_php = .*$/expose_php = Off/" /etc/php/8.4/cli/php.ini
|
||||
|
||||
# Capture the git commit for this build if we provide one
|
||||
ARG GIT_COMMIT=ci
|
||||
|
||||
# Set some environment variables
|
||||
ENV CMS_DEV_MODE=true \
|
||||
INSTALL_TYPE=ci \
|
||||
MYSQL_HOST=db \
|
||||
MYSQL_PORT=3306 \
|
||||
MYSQL_USER=root \
|
||||
MYSQL_PASSWORD=root \
|
||||
MYSQL_DATABASE=cms \
|
||||
MYSQL_BACKUP_ENABLED=false \
|
||||
MYSQL_ATTR_SSL_CA=none \
|
||||
MYSQL_ATTR_SSL_VERIFY_SERVER_CERT=true \
|
||||
CMS_SERVER_NAME=localhost \
|
||||
CMS_ALIAS=none \
|
||||
CMS_PHP_SESSION_GC_MAXLIFETIME=1440 \
|
||||
CMS_PHP_POST_MAX_SIZE=2G \
|
||||
CMS_PHP_UPLOAD_MAX_FILESIZE=2G \
|
||||
CMS_PHP_MAX_EXECUTION_TIME=300 \
|
||||
CMS_PHP_MEMORY_LIMIT=256M \
|
||||
CMS_PHP_CLI_MAX_EXECUTION_TIME=0 \
|
||||
CMS_PHP_CLI_MEMORY_LIMIT=256M \
|
||||
CMS_PHP_COOKIE_SECURE=Off \
|
||||
CMS_PHP_COOKIE_HTTP_ONLY=On \
|
||||
CMS_PHP_COOKIE_SAMESITE=Lax \
|
||||
CMS_APACHE_START_SERVERS=2 \
|
||||
CMS_APACHE_MIN_SPARE_SERVERS=5 \
|
||||
CMS_APACHE_MAX_SPARE_SERVERS=10 \
|
||||
CMS_APACHE_MAX_REQUEST_WORKERS=60 \
|
||||
CMS_APACHE_MAX_CONNECTIONS_PER_CHILD=300 \
|
||||
CMS_APACHE_TIMEOUT=30 \
|
||||
CMS_APACHE_OPTIONS_INDEXES=false \
|
||||
CMS_QUICK_CHART_URL=http://cms-quickchart:3400 \
|
||||
CMS_APACHE_SERVER_TOKENS=OS \
|
||||
CMS_USE_MEMCACHED=false \
|
||||
MEMCACHED_HOST=memcached \
|
||||
MEMCACHED_PORT=11211 \
|
||||
CMS_USAGE_REPORT=false \
|
||||
XTR_ENABLED=true \
|
||||
GIT_COMMIT=$GIT_COMMIT \
|
||||
GNUPGHOME=/var/www/.gnupg
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
# Map the source files into /var/www/cms
|
||||
# Create library and cache, because they might not exist
|
||||
# Create /var/www/backup so that we have somewhere for entrypoint to log errors.
|
||||
RUN mkdir -p /var/www/cms && \
|
||||
mkdir -p /var/www/cms/library/temp && \
|
||||
mkdir -p /var/www/cms/cache && \
|
||||
mkdir -p /var/www/backup
|
||||
|
||||
# Composer generated vendor files
|
||||
COPY --from=composer /app /var/www/cms
|
||||
|
||||
# Copy dist built webpack app folder to web
|
||||
COPY --from=webpack /app/web/dist /var/www/cms/web/dist
|
||||
|
||||
# Copy modules built webpack app folder to cms modules
|
||||
COPY --from=webpack /app/modules /var/www/cms/modules
|
||||
|
||||
# All other files (.dockerignore excludes things we don't want)
|
||||
COPY --chown=www-data:www-data . /var/www/cms
|
||||
|
||||
# OpenOOH specification
|
||||
RUN mkdir /var/www/cms/openooh \
|
||||
&& curl -o /var/www/cms/openooh/specification.json https://raw.githubusercontent.com/openooh/venue-taxonomy/main/specification.json
|
||||
|
||||
# Help Links
|
||||
RUN curl -o help_links.yaml https://raw.githubusercontent.com/xibosignage/xibo-manual/develop/help_links.yaml || true
|
||||
|
||||
# Run entry
|
||||
CMD ["/entrypoint.sh"]
|
||||
16
Dockerfile.cypress
Normal file
16
Dockerfile.cypress
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM cypress/base:12
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm install har-validator
|
||||
RUN npm install --save-dev --slient cypress@10.1.0
|
||||
|
||||
RUN $(npm bin)/cypress verify
|
||||
|
||||
RUN mkdir /app/results
|
||||
|
||||
# Create this file so that we can volume mount it
|
||||
RUN touch /app/cypress.config.js
|
||||
|
||||
# Create this folder so that we can volume mount it
|
||||
RUN mkdir /app/cypress
|
||||
142
Dockerfile.dev
Normal file
142
Dockerfile.dev
Normal file
@@ -0,0 +1,142 @@
|
||||
# Xibo Development Container
|
||||
# This is the dev container and doesn't contain CRON or composer it maps the PWD straight through
|
||||
# Stage 1
|
||||
# Build the CMS container
|
||||
FROM debian:bullseye-slim
|
||||
LABEL org.opencontainers.image.authors="support@xibosignage.com"
|
||||
|
||||
RUN apt update && \
|
||||
apt install -y software-properties-common lsb-release ca-certificates curl && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime
|
||||
|
||||
# Add sury.org PHP Repository
|
||||
RUN curl -sSLo /usr/share/keyrings/deb.sury.org-php.gpg https://packages.sury.org/php/apt.gpg && \
|
||||
sh -c 'echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list'
|
||||
|
||||
RUN LC_ALL=C.UTF-8 DEBIAN_FRONTEND=noninteractive apt update && apt upgrade -y && apt install -y \
|
||||
tar \
|
||||
bash \
|
||||
curl \
|
||||
apache2 \
|
||||
libapache2-mod-xsendfile \
|
||||
netcat \
|
||||
iputils-ping \
|
||||
gnupg \
|
||||
php8.4 \
|
||||
libapache2-mod-php8.4 \
|
||||
php8.4-gd \
|
||||
php8.4-dom \
|
||||
php8.4-pdo \
|
||||
php8.4-zip \
|
||||
php8.4-mysql \
|
||||
php8.4-gettext \
|
||||
php8.4-soap \
|
||||
php8.4-iconv \
|
||||
php8.4-curl \
|
||||
php8.4-ctype \
|
||||
php8.4-fileinfo \
|
||||
php8.4-xml \
|
||||
php8.4-simplexml \
|
||||
php8.4-mbstring \
|
||||
php8.4-memcached \
|
||||
php8.4-phar \
|
||||
php8.4-opcache \
|
||||
php8.4-mongodb \
|
||||
php8.4-gnupg \
|
||||
tzdata \
|
||||
msmtp \
|
||||
openssl \
|
||||
cron \
|
||||
default-mysql-client \
|
||||
&& dpkg-reconfigure --frontend noninteractive tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN update-alternatives --set php /usr/bin/php8.4
|
||||
|
||||
# Enable Apache module
|
||||
RUN a2enmod rewrite \
|
||||
&& a2enmod headers \
|
||||
&& a2enmod proxy \
|
||||
&& a2enmod proxy_http \
|
||||
&& a2enmod proxy_wstunnel
|
||||
|
||||
# Add all necessary config files in one layer
|
||||
ADD docker/ /
|
||||
|
||||
# Adjust file permissions as appropriate
|
||||
RUN chmod +x /entrypoint.sh /usr/local/bin/httpd-foreground /usr/local/bin/wait-for-command.sh \
|
||||
/etc/periodic/15min/cms-db-backup && \
|
||||
chmod 777 /tmp && \
|
||||
chown -R www-data:www-data /var/www/.gnupg
|
||||
|
||||
# Update the PHP.ini file
|
||||
RUN sed -i "s/error_reporting = .*$/error_reporting = E_ERROR | E_WARNING | E_PARSE/" /etc/php/8.4/apache2/php.ini && \
|
||||
sed -i "s/session.gc_probability = .*$/session.gc_probability = 1/" /etc/php/8.4/apache2/php.ini && \
|
||||
sed -i "s/session.gc_divisor = .*$/session.gc_divisor = 100/" /etc/php/8.4/apache2/php.ini && \
|
||||
sed -i "s/allow_url_fopen = .*$/allow_url_fopen = Off/" /etc/php/8.4/apache2/php.ini && \
|
||||
sed -i "s/expose_php = .*$/expose_php = Off/" /etc/php/8.4/apache2/php.ini && \
|
||||
sed -i "s/error_reporting = .*$/error_reporting = E_ERROR | E_WARNING | E_PARSE/" /etc/php/8.4/cli/php.ini && \
|
||||
sed -i "s/session.gc_probability = .*$/session.gc_probability = 1/" /etc/php/8.4/cli/php.ini && \
|
||||
sed -i "s/session.gc_divisor = .*$/session.gc_divisor = 100/" /etc/php/8.4/cli/php.ini && \
|
||||
sed -i "s/allow_url_fopen = .*$/allow_url_fopen = Off/" /etc/php/8.4/cli/php.ini && \
|
||||
sed -i "s/expose_php = .*$/expose_php = Off/" /etc/php/8.4/cli/php.ini
|
||||
|
||||
# Capture the git commit for this build if we provide one
|
||||
ARG GIT_COMMIT=dev
|
||||
|
||||
# Set some environment variables
|
||||
ENV CMS_DEV_MODE=true \
|
||||
INSTALL_TYPE=dev \
|
||||
XMR_HOST=xmr \
|
||||
MYSQL_HOST=db \
|
||||
MYSQL_PORT=3306 \
|
||||
MYSQL_USER=root \
|
||||
MYSQL_PASSWORD=root \
|
||||
MYSQL_DATABASE=cms \
|
||||
MYSQL_BACKUP_ENABLED=false \
|
||||
MYSQL_ATTR_SSL_CA=none \
|
||||
MYSQL_ATTR_SSL_VERIFY_SERVER_CERT=true \
|
||||
CMS_SERVER_NAME=localhost \
|
||||
CMS_ALIAS=none \
|
||||
CMS_PHP_SESSION_GC_MAXLIFETIME=1440 \
|
||||
CMS_PHP_POST_MAX_SIZE=2G \
|
||||
CMS_PHP_UPLOAD_MAX_FILESIZE=2G \
|
||||
CMS_PHP_MAX_EXECUTION_TIME=300 \
|
||||
CMS_PHP_MEMORY_LIMIT=256M \
|
||||
CMS_PHP_CLI_MAX_EXECUTION_TIME=0 \
|
||||
CMS_PHP_CLI_MEMORY_LIMIT=256M \
|
||||
CMS_PHP_COOKIE_SECURE=Off \
|
||||
CMS_PHP_COOKIE_HTTP_ONLY=On \
|
||||
CMS_PHP_COOKIE_SAMESITE=Lax \
|
||||
CMS_APACHE_START_SERVERS=2 \
|
||||
CMS_APACHE_MIN_SPARE_SERVERS=5 \
|
||||
CMS_APACHE_MAX_SPARE_SERVERS=10 \
|
||||
CMS_APACHE_MAX_REQUEST_WORKERS=60 \
|
||||
CMS_APACHE_MAX_CONNECTIONS_PER_CHILD=300 \
|
||||
CMS_APACHE_TIMEOUT=30 \
|
||||
CMS_APACHE_OPTIONS_INDEXES=false \
|
||||
CMS_QUICK_CHART_URL=http://quickchart:3400 \
|
||||
CMS_APACHE_SERVER_TOKENS=OS \
|
||||
CMS_APACHE_LOG_REQUEST_TIME=true \
|
||||
CMS_USE_MEMCACHED=true \
|
||||
MEMCACHED_HOST=memcached \
|
||||
MEMCACHED_PORT=11211 \
|
||||
CMS_USAGE_REPORT=true \
|
||||
XTR_ENABLED=false \
|
||||
GIT_COMMIT=$GIT_COMMIT \
|
||||
GNUPGHOME=/var/www/.gnupg
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
# Map the source files into /var/www/cms
|
||||
# Create library and cache, because they might not exist
|
||||
# Create /var/www/backup so that we have somewhere for entrypoint to log errors.
|
||||
RUN mkdir -p /var/www/cms && \
|
||||
mkdir -p /var/www/cms/library/temp && \
|
||||
mkdir -p /var/www/cms/cache && \
|
||||
mkdir -p /var/www/backup
|
||||
|
||||
# Run entry
|
||||
CMD ["/entrypoint.sh"]
|
||||
662
LICENSE
Normal file
662
LICENSE
Normal file
@@ -0,0 +1,662 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
97
README.md
Normal file
97
README.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Cloud-CMS
|
||||
|
||||
Cloud-CMS is a powerful **Open Source Digital Signage platform** with a web-based content management system and display player software. It enables you to manage and deliver dynamic content across multiple screens from a centralized interface.
|
||||
|
||||
We offer commercial player options for Android, LG webOS, and Samsung Tizen, as well as CMS hosting and support services.
|
||||
|
||||
For more information about the original project, visit [Xibo Signage](https://xibosignage.com).
|
||||
|
||||
---
|
||||
|
||||
## About Cloud-CMS
|
||||
|
||||
Cloud-CMS is based on the Xibo CMS project and provides everything you need to run a digital signage network or a single screen. Our goal is to make digital signage accessible, flexible, and easy to manage.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- Web-based content management
|
||||
- Layout and playlist scheduling
|
||||
- Support for multiple display types and players
|
||||
- API for integration with third-party systems
|
||||
- Docker-based deployment for easy setup
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Quick Installation Guide
|
||||
|
||||
We recommend installing an official release via **Docker**.
|
||||
Follow these steps to quickly install **Cloud-CMS** using Docker:
|
||||
|
||||
1. **Install Docker and Docker Compose**
|
||||
- Download and install Docker from [https://docs.docker.com/get-docker/](https://docs.docker.com/get-docker/).
|
||||
|
||||
2. **Create a Project Directory**
|
||||
```bash
|
||||
mkdir cloud-cms
|
||||
cd cloud-cms
|
||||
```
|
||||
|
||||
3. **Download the Docker Compose File**
|
||||
- Obtain the official `docker-compose.yml`
|
||||
|
||||
4. **Configure Environment Variables**
|
||||
- Create a `.env` file and set values for:
|
||||
- `MYSQL_PASSWORD`
|
||||
- `CMS_DB_NAME`
|
||||
- `CMS_DB_USER`
|
||||
- `CMS_DB_PASSWORD`
|
||||
|
||||
5. **Start the Containers**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
6. **Access Cloud-CMS**
|
||||
- Open your browser and navigate to `http://localhost` (or your server IP).
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
Please only install a development environment if you intend to make code changes to Cloud-CMS. Installing from the repository is not suitable for production installations.
|
||||
|
||||
Cloud-CMS uses Docker to ensure all contributors have a repeatable development environment that is easy to set up.
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
Cloud-CMS is supported by **Oribi Technology Services**.
|
||||
For assistance, please use one of the following options:
|
||||
|
||||
- **Submit a ticket:** [https://portal.oribi-tech.com/new-ticket](https://portal.oribi-tech.com/new-ticket)
|
||||
- **Email:** [support@oribi-tech.com](mailto:support@oribi-tech.com)
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
Copyright (C) 2006-2025 Xibo Signage Ltd and Contributors.
|
||||
|
||||
Cloud-CMS (based on Xibo CMS) is free software: you can redistribute it and/or modify it under the terms of the **GNU Affero General Public License** as published by the Free Software Foundation, either version 3 of the License, or any later version.
|
||||
|
||||
Cloud-CMS is distributed in the hope that it will be useful, but **WITHOUT ANY WARRANTY**; without even the implied warranty of **MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE**. See the GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along with Cloud-CMS. If not, see [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/).
|
||||
|
||||
---
|
||||
|
||||
## Attribution
|
||||
|
||||
Cloud-CMS is a white-label version of the Xibo CMS project. Original source code and documentation are available at [Xibo CMS GitHub Repository](https://github.com/xibosignage/xibo-cms).
|
||||
13
SECURITY.md
Normal file
13
SECURITY.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
Xibo supports and maintains the current and prior major releases, with updates being provided to the latest minor
|
||||
release within that.
|
||||
|
||||
A full list of support is available on our website: https://xibosignage.com/docs/setup/supported-versions-and-environments
|
||||
|
||||
## Reporting a Vulnerability
|
||||
Please report (suspected) security vulnerabilities using the Security tab in this repository.
|
||||
|
||||
You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as
|
||||
possible depending on complexity but historically within a few days.
|
||||
223
bin/locale.php
Normal file
223
bin/locale.php
Normal file
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2024 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is a simple script to load all twig files recursively so that we have a complete set of twig files in the
|
||||
* /cache folder
|
||||
* we can then reliably run xgettext over them to update our POT file
|
||||
*/
|
||||
|
||||
use Slim\Flash\Messages;
|
||||
use Slim\Views\Twig;
|
||||
use Twig\TwigFilter;
|
||||
use Xibo\Service\ConfigService;
|
||||
use Xibo\Twig\ByteFormatterTwigExtension;
|
||||
use Xibo\Twig\DateFormatTwigExtension;
|
||||
use Xibo\Twig\TransExtension;
|
||||
use Xibo\Twig\TwigMessages;
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
define('PROJECT_ROOT', realpath(__DIR__ . '/..'));
|
||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||
|
||||
$view = Twig::create([
|
||||
PROJECT_ROOT . '/views',
|
||||
PROJECT_ROOT . '/modules',
|
||||
PROJECT_ROOT . '/reports'
|
||||
], [
|
||||
'cache' => PROJECT_ROOT . '/cache'
|
||||
]);
|
||||
$view->addExtension(new TransExtension());
|
||||
$view->addExtension(new ByteFormatterTwigExtension());
|
||||
$view->addExtension(new DateFormatTwigExtension());
|
||||
$view->getEnvironment()->addFilter(new TwigFilter('url_decode', 'urldecode'));
|
||||
|
||||
// Trick the flash middleware
|
||||
$storage = [];
|
||||
$view->addExtension(new TwigMessages(new Messages($storage)));
|
||||
|
||||
foreach (glob(PROJECT_ROOT . '/views/*.twig') as $file) {
|
||||
$view->getEnvironment()->load(str_replace(PROJECT_ROOT . '/views/', '', $file));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock PDO Storage Service which returns an empty array when select queried.
|
||||
*/
|
||||
class MockPdoStorageServiceForModuleFactory extends \Xibo\Storage\PdoStorageService
|
||||
{
|
||||
public function select($sql, $params, $connection = 'default', $reconnect = false, $close = false)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Mock Config Service
|
||||
class MockConfigService extends ConfigService
|
||||
{
|
||||
public function getSetting($setting, $default = null, $full = false)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Translator function
|
||||
function __($original)
|
||||
{
|
||||
return $original;
|
||||
}
|
||||
|
||||
// Stash
|
||||
$pool = new \Stash\Pool();
|
||||
|
||||
// Create a new Sanitizer service
|
||||
$sanitizerService = new \Xibo\Helper\SanitizerService();
|
||||
|
||||
// Create a new base dependency service
|
||||
$baseDependencyService = new \Xibo\Service\BaseDependenciesService();
|
||||
$baseDependencyService->setConfig(new MockConfigService());
|
||||
$baseDependencyService->setStore(new MockPdoStorageServiceForModuleFactory());
|
||||
$baseDependencyService->setSanitizer($sanitizerService);
|
||||
|
||||
$moduleFactory = new \Xibo\Factory\ModuleFactory(
|
||||
'',
|
||||
$pool,
|
||||
$view,
|
||||
new MockConfigService(),
|
||||
);
|
||||
$moduleFactory->useBaseDependenciesService($baseDependencyService);
|
||||
// Get all module
|
||||
$modules = $moduleFactory->getAll();
|
||||
|
||||
$moduleTemplateFactory = new \Xibo\Factory\ModuleTemplateFactory(
|
||||
$pool,
|
||||
$view,
|
||||
);
|
||||
$moduleTemplateFactory->useBaseDependenciesService($baseDependencyService);
|
||||
// Get all module templates
|
||||
$moduleTemplates = $moduleTemplateFactory->getAll(null, false);
|
||||
|
||||
// --------------
|
||||
// Create translation file
|
||||
// Each line contains title or description or properties of the module/templates
|
||||
$file = PROJECT_ROOT. '/locale/moduletranslate.php';
|
||||
$content = '<?php' . PHP_EOL;
|
||||
|
||||
$content .= '// Module translation' . PHP_EOL;
|
||||
// Module translation
|
||||
foreach ($modules as $module) {
|
||||
$content .= 'echo __(\''.$module->name.'\');' . PHP_EOL;
|
||||
$content .= 'echo __(\''.$module->description.'\');' . PHP_EOL;
|
||||
|
||||
// Settings Translation
|
||||
foreach ($module->settings as $setting) {
|
||||
if (!empty($setting->title)) {
|
||||
$content .= 'echo __(\''.$setting->title.'\');' . PHP_EOL;
|
||||
}
|
||||
if (!empty($setting->helpText)) {
|
||||
// replaces any single quote within the value with a backslash followed by a single quote
|
||||
$helpText = addslashes(trim($setting->helpText));
|
||||
$content .= 'echo __(\''.$helpText.'\');' . PHP_EOL;
|
||||
}
|
||||
|
||||
if (isset($setting->options) > 0) {
|
||||
foreach ($setting->options as $option) {
|
||||
if (!empty($option->title)) {
|
||||
$content .= 'echo __(\''.$option->title.'\');' . PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Properties translation
|
||||
foreach ($module->properties as $property) {
|
||||
if (!empty($property->title)) {
|
||||
$content .= 'echo __(\''.addslashes(trim($property->title)).'\');' . PHP_EOL;
|
||||
}
|
||||
if (!empty($property->helpText)) {
|
||||
// replaces any single quote within the value with a backslash followed by a single quote
|
||||
$helpText = addslashes($property->helpText);
|
||||
$content .= 'echo __(\''.$helpText.'\');' . PHP_EOL;
|
||||
}
|
||||
|
||||
if (isset($property->validation) > 0) {
|
||||
$tests = $property->validation->tests;
|
||||
foreach ($tests as $test) {
|
||||
// Property rule test message
|
||||
$message = $test->message;
|
||||
if (!empty($message)) {
|
||||
$content .= 'echo __(\''.addslashes(trim($message)).'\');' . PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($property->options) > 0) {
|
||||
foreach ($property->options as $option) {
|
||||
if (!empty($option->title)) {
|
||||
$content .= 'echo __(\''.$option->title.'\');' . PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$content .= '// Module Template translation' . PHP_EOL;
|
||||
// Template Translation
|
||||
foreach ($moduleTemplates as $moduleTemplate) {
|
||||
$content .= 'echo __(\''.$moduleTemplate->title.'\');' . PHP_EOL;
|
||||
|
||||
// Properties Translation
|
||||
foreach ($moduleTemplate->properties as $property) {
|
||||
if (!empty($property->title)) {
|
||||
$content .= 'echo __(\''.addslashes(trim($property->title)).'\');' . PHP_EOL;
|
||||
}
|
||||
if (!empty($property->helpText)) {
|
||||
// replaces any single quote within the value with a backslash followed by a single quote
|
||||
$helpText = addslashes(trim($property->helpText));
|
||||
$content .= 'echo __(\''.$helpText.'\');' . PHP_EOL;
|
||||
}
|
||||
|
||||
if (isset($property->validation) > 0) {
|
||||
$tests = $property->validation->tests;
|
||||
foreach ($tests as $test) {
|
||||
// Property rule test message
|
||||
$message = $test->message;
|
||||
if (!empty($message)) {
|
||||
$content .= 'echo __(\''.$message.'\');' . PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($property->options) > 0) {
|
||||
foreach ($property->options as $option) {
|
||||
if (!empty($option->title)) {
|
||||
$content .= 'echo __(\''.$option->title.'\');' . PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents($file, $content);
|
||||
echo 'moduletranslate.file created and data written successfully.';
|
||||
118
bin/run.php
Normal file
118
bin/run.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use Monolog\Logger;
|
||||
use Monolog\Processor\UidProcessor;
|
||||
use Nyholm\Psr7\ServerRequest;
|
||||
use Slim\Http\ServerRequest as Request;
|
||||
use Slim\Views\TwigMiddleware;
|
||||
use Xibo\Factory\ContainerFactory;
|
||||
|
||||
define('XIBO', true);
|
||||
define('PROJECT_ROOT', realpath(__DIR__ . '/..'));
|
||||
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
require PROJECT_ROOT . '/vendor/autoload.php';
|
||||
|
||||
if (!file_exists(PROJECT_ROOT . '/web/settings.php')) {
|
||||
die('Not configured');
|
||||
}
|
||||
|
||||
// convert all the command line arguments into a URL
|
||||
$argv = $GLOBALS['argv'];
|
||||
array_shift($GLOBALS['argv']);
|
||||
$pathInfo = '/' . implode('/', $argv);
|
||||
|
||||
// Create the container for dependency injection.
|
||||
try {
|
||||
$container = ContainerFactory::create();
|
||||
} catch (Exception $e) {
|
||||
die($e->getMessage());
|
||||
}
|
||||
$container->set('name', 'xtr');
|
||||
|
||||
$container->set('logger', function () {
|
||||
$logger = new Logger('CONSOLE');
|
||||
|
||||
$uidProcessor = new UidProcessor();
|
||||
// db
|
||||
$dbhandler = new \Xibo\Helper\DatabaseLogHandler();
|
||||
|
||||
$logger->pushProcessor($uidProcessor);
|
||||
$logger->pushHandler($dbhandler);
|
||||
|
||||
// Optionally allow console logging
|
||||
if (isset($_SERVER['LOG_TO_CONSOLE']) && $_SERVER['LOG_TO_CONSOLE']) {
|
||||
$logger->pushHandler(new \Monolog\Handler\StreamHandler(STDERR, Logger::DEBUG));
|
||||
}
|
||||
|
||||
return $logger;
|
||||
});
|
||||
|
||||
$app = \DI\Bridge\Slim\Bridge::create($container);
|
||||
|
||||
$app->setBasePath($container->get('basePath'));
|
||||
// Config
|
||||
$app->configService = $container->get('configService');
|
||||
|
||||
// Check for upgrade after we've loaded settings to make sure the main app gets any custom settings it needs.
|
||||
if (\Xibo\Helper\Environment::migrationPending()) {
|
||||
die('Upgrade pending');
|
||||
}
|
||||
|
||||
// Handle additional Middleware
|
||||
\Xibo\Middleware\State::setMiddleWare($app);
|
||||
|
||||
$twigMiddleware = TwigMiddleware::createFromContainer($app);
|
||||
|
||||
$app->add(new \Xibo\Middleware\ConnectorMiddleware($app));
|
||||
$app->add(new \Xibo\Middleware\ListenersMiddleware($app));
|
||||
$app->add(new \Xibo\Middleware\Storage($app));
|
||||
$app->add(new \Xibo\Middleware\Xtr($app));
|
||||
$app->add(new \Xibo\Middleware\State($app));
|
||||
$app->add($twigMiddleware);
|
||||
$app->add(new \Xibo\Middleware\Log($app));
|
||||
$app->add(new \Xibo\Middleware\Xmr($app));
|
||||
|
||||
$app->addRoutingMiddleware();
|
||||
$errorMiddleware = $app->addErrorMiddleware(
|
||||
\Xibo\Helper\Environment::isDevMode() || \Xibo\Helper\Environment::isForceDebugging(),
|
||||
true,
|
||||
true
|
||||
);
|
||||
$errorMiddleware->setDefaultErrorHandler(\Xibo\Middleware\Handlers::jsonErrorHandler($container));
|
||||
|
||||
// All routes
|
||||
$app->get('/', ['\Xibo\Controller\Task','poll']);
|
||||
$app->get('/{id}', ['\Xibo\Controller\Task','run']);
|
||||
|
||||
// if we passed taskId in console
|
||||
if (!empty($argv)) {
|
||||
$request = new Request(new ServerRequest('GET', $pathInfo));
|
||||
return $app->handle($request);
|
||||
}
|
||||
|
||||
// Run app
|
||||
$app->run();
|
||||
|
||||
9
bin/xtr.php
Normal file
9
bin/xtr.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
/*
|
||||
* Spring Signage Ltd - http://www.springsignage.com
|
||||
* Copyright (C) 2016 Spring Signage Ltd
|
||||
* (xtr.php)
|
||||
*
|
||||
* This is just a wrapper for run.php
|
||||
*/
|
||||
require('run.php');
|
||||
8
build-composer.sh
Normal file
8
build-composer.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# A simple helper script to run composer
|
||||
# useful if your dev host environment doesn't have PHP
|
||||
# on windows replace $PWD with your working repo root folder
|
||||
docker run --rm \
|
||||
--volume $PWD:/app \
|
||||
composer install --ignore-platform-reqs --optimize-autoloader
|
||||
38
build.sh
Executable file
38
build.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
VERSION="latest"
|
||||
while getopts v: option
|
||||
do
|
||||
case "${option}"
|
||||
in
|
||||
v) VERSION=${OPTARG};;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Building an archive for $VERSION"
|
||||
|
||||
|
||||
# Helper script to extract a release archive from a docker container (after building or pulling it).
|
||||
docker pull xibosignage/xibo-cms:"$VERSION"
|
||||
|
||||
echo "Pulled container"
|
||||
|
||||
CONTAINER=$(docker create xibosignage/xibo-cms:"$VERSION")
|
||||
|
||||
echo "Created container $CONTAINER"
|
||||
|
||||
docker cp "$CONTAINER":/var/www/cms/ "$VERSION"
|
||||
|
||||
echo "Copied out CMS /var/www/cms"
|
||||
|
||||
tar -czf "$VERSION".tar.gz "$VERSION"
|
||||
|
||||
echo "Tarred"
|
||||
|
||||
zip -rq "$VERSION".zip "$VERSION"
|
||||
|
||||
echo "Zipped"
|
||||
|
||||
docker rm "$CONTAINER"
|
||||
|
||||
echo "Container Removed"
|
||||
echo "Please remove $VERSION folder"
|
||||
131
composer.json
Normal file
131
composer.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"name": "xibosignage/xibo-cms",
|
||||
"description": "Xibo Open Source Digital Signage",
|
||||
"keywords": [
|
||||
"xibo",
|
||||
"digital signage",
|
||||
"spring signage"
|
||||
],
|
||||
"homepage": "https://xibosignage.com",
|
||||
"license": "AGPL-3.0",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Xibo Signage Ltd",
|
||||
"homepage": "https://xibosignage.com"
|
||||
},
|
||||
{
|
||||
"name": "Xibo Contributors",
|
||||
"homepage": "https://github.com/xibosignage/xibo/tree/master/contributors"
|
||||
}
|
||||
],
|
||||
"repositories": [{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/dasgarner/picofeed"
|
||||
}],
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.1",
|
||||
"ext-gd": "1",
|
||||
"ext-dom": "1",
|
||||
"ext-gnupg": "1",
|
||||
"ext-json": "1",
|
||||
"ext-mongodb": "2.0.0",
|
||||
"ext-pdo": "1",
|
||||
"ext-soap": "1",
|
||||
"ext-sockets": "1",
|
||||
"ext-zip": "1"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"ext-ctype": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-filter": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-gnupg": "*",
|
||||
"ext-json": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-mongodb": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-session": "*",
|
||||
"ext-soap": "*",
|
||||
"ext-sockets": "*",
|
||||
"ext-simplexml": "*",
|
||||
"ext-zip": "*",
|
||||
"akrabat/ip-address-middleware" : "2.6.*",
|
||||
"apereo/phpcas": "dev-master#6ef19a1e636303bda2f05f46b08035d53a8b602d",
|
||||
"dragonmantank/cron-expression": "^3.4",
|
||||
"erusev/parsedown": "dev-master#26cfde9dbffa43a77bfd45c87b0394df0dfcba85",
|
||||
"flynsarmy/slim-monolog": "~1.0",
|
||||
"gettext/gettext": "~4.0",
|
||||
"guzzlehttp/guzzle": "7.9.*",
|
||||
"guzzlehttp/psr7": "2.7.*",
|
||||
"illuminate/support": "v10.48.*",
|
||||
"infostars/picofeed": "dev-westphal/php8",
|
||||
"intervention/image": "2.7.2",
|
||||
"james-heinrich/getid3": "1.9.*",
|
||||
"jmikola/geojson": "^1.0",
|
||||
"johngrogg/ics-parser": "^3.1",
|
||||
"league/oauth2-server": "^8.0",
|
||||
"lcobucci/clock": "3.0.*",
|
||||
"lcobucci/jwt": "5.3.*",
|
||||
"mjaschen/phpgeo": "^5.0",
|
||||
"mongodb/mongodb": "2.0.*",
|
||||
"monolog/monolog": "2.10.*",
|
||||
"mpdf/mpdf": "v8.1.*",
|
||||
"nesbot/carbon": "^2.73.0",
|
||||
"nyholm/psr7": "^1.8",
|
||||
"onelogin/php-saml": "4.1.*",
|
||||
"phenx/php-font-lib": "^0.5.0",
|
||||
"php-di/php-di": "7.0.*",
|
||||
"php-di/slim-bridge": "3.4.*",
|
||||
"phpmailer/phpmailer": "6.5.*",
|
||||
"psr/container": "2.0.*",
|
||||
"psr/http-message": "1.1.*",
|
||||
"psr/http-server-handler": "1.0.*",
|
||||
"psr/http-server-middleware": "1.0.*",
|
||||
"psr/log": "1.1.*",
|
||||
"ralouphie/mimey": "^1.0",
|
||||
"respect/validation": "2.2.*",
|
||||
"robmorgan/phinx": "0.13.*",
|
||||
"robthree/twofactorauth": "1.8.2",
|
||||
"slim/flash": "^0.4",
|
||||
"slim/http": "^1.2",
|
||||
"slim/slim": "^4.14",
|
||||
"slim/twig-view": "3.3.0",
|
||||
"symfony/event-dispatcher": "^4.1",
|
||||
"symfony/event-dispatcher-contracts": "^1.10",
|
||||
"symfony/filesystem": "v6.4.*",
|
||||
"symfony/html-sanitizer": "v6.4.*",
|
||||
"symfony/yaml": "5.4.*",
|
||||
"tedivm/stash": "v1.2.*",
|
||||
"twig/twig": "3.11.*",
|
||||
"xibosignage/support": "dev-php8"
|
||||
},
|
||||
"require-dev": {
|
||||
"exussum12/coverage-checker": "^0.11.2",
|
||||
"doctrine/instantiator": "1.5.0",
|
||||
"doctrine/annotations": "^1",
|
||||
"league/oauth2-client": "^2.4",
|
||||
"micheh/phpcs-gitlab": "^1.1",
|
||||
"phpunit/phpunit": "10.5.*",
|
||||
"shipmonk/composer-dependency-analyser": "^1.8",
|
||||
"squizlabs/php_codesniffer": "3.*",
|
||||
"xibosignage/oauth2-xibo-cms": "dev-feature/3.0",
|
||||
"zircote/swagger-php": "^2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Xibo\\" : "lib/", "Xibo\\Custom\\": "custom/" }
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Xibo\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"phpcs": "phpcs --standard=vendor/xibosignage/support/src/Standards/xibo_ruleset.xml"
|
||||
}
|
||||
}
|
||||
9210
composer.lock
generated
Normal file
9210
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
custom/README.md
Normal file
30
custom/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Custom Modules
|
||||
This folder is provided as a reasonable place to copy/develop custom modules. This folder is auto-loaded based on the
|
||||
`Xibo\Custom` namespace.
|
||||
|
||||
This folder is also monitored by the Modules Page for `.json` files describing modules available to be installed, the
|
||||
structure of such a file is:
|
||||
|
||||
``` json
|
||||
{
|
||||
"title": "Module Title",
|
||||
"author": "Module Author",
|
||||
"description": "Module Description",
|
||||
"name": "code-name",
|
||||
"class": "Xibo\\Custom\\ClassName"
|
||||
}
|
||||
```
|
||||
|
||||
The module class must `extend Xibo\Widget\ModuleWidget` and implement the installOrUpdate method.
|
||||
|
||||
We recommend that modules put their Twig Views in a sub-folder of this one, named as their module name. This should be
|
||||
set in `installOrUpdate` like `$module->viewPath = '../custom/{name}';`.
|
||||
|
||||
|
||||
## Web Accessible Resources
|
||||
All web accessible resources must placed in the `/web/modules` folder and be installed to the library in `installFiles`.
|
||||
|
||||
|
||||
# Theme Views
|
||||
This location can also be used for theme views - we recommend a sub-folder for each theme. The theme `config.php` file
|
||||
should set its `view_path` to `PROJECT_ROOT . '/custom/folder-name`.
|
||||
43
cypress.config.js
Normal file
43
cypress.config.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2022-2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const { defineConfig } = require('cypress')
|
||||
|
||||
module.exports = defineConfig({
|
||||
viewportWidth: 1366,
|
||||
viewportHeight: 768,
|
||||
numTestsKeptInMemory: 5,
|
||||
defaultCommandTimeout: 10000,
|
||||
requestTimeout: 10000,
|
||||
env: {
|
||||
client_id: "MrGPc7e3IL1hA6w13l7Ru5giygxmNiafGNhFv89d",
|
||||
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) {
|
||||
return require('./cypress/plugins/index.js')(on, config)
|
||||
},
|
||||
baseUrl: 'http://localhost',
|
||||
},
|
||||
})
|
||||
BIN
cypress/assets/audioSample.mp3
Normal file
BIN
cypress/assets/audioSample.mp3
Normal file
Binary file not shown.
BIN
cypress/assets/export_test_layout.zip
Normal file
BIN
cypress/assets/export_test_layout.zip
Normal file
Binary file not shown.
BIN
cypress/assets/imageSample.png
Normal file
BIN
cypress/assets/imageSample.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 285 KiB |
79
cypress/e2e/Administration/applications.cy.js
Normal file
79
cypress/e2e/Administration/applications.cy.js
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Applications', function() {
|
||||
let testRun = '';
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
testRun = Cypress._.random(0, 1e9);
|
||||
});
|
||||
|
||||
it('should add edit an application', function() {
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/application/*',
|
||||
}).as('putRequest');
|
||||
|
||||
cy.visit('/application/view');
|
||||
|
||||
// Click on the Add Application button
|
||||
cy.contains('Add Application').click();
|
||||
|
||||
cy.get('.modal input#name')
|
||||
.type('Cypress Test Application ' + testRun);
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Check if application is added in toast message
|
||||
cy.contains('Edit Application');
|
||||
|
||||
cy.get('.modal input#name').clear()
|
||||
.type('Cypress Test Application Edited ' + testRun);
|
||||
|
||||
// edit test application
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
|
||||
// assertion on the "application" value
|
||||
expect(responseData.name).to.eq('Cypress Test Application Edited ' + testRun);
|
||||
// Return appKey as a Cypress.Promise to ensure proper scoping
|
||||
return Cypress.Promise.resolve(responseData.key);
|
||||
}).then((appKey) => {
|
||||
if (appKey) {
|
||||
// TODO cannot be deleted via cypress
|
||||
// Delete the application and assert success
|
||||
// cy.deleteApplication(appKey).then((res) => {
|
||||
// expect(res.status).to.equal(200);
|
||||
// });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
198
cypress/e2e/Administration/folder.cy.js
Normal file
198
cypress/e2e/Administration/folder.cy.js
Normal file
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Folders', function() {
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('creating a new folder and rename it', () => {
|
||||
cy.visit('/folders/view');
|
||||
cy.contains('Root Folder').rightclick();
|
||||
cy.contains('Create').should('be.visible').click();
|
||||
|
||||
cy.visit('/folders/view');
|
||||
cy.contains('New Folder').should('be.visible').rightclick();
|
||||
cy.contains('Rename').type('Folder123{enter}');
|
||||
});
|
||||
|
||||
it('Moving an image from Root Folder to another folder', () => {
|
||||
cy.intercept({
|
||||
url: '/library?*',
|
||||
query: {media: 'child_folder_media'},
|
||||
}).as('mediaGridLoadAfterSearch');
|
||||
|
||||
// Go to library
|
||||
cy.visit('/library/view');
|
||||
|
||||
cy.get('#media').type('child_folder_media');
|
||||
|
||||
// Wait for the search to complete
|
||||
cy.wait('@mediaGridLoadAfterSearch');
|
||||
|
||||
cy.get('#libraryItems tbody tr').should('have.length', 1);
|
||||
cy.get('#datatable-container').should('contain', 'child_folder_media');
|
||||
|
||||
// Click the dropdown menu and choose a folder to move the image to
|
||||
cy.get('#libraryItems tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#libraryItems tr:first-child .library_button_selectfolder').click({force: true});
|
||||
|
||||
// Expand the folder tree and select ChildFolder
|
||||
cy.get('#container-folder-form-tree>ul>li>i').click();
|
||||
cy.get('#container-folder-form-tree>ul>li:not(.jstree-loading)>i').click();
|
||||
cy.contains('ChildFolder').click();
|
||||
|
||||
// Click the save button
|
||||
cy.get('.save-button').click();
|
||||
});
|
||||
|
||||
it('Sharing', () => {
|
||||
// Create and alias for load user permissions for folders
|
||||
cy.intercept({
|
||||
url: '/user/permissions/Folder/*',
|
||||
query: {name: 'folder_user'},
|
||||
}).as('permissionsFoldersAfterSearch');
|
||||
|
||||
cy.visit('/folders/view');
|
||||
|
||||
cy.contains('ShareFolder').should('be.visible').rightclick();
|
||||
cy.get('ul.jstree-contextmenu >li:nth-child(6) > a').click(); // Click on Share Link
|
||||
cy.get('#name').type('folder_user');
|
||||
|
||||
cy.wait('@permissionsFoldersAfterSearch');
|
||||
|
||||
cy.get('#permissionsTable tbody tr').should('have.length', 1);
|
||||
cy.get('#permissionsTable tbody tr:nth-child(1) td:nth-child(1)').contains('folder_user');
|
||||
cy.get('#permissionsTable tbody tr:nth-child(1) td:nth-child(2)> input').click();
|
||||
cy.get('.save-button').click();
|
||||
});
|
||||
|
||||
it('Set Home Folders for a user', () => {
|
||||
// Create and alias for load users
|
||||
cy.intercept({
|
||||
url: '/user*',
|
||||
query: {userName: 'folder_user'},
|
||||
}).as('userGridLoadAfterSearch');
|
||||
|
||||
cy.visit('/user/view');
|
||||
cy.get('#userName').type('folder_user');
|
||||
|
||||
cy.wait('@userGridLoadAfterSearch');
|
||||
cy.get('#users tbody tr').should('have.length', 1);
|
||||
cy.get('#users tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#users tr:first-child .user_button_set_home').click({force: true});
|
||||
cy.get('#home-folder').should('be.visible');
|
||||
cy.get('.jstree-anchor:contains(\'FolderHome\')').should('be.visible').click();
|
||||
cy.get('.save-button').click();
|
||||
|
||||
// Check
|
||||
cy.visit('/user/view');
|
||||
cy.get('#userName').clear();
|
||||
cy.get('#userName').type('folder_user');
|
||||
cy.wait('@userGridLoadAfterSearch');
|
||||
|
||||
cy.get('#users tbody tr').should('have.length', 1);
|
||||
cy.get('#users tbody tr:nth-child(1) td:nth-child(1)').contains('folder_user');
|
||||
cy.get('#users tbody tr:nth-child(1) td:nth-child(3)').contains('FolderHome');
|
||||
});
|
||||
|
||||
it('Remove an empty folder', () => {
|
||||
cy.visit('/folders/view');
|
||||
// Find the EmptyFolder element and right-click on it
|
||||
cy.get('.jstree-anchor:contains(\'EmptyFolder\')')
|
||||
.rightclick()
|
||||
.should('have.class', 'jstree-hovered'); // Ensure the right-click effect
|
||||
|
||||
// Find the context menu item with "Remove" text and click on it
|
||||
cy.contains('Remove').click();
|
||||
|
||||
// Validate
|
||||
cy.visit('/folders/view');
|
||||
cy.get('.jstree-anchor:contains(\'EmptyFolder\')').should('not.exist');
|
||||
});
|
||||
|
||||
it('cannot remove a folder with content', () => {
|
||||
cy.visit('/folders/view');
|
||||
cy.get('.jstree-anchor:contains(\'FolderWithContent\')')
|
||||
.rightclick();
|
||||
|
||||
// Find the context menu item with "Remove" text and click on it
|
||||
cy.contains('Remove').click();
|
||||
|
||||
// Check folder still exists
|
||||
cy.visit('/folders/view');
|
||||
cy.get('.jstree-anchor:contains(\'FolderWithContent\')').should('exist');
|
||||
});
|
||||
|
||||
it('search a media in a folder', () => {
|
||||
// Go to library
|
||||
cy.visit('/library/view');
|
||||
cy.get('.jstree-anchor:contains(\'Root Folder\')')
|
||||
.should('be.visible') // Ensure the element is visible
|
||||
.parent()
|
||||
.find('.jstree-icon.jstree-ocl')
|
||||
.click();
|
||||
|
||||
cy.get('.jstree-anchor:contains(\'FolderWithImage\')').click();
|
||||
cy.get('#libraryItems tbody tr').should('have.length', 1);
|
||||
cy.get('#libraryItems tbody').contains('media_for_search_in_folder')
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
it('Hide Folder tree', () => {
|
||||
// Go to library
|
||||
cy.visit('/library/view');
|
||||
// The Folder tree is open by default on a grid
|
||||
cy.get('#folder-tree-select-folder-button').click();
|
||||
// clicking on the folder icon hides it
|
||||
cy.get('#grid-folder-filter').should('have.css', 'display', 'none');
|
||||
});
|
||||
|
||||
it('Move folders and Merge', () => {
|
||||
// Go to folders
|
||||
cy.visit('/folders/view');
|
||||
cy.get('.jstree-anchor:contains(\'MoveFromFolder\')').rightclick();
|
||||
cy.contains('Move Folder').click();
|
||||
cy.get('#container-folder-form-tree').within(() => {
|
||||
// Find the "MoveToFolder" link and click it
|
||||
cy.contains('MoveToFolder').click();
|
||||
});
|
||||
cy.get('#merge').should('be.visible').check();
|
||||
cy.get('.save-button').click();
|
||||
|
||||
// Validate test34 image exist in MoveToFolder
|
||||
cy.visit('/folders/view');
|
||||
cy.get('.jstree-anchor:contains(\'MoveFromFolder\')').should('not.exist');
|
||||
|
||||
// Validate test34 image exist in MoveToFolder
|
||||
// Go to library
|
||||
cy.visit('/library/view');
|
||||
|
||||
cy.get('.jstree-anchor:contains(\'Root Folder\')')
|
||||
.should('be.visible') // Ensure the element is visible
|
||||
.parent()
|
||||
.find('.jstree-icon.jstree-ocl')
|
||||
.click();
|
||||
cy.get('.jstree-anchor:contains(\'MoveToFolder\')').click();
|
||||
cy.get('#libraryItems tbody').contains('test34');
|
||||
});
|
||||
});
|
||||
38
cypress/e2e/Administration/modules.cy.js
Normal file
38
cypress/e2e/Administration/modules.cy.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
describe('Modules Page', function () {
|
||||
beforeEach(function () {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it.skip('should load the modules page and show a complete table of modules', function () {
|
||||
|
||||
cy.visit('/module/view');
|
||||
|
||||
cy.contains('Modules');
|
||||
|
||||
// Click on the first page of the pagination
|
||||
cy.get('.pagination > :nth-child(2) > a').click();
|
||||
|
||||
cy.contains('Showing 1 to');
|
||||
});
|
||||
});
|
||||
157
cypress/e2e/Administration/tags.cy.js
Normal file
157
cypress/e2e/Administration/tags.cy.js
Normal file
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Tags', function() {
|
||||
let testRun = '';
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
testRun = Cypress._.random(0, 1e9);
|
||||
});
|
||||
|
||||
it('should add a tag', function() {
|
||||
cy.visit('/tag/view');
|
||||
|
||||
// Click on the Add Tag button
|
||||
cy.contains('Add Tag').click();
|
||||
|
||||
cy.get('.modal input#name')
|
||||
.type('Cypress Test Tag ' + testRun + '_1');
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Check if tag is added in toast message
|
||||
cy.contains('Added Cypress Test Tag ' + testRun + '_1');
|
||||
});
|
||||
|
||||
it('searches and edit existing tag', function() {
|
||||
// Create a new tag and then search for it and delete it
|
||||
cy.createTag('Cypress Test Tag ' + testRun).then((res) => {
|
||||
cy.intercept({
|
||||
url: '/tag?*',
|
||||
query: {tag: 'Cypress Test Tag ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/tag/*',
|
||||
}).as('putRequest');
|
||||
|
||||
cy.visit('/tag/view');
|
||||
|
||||
// Filter for the created tag
|
||||
cy.get('#Filter input[name="tag"]')
|
||||
.type('Cypress Test Tag ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#tags tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#tags tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#tags tr:first-child .tag_button_edit').click({force: true});
|
||||
|
||||
cy.get('.modal input#name').clear()
|
||||
.type('Cypress Test Tag Edited ' + testRun);
|
||||
|
||||
// edit test tag
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
const tag = responseData.tag;
|
||||
|
||||
// assertion on the "tag" value
|
||||
expect(tag).to.eq('Cypress Test Tag Edited ' + testRun);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('searches and delete existing tag', function() {
|
||||
// Create a new tag and then search for it and delete it
|
||||
cy.createTag('Cypress Test Tag ' + testRun).then((res) => {
|
||||
cy.intercept({
|
||||
url: '/tag?*',
|
||||
query: {tag: 'Cypress Test Tag ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
cy.visit('/tag/view');
|
||||
|
||||
// Filter for the created tag
|
||||
cy.get('#Filter input[name="tag"]')
|
||||
.type('Cypress Test Tag ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#tags tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#tags tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#tags tr:first-child .tag_button_delete').click({force: true});
|
||||
|
||||
// Delete test tag
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check if tag is deleted in toast message
|
||||
cy.get('.toast').contains('Deleted Cypress Test Tag');
|
||||
});
|
||||
});
|
||||
|
||||
it('selects multiple tags and delete them', function() {
|
||||
// Create a new tag and then search for it and delete it
|
||||
cy.createTag('Cypress Test Tag ' + testRun).then((res) => {
|
||||
cy.intercept({
|
||||
url: '/tag?*',
|
||||
query: {tag: 'Cypress Test Tag'},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Delete all test tags
|
||||
cy.visit('/tag/view');
|
||||
|
||||
// Clear filter
|
||||
cy.get('.clear-filter-btn').click();
|
||||
cy.get('#Filter input[name="tag"]')
|
||||
.type('Cypress Test Tag');
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
|
||||
// Select all
|
||||
cy.get('button[data-toggle="selectAll"]').click();
|
||||
|
||||
// Delete all
|
||||
cy.get('.dataTables_info button[data-toggle="dropdown"]').click();
|
||||
cy.get('.dataTables_info a[data-button-id="tag_button_delete"]').click();
|
||||
|
||||
cy.get('button.save-button').click();
|
||||
|
||||
// Modal should contain one successful delete at least
|
||||
cy.get('.modal-body').contains(': Success');
|
||||
});
|
||||
});
|
||||
});
|
||||
64
cypress/e2e/Administration/tasks.cy.js
Normal file
64
cypress/e2e/Administration/tasks.cy.js
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Tasks', function() {
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('should edit a task', function() {
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/task/*',
|
||||
}).as('putRequest');
|
||||
|
||||
cy.visit('/task/view');
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#tasks tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#tasks tr:first-child .task_button_edit').click({force: true});
|
||||
|
||||
// Assuming you have an input field with the id 'myInputField'
|
||||
cy.get('.modal input#name').invoke('val').then((value) => {
|
||||
return Cypress.Promise.resolve(value);
|
||||
}).then((value) => {
|
||||
if (value) {
|
||||
cy.get('.modal input#name').clear()
|
||||
.type(value + ' Edited');
|
||||
|
||||
// edit test tag
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
|
||||
// assertion on the "task" value
|
||||
expect(responseData.name).to.eq(value + ' Edited');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
64
cypress/e2e/Administration/transitions.cy.js
Normal file
64
cypress/e2e/Administration/transitions.cy.js
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Transitions', function() {
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('should edit an transition', function() {
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/transition/*',
|
||||
}).as('putRequest');
|
||||
|
||||
cy.visit('/transition/view');
|
||||
cy.get('#transitions tbody tr').should('have.length', 3);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#transitions tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#transitions tr:first-child .transition_button_edit').click({force: true});
|
||||
|
||||
cy.get('.modal #availableAsIn').then(($checkbox) => {
|
||||
const isChecked = $checkbox.prop('checked');
|
||||
cy.get('#availableAsIn').should('be.visible').click(); // Click to check/uncheck
|
||||
|
||||
// edit
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
|
||||
// assertion on the "task" value
|
||||
if (isChecked) {
|
||||
expect(responseData.availableAsIn).to.eq(0);
|
||||
} else {
|
||||
expect(responseData.availableAsIn).to.eq(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
128
cypress/e2e/Administration/usergroups.cy.js
Normal file
128
cypress/e2e/Administration/usergroups.cy.js
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Usergroups', function() {
|
||||
let testRun = '';
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
testRun = Cypress._.random(0, 1e9);
|
||||
});
|
||||
|
||||
it('should add a usergroup', function() {
|
||||
cy.visit('/group/view');
|
||||
|
||||
// Click on the Add Usergroup button
|
||||
cy.contains('Add User Group').click();
|
||||
|
||||
cy.get('.modal input#group')
|
||||
.type('Cypress Test Usergroup ' + testRun + '_1');
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Check if usergroup is added in toast message
|
||||
cy.contains('Added Cypress Test Usergroup');
|
||||
});
|
||||
|
||||
it('searches and edit existing usergroup', function() {
|
||||
// Create a new usergroup and then search for it and delete it
|
||||
cy.createUsergroup('Cypress Test Usergroup ' + testRun).then((groupId) => {
|
||||
cy.intercept({
|
||||
url: '/group?*',
|
||||
query: {userGroup: 'Cypress Test Usergroup ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/group/*',
|
||||
}).as('putRequest');
|
||||
|
||||
cy.visit('/group/view');
|
||||
|
||||
// Filter for the created usergroup
|
||||
cy.get('#Filter input[name="userGroup"]')
|
||||
.type('Cypress Test Usergroup ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#userGroups tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#userGroups tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#userGroups tr:first-child .usergroup_button_edit').click({force: true});
|
||||
|
||||
cy.get('.modal input#group').clear()
|
||||
.type('Cypress Test Usergroup Edited ' + testRun);
|
||||
|
||||
// edit test usergroup
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
|
||||
// assertion on the "usergroup" value
|
||||
expect(responseData.group).to.eq('Cypress Test Usergroup Edited ' + testRun);
|
||||
|
||||
// Delete the usergroup and assert success
|
||||
cy.deleteUsergroup(groupId).then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('searches and delete existing usergroup', function() {
|
||||
// Create a new usergroup and then search for it and delete it
|
||||
cy.createUsergroup('Cypress Test Usergroup ' + testRun).then((groupId) => {
|
||||
cy.intercept({
|
||||
url: '/group?*',
|
||||
query: {userGroup: 'Cypress Test Usergroup ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
cy.visit('/group/view');
|
||||
|
||||
// Filter for the created usergroup
|
||||
cy.get('#Filter input[name="userGroup"]')
|
||||
.type('Cypress Test Usergroup ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#userGroups tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#userGroups tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#userGroups tr:first-child .usergroup_button_delete').click({force: true});
|
||||
|
||||
// Delete test usergroup
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check if usergroup is deleted in toast message
|
||||
cy.get('.toast').contains('Deleted Cypress Test Usergroup');
|
||||
});
|
||||
});
|
||||
});
|
||||
166
cypress/e2e/Administration/users.cy.js
Normal file
166
cypress/e2e/Administration/users.cy.js
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Users', function() {
|
||||
let testRun = '';
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
testRun = Cypress._.random(0, 1e9);
|
||||
});
|
||||
|
||||
it('should add a user', function() {
|
||||
cy.intercept({
|
||||
url: '/user/form/homepages?groupId=1&userTypeId=3*',
|
||||
query: {},
|
||||
}).as('loadHomepageAfterSearch');
|
||||
|
||||
cy.visit('/user/view');
|
||||
|
||||
// Click on the Add User button
|
||||
cy.contains('Add User').click();
|
||||
cy.get('.radio input[value="manual"]').click();
|
||||
|
||||
cy.get('#onboarding-steper-next-button').click();
|
||||
|
||||
cy.get('.modal input#userName')
|
||||
.type('CypressTestUser' + testRun);
|
||||
|
||||
cy.get('.modal input#password')
|
||||
.type('cypress');
|
||||
|
||||
// Error checking - for incorrect email format
|
||||
cy.get('.modal input#email').type('cypress');
|
||||
|
||||
cy.get('.select2-container--bootstrap').eq(1).click();
|
||||
cy.log('Before waiting for Icon Dashboard element');
|
||||
cy.wait('@loadHomepageAfterSearch');
|
||||
cy.get('.select2-results__option')
|
||||
.should('contain', 'Icon Dashboard')
|
||||
.click();
|
||||
|
||||
// Try saving
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
cy.contains('Please enter a valid email address.');
|
||||
cy.get('.modal input#email').clear().type('cypress@test.com');
|
||||
|
||||
// Save
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Check if user is added in toast message
|
||||
cy.contains('Added CypressTestUser');
|
||||
});
|
||||
|
||||
it('searches and edit existing user', function() {
|
||||
// Create a new user and then search for it and delete it
|
||||
cy.createUser('CypressTestUser' + testRun, 'password', 3, 1).then((id) => {
|
||||
cy.intercept({
|
||||
url: '/user?*',
|
||||
query: {userName: 'CypressTestUser' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/user/*',
|
||||
}).as('putRequest');
|
||||
|
||||
cy.visit('/user/view');
|
||||
|
||||
// Filter for the created user
|
||||
cy.get('#Filter input[name="userName"]')
|
||||
.type('CypressTestUser' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#users tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#users tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#users tr:first-child .user_button_edit').click({force: true});
|
||||
|
||||
cy.get('.modal input#userName').clear()
|
||||
.type('CypressTestUserEdited' + testRun);
|
||||
|
||||
cy.get('.modal input#newPassword').clear().type('newPassword');
|
||||
cy.get('.modal input#retypeNewPassword').clear().type('wrongPassword');
|
||||
|
||||
// edit test user
|
||||
cy.get('.bootbox .save-button').click();
|
||||
cy.wait('@putRequest')
|
||||
|
||||
// Error checking - for password mismatch
|
||||
cy.contains('Passwords do not match');
|
||||
cy.get('.modal input#retypeNewPassword').clear().type('newPassword');
|
||||
|
||||
// edit test user
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
|
||||
// assertion on the "user" value
|
||||
expect(responseData.userName).to.eq('CypressTestUserEdited' + testRun);
|
||||
});
|
||||
|
||||
// Delete the user and assert success
|
||||
cy.deleteUser(id).then((res) => {
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('searches and delete existing user', function() {
|
||||
// Create a new user and then search for it and delete it
|
||||
cy.createUser('CypressTestUser' + testRun, 'password', 3, 1).then((id) => {
|
||||
cy.intercept({
|
||||
url: '/user?*',
|
||||
query: {userName: 'CypressTestUser' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
cy.visit('/user/view');
|
||||
|
||||
// Filter for the created user
|
||||
cy.get('#Filter input[name="userName"]')
|
||||
.type('CypressTestUser' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#users tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#users tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#users tr:first-child .user_button_delete').click({force: true});
|
||||
|
||||
// Delete test User
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check if User is deleted in toast message
|
||||
cy.get('.toast').contains('Deleted CypressTestUser');
|
||||
});
|
||||
});
|
||||
});
|
||||
167
cypress/e2e/Campaign/campaigns.cy.js
Normal file
167
cypress/e2e/Campaign/campaigns.cy.js
Normal file
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Campaigns', function() {
|
||||
const testRun = Cypress._.random(0, 1e9);
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
// 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({
|
||||
url: '/campaign?*',
|
||||
query: {name: 'Cypress Test Campaign ' + testRun},
|
||||
}).as('campaignGridLoadAfterSearch');
|
||||
|
||||
cy.intercept({
|
||||
url: '/layout?*',
|
||||
query: {layout: 'List Campaign Layout'},
|
||||
}).as('layoutLoadAfterSearch');
|
||||
|
||||
// Intercept the POST request to get the campaign Id
|
||||
cy.intercept('/campaign').as('postCampaign');
|
||||
cy.intercept('/campaign/form/add?*').as('campaignFormAdd');
|
||||
|
||||
cy.visit('/campaign/view');
|
||||
|
||||
// Click on the Add Campaign button
|
||||
cy.contains('Add Campaign').click();
|
||||
|
||||
cy.get('.modal input#name')
|
||||
.type('Cypress Test Campaign ' + testRun);
|
||||
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// 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();
|
||||
|
||||
// Should have no layouts assigned
|
||||
cy.get('.modal #LayoutAssignSortable').children()
|
||||
.should('have.length', 0);
|
||||
|
||||
// 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').blur();
|
||||
|
||||
// Wait for the intercepted request and check the URL for the desired query parameter value
|
||||
cy.wait('@layoutLoadAfterSearch').then((interception) => {
|
||||
// Perform your desired actions or assertions here
|
||||
cy.log('Layout Loading');
|
||||
|
||||
cy.get('#layoutAssignments tbody tr').should('have.length', 2);
|
||||
|
||||
// Assign a layout
|
||||
cy.get('#layoutAssignments tr:nth-child(1) a.assignItem').click();
|
||||
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);
|
||||
|
||||
cy.wait('@campaignGridLoadAfterSearch');
|
||||
|
||||
// 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');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
// Schedule the campaign
|
||||
cy.scheduleCampaign(sessionCampaignId, displayName).then((res) => {
|
||||
cy.displaySetStatus(displayName, 1);
|
||||
|
||||
// Go to display grid
|
||||
cy.intercept('/display?draw=3&*').as('displayGridLoad');
|
||||
|
||||
cy.visit('/display/view');
|
||||
|
||||
// Filter for the created campaign
|
||||
cy.get('.FilterDiv input[name="display"]')
|
||||
.type(displayName);
|
||||
|
||||
// Should have the display
|
||||
cy.get('#displays tbody tr').should('have.length', 1);
|
||||
|
||||
// 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('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');
|
||||
|
||||
// Filter for the created campaign
|
||||
cy.get('#Filter input[name="name"]')
|
||||
.type('Cypress Test Campaign ' + testRun);
|
||||
|
||||
// Wait for 2nd campaign grid reload
|
||||
cy.wait('@campaignGridLoad');
|
||||
|
||||
cy.get('#campaigns tbody tr').should('have.length', 1);
|
||||
|
||||
cy.get('#campaigns tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#campaigns tr:first-child .campaign_button_delete').click({force: true});
|
||||
|
||||
// Delete the campaign
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted DELETE request to complete with status 200
|
||||
cy.wait('@deleteCampaign').its('response.statusCode').should('eq', 200);
|
||||
|
||||
// check the display status
|
||||
cy.displayStatusEquals('List Campaign Display 1', 3).then((res) => {
|
||||
expect(res.body).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
318
cypress/e2e/Display/displaygroups.cy.js
Normal file
318
cypress/e2e/Display/displaygroups.cy.js
Normal file
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Display Groups', function() {
|
||||
let testRun = '';
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
testRun = Cypress._.random(0, 1e9);
|
||||
});
|
||||
|
||||
it('should add one empty and one filled display groups', function() {
|
||||
cy.visit('/displaygroup/view');
|
||||
|
||||
// Click on the Add Displaygroup button
|
||||
cy.contains('Add Display Group').click();
|
||||
|
||||
cy.get('.modal input#displayGroup')
|
||||
.type('Cypress Test Displaygroup ' + testRun + '_1');
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal').contains('Next').click();
|
||||
|
||||
// Check if displaygroup is added in toast message
|
||||
cy.contains('Added Cypress Test Displaygroup ' + testRun + '_1');
|
||||
|
||||
cy.get('.modal input#displayGroup')
|
||||
.type('Cypress Test Displaygroup ' + testRun + '_2');
|
||||
|
||||
cy.get('.modal input#description')
|
||||
.type('Description');
|
||||
|
||||
cy.get('.modal input#isDynamic').check();
|
||||
|
||||
cy.get('.modal input#dynamicCriteria')
|
||||
.type('testLayoutId');
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Check if displaygroup is added in toast message
|
||||
cy.contains('Added Cypress Test Displaygroup ' + testRun + '_2');
|
||||
});
|
||||
|
||||
it('copy an existing displaygroup', function() {
|
||||
// Create a new displaygroup and then search for it and delete it
|
||||
cy.createDisplaygroup('Cypress Test Displaygroup ' + testRun).then((res) => {
|
||||
cy.intercept({
|
||||
url: '/displaygroup?*',
|
||||
query: {displayGroup: 'Cypress Test Displaygroup ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the POST request
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: /\/displaygroup\/\d+\/copy$/,
|
||||
}).as('postRequest');
|
||||
|
||||
cy.visit('/displaygroup/view');
|
||||
|
||||
// Filter for the created displaygroup
|
||||
cy.get('#Filter input[name="displayGroup"]')
|
||||
.type('Cypress Test Displaygroup ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#displaygroups tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#displaygroups tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#displaygroups tr:first-child .displaygroup_button_copy').click({force: true});
|
||||
|
||||
// Delete test displaygroup
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted POST request and check the form data
|
||||
cy.wait('@postRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
expect(responseData.displayGroup).to.include('Cypress Test Displaygroup ' + testRun + ' 2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('searches and delete existing displaygroup', function() {
|
||||
// Create a new displaygroup and then search for it and delete it
|
||||
cy.createDisplaygroup('Cypress Test Displaygroup ' + testRun).then((res) => {
|
||||
cy.intercept({
|
||||
url: '/displaygroup?*',
|
||||
query: {displayGroup: 'Cypress Test Displaygroup ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
cy.visit('/displaygroup/view');
|
||||
|
||||
// Filter for the created displaygroup
|
||||
cy.get('#Filter input[name="displayGroup"]')
|
||||
.type('Cypress Test Displaygroup ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#displaygroups tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#displaygroups tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#displaygroups tr:first-child .displaygroup_button_delete').click({force: true});
|
||||
|
||||
// Delete test displaygroup
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check if displaygroup is deleted in toast message
|
||||
cy.get('.toast').contains('Deleted Cypress Test Displaygroup');
|
||||
});
|
||||
});
|
||||
|
||||
// Seeded displays: dispgrp_disp1, dispgrp_disp2
|
||||
it('manage membership for a displaygroup', function() {
|
||||
cy.createDisplaygroup('Cypress Test Displaygroup ' + testRun).then((res) => {
|
||||
// assign displays to display group
|
||||
cy.intercept({
|
||||
url: '/displaygroup?*',
|
||||
query: {displayGroup: 'Cypress Test Displaygroup ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: /\/displaygroup\/\d+\/display\/assign$/,
|
||||
}).as('postRequest');
|
||||
|
||||
cy.intercept({
|
||||
url: '/display*',
|
||||
query: {display: 'dispgrp_disp1'},
|
||||
}).as('loadDisplayAfterSearch');
|
||||
|
||||
cy.visit('/displaygroup/view');
|
||||
|
||||
// Filter for the created displaygroup
|
||||
cy.get('#Filter input[name="displayGroup"]')
|
||||
.type('Cypress Test Displaygroup ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#displaygroups tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#displaygroups tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#displaygroups tr:first-child .displaygroup_button_group_members').click({force: true});
|
||||
|
||||
cy.get('.modal #display').type('dispgrp_disp1');
|
||||
|
||||
cy.wait('@loadDisplayAfterSearch');
|
||||
cy.get('#displaysMembersTable').within(() => {
|
||||
// count the rows within table
|
||||
cy.get('tbody').find('tr').should('have.length', 1);
|
||||
cy.get('tbody tr:first-child input[type="checkbox"]').check();
|
||||
});
|
||||
|
||||
// Save assignments
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted POST request and check the form data
|
||||
cy.wait('@postRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const body = response.body;
|
||||
expect(body.success).to.eq(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// -------
|
||||
// Seeded displays: dispgrp_disp_dynamic1, dispgrp_disp_dynamic2
|
||||
it('should add a dynamic display group', function() {
|
||||
cy.intercept({
|
||||
url: '/display?*',
|
||||
query: {display: 'dynamic'},
|
||||
}).as('loadDisplayGridAfterSearch');
|
||||
|
||||
cy.visit('/displaygroup/view');
|
||||
|
||||
// Click on the Add Displaygroup button
|
||||
cy.contains('Add Display Group').click();
|
||||
|
||||
cy.get('.modal input#displayGroup')
|
||||
.type('Cypress Test Displaygroup ' + testRun);
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal #isDynamic').check();
|
||||
// Type "dynamic" into the input field with the name "dynamicCriteria"
|
||||
cy.get('.modal input[name="dynamicCriteria"]').type('dynamic');
|
||||
cy.wait('@loadDisplayGridAfterSearch');
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Check if displaygroup is added in toast message
|
||||
cy.contains('Added Cypress Test Displaygroup ' + testRun);
|
||||
});
|
||||
|
||||
it('should edit the criteria of a dynamic display group', function() {
|
||||
// Create a new displaygroup with dynamic criteria
|
||||
cy.createDisplaygroup('Cypress Test Displaygroup Dynamic ' + testRun, true, 'dynamic').then((res) => {
|
||||
cy.intercept({
|
||||
url: '/displaygroup?*',
|
||||
query: {displayGroup: 'Cypress Test Displaygroup Dynamic ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/displaygroup/*',
|
||||
}).as('putRequest');
|
||||
|
||||
cy.visit('/displaygroup/view');
|
||||
|
||||
// Filter for the created displaygroup
|
||||
cy.get('#Filter input[name="displayGroup"]')
|
||||
.type('Cypress Test Displaygroup Dynamic ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#displaygroups tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#displaygroups tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#displaygroups tr:first-child .displaygroup_button_edit').click({force: true});
|
||||
|
||||
cy.get('.modal input[name="dynamicCriteria"]').clear().type('dynamic_edited');
|
||||
|
||||
// Delete test displaygroup
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
|
||||
// assertion on the "display" value
|
||||
expect(responseData.dynamicCriteria).to.eq('dynamic_edited');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// -------
|
||||
// -- Delete Many
|
||||
it('selects multiple display groups and delete them', function() {
|
||||
// Create a new displaygroup and then search for it and delete it
|
||||
cy.createDisplaygroup('Cypress Test Displaygroup ' + testRun).then((res) => {
|
||||
cy.intercept('GET', '/displaygroup?draw=2&*').as('displaygroupGridLoad');
|
||||
|
||||
// Delete all test displaygroups
|
||||
cy.visit('/displaygroup/view');
|
||||
|
||||
// Clear filter
|
||||
cy.get('#Filter input[name="displayGroup"]')
|
||||
.clear()
|
||||
.type('Cypress Test Displaygroup');
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@displaygroupGridLoad');
|
||||
|
||||
// Select all
|
||||
cy.get('button[data-toggle="selectAll"]').click();
|
||||
|
||||
// Delete all
|
||||
cy.get('.dataTables_info button[data-toggle="dropdown"]').click();
|
||||
cy.get('.dataTables_info a[data-button-id="displaygroup_button_delete"]').click();
|
||||
|
||||
cy.get('input#checkbox-confirmDelete').check();
|
||||
cy.get('button.save-button').click();
|
||||
|
||||
// Modal should contain one successful delete at least
|
||||
cy.get('.modal-body').contains(': Success');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------
|
||||
// Tests - Error handling
|
||||
it('should not add a displaygroup without dynamic criteria', function() {
|
||||
cy.visit('/displaygroup/view');
|
||||
|
||||
// Click on the Add Displaygroup button
|
||||
cy.contains('Add Display Group').click();
|
||||
|
||||
cy.get('.modal input#displayGroup')
|
||||
.type('Cypress Test Displaygroup ' + testRun + '_1');
|
||||
|
||||
cy.get('.modal input#isDynamic').check();
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Check toast message
|
||||
cy.contains('Dynamic Display Groups must have at least one Criteria specified.');
|
||||
});
|
||||
});
|
||||
334
cypress/e2e/Display/displays.cy.js
Normal file
334
cypress/e2e/Display/displays.cy.js
Normal file
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Displays', function() {
|
||||
let testRun = '';
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
testRun = Cypress._.random(0, 1e9);
|
||||
});
|
||||
|
||||
// Seeded displays: disp1, disp2, disp3, disp4, disp5
|
||||
// Seeded display Groups: disp5_dispgrp
|
||||
// Seeded layouts: disp4_default_layout
|
||||
it('searches and edit existing display', function() {
|
||||
// search for a display disp1 and edit
|
||||
cy.intercept({
|
||||
url: '/display?*',
|
||||
query: {display: 'dis_disp1'},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/display/*',
|
||||
}).as('putRequest');
|
||||
|
||||
cy.visit('/display/view');
|
||||
|
||||
// Filter for the created display
|
||||
cy.get('#Filter input[name="display"]')
|
||||
.type('dis_disp1');
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#displays tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#displays tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#displays tr:first-child .display_button_edit').click({force: true});
|
||||
|
||||
cy.get('.modal input#display').clear()
|
||||
.type('dis_disp1 Edited');
|
||||
|
||||
cy.get('.modal input#license').clear()
|
||||
.type('dis_disp1_license');
|
||||
|
||||
cy.get('.modal input#description').clear()
|
||||
.type('description');
|
||||
|
||||
// edit test display
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
|
||||
// assertion on the "display" value
|
||||
expect(responseData.display).to.eq('dis_disp1 Edited');
|
||||
expect(responseData.description).to.eq('description');
|
||||
expect(responseData.license).to.eq('dis_disp1_license');
|
||||
});
|
||||
});
|
||||
|
||||
// Display: disp2
|
||||
it('searches and delete existing display', function() {
|
||||
cy.intercept({
|
||||
url: '/display?*',
|
||||
query: {display: 'dis_disp2'},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
cy.visit('/display/view');
|
||||
|
||||
// Filter for the created display
|
||||
cy.get('#Filter input[name="display"]')
|
||||
.type('dis_disp2');
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#displays tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#displays tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#displays tr:first-child .display_button_delete').click({force: true});
|
||||
|
||||
// Delete test display
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check if display is deleted in toast message
|
||||
cy.get('.toast').contains('Deleted dis_disp2');
|
||||
});
|
||||
|
||||
// Display: disp3
|
||||
it('searches and authorise an unauthorised display', function() {
|
||||
// search for a display disp1 and edit
|
||||
cy.intercept({
|
||||
url: '/display?*',
|
||||
query: {display: 'dis_disp3'},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/display/authorise/*',
|
||||
}).as('putRequest');
|
||||
|
||||
cy.visit('/display/view');
|
||||
|
||||
// Filter for the created display
|
||||
cy.get('#Filter input[name="display"]')
|
||||
.type('dis_disp3');
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#displays tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#displays tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#displays tr:first-child .display_button_authorise').click({force: true});
|
||||
|
||||
// edit test display
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
// assertion
|
||||
expect(response.body.message).to.eq('Authorised set to 1 for dis_disp3');
|
||||
});
|
||||
});
|
||||
|
||||
// Display: disp4
|
||||
it('set a default layout', function() {
|
||||
cy.intercept({
|
||||
url: '/display?*',
|
||||
query: {display: 'dis_disp4'},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/display/defaultlayout/*',
|
||||
}).as('putRequest');
|
||||
|
||||
cy.intercept({
|
||||
url: '/layout*',
|
||||
query: {
|
||||
layout: 'disp4_default_layout',
|
||||
},
|
||||
}).as('loadLayoutAfterSearch');
|
||||
|
||||
cy.visit('/display/view');
|
||||
|
||||
// Filter for the created display
|
||||
cy.get('#Filter input[name="display"]')
|
||||
.type('dis_disp4');
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#displays tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#displays tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#displays tr:first-child .display_button_defaultlayout').click({force: true});
|
||||
|
||||
// Set the default layout
|
||||
cy.get('.modal .select2-container--bootstrap').click();
|
||||
cy.get('.select2-search__field').type('disp4_default_layout');
|
||||
|
||||
cy.wait('@loadLayoutAfterSearch');
|
||||
cy.get('.select2-results__option').contains('disp4_default_layout').click();
|
||||
|
||||
// edit test display
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const body = response.body;
|
||||
expect(body.success).to.eq(true);
|
||||
});
|
||||
});
|
||||
|
||||
// Display: disp5
|
||||
it('manage membership for disp5', function() {
|
||||
cy.intercept({
|
||||
url: '/display?*',
|
||||
query: {display: 'dis_disp5'},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: /\/display\/\d+\/displaygroup\/assign$/,
|
||||
}).as('postRequest');
|
||||
|
||||
cy.intercept({
|
||||
url: '/displaygroup*',
|
||||
query: {
|
||||
displayGroup: 'disp5_dispgrp',
|
||||
},
|
||||
}).as('loadDisplaypGroupAfterSearch');
|
||||
|
||||
cy.visit('/display/view');
|
||||
|
||||
// Filter for the created display
|
||||
cy.get('#Filter input[name="display"]')
|
||||
.type('dis_disp5');
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#displays tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#displays tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#displays tr:first-child .display_button_group_membership').click({force: true});
|
||||
|
||||
cy.get('.modal #displayGroup').type('disp5_dispgrp');
|
||||
|
||||
cy.wait('@loadDisplaypGroupAfterSearch');
|
||||
cy.get('#displaysGroupsMembersTable').within(() => {
|
||||
// count the rows within table
|
||||
cy.get('tbody').find('tr')
|
||||
.should('have.length', 1)
|
||||
.and('contain', 'disp5_dispgrp');
|
||||
cy.get('tbody tr:first-child input[type="checkbox"]')
|
||||
.should('not.be.checked')
|
||||
.check();
|
||||
});
|
||||
|
||||
// Save assignments
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted POST request and check the form data
|
||||
cy.wait('@postRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const body = response.body;
|
||||
expect(body.success).to.eq(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should display map and revert back to table', function() {
|
||||
cy.intercept('GET', '/user/pref?preference=displayGrid').as('displayPrefsLoad');
|
||||
cy.intercept('GET', '/display?draw=2*').as('displayLoad');
|
||||
cy.intercept('POST', '/user/pref').as('userPrefPost');
|
||||
|
||||
cy.visit('/display/view');
|
||||
|
||||
cy.wait('@displayPrefsLoad');
|
||||
cy.wait('@displayLoad');
|
||||
cy.wait('@userPrefPost');
|
||||
|
||||
cy.get('#map_button').click();
|
||||
|
||||
cy.get('#display-map.leaflet-container').should('be.visible');
|
||||
|
||||
cy.get('#list_button').click();
|
||||
|
||||
cy.get('#displays_wrapper.dataTables_wrapper').should('be.visible');
|
||||
});
|
||||
|
||||
// ---------
|
||||
// Tests - Error handling
|
||||
it('should not be able to save while editing existing display with incorrect latitude/longitude', function() {
|
||||
// search for a display disp1 and edit
|
||||
cy.intercept({
|
||||
url: '/display?*',
|
||||
query: {display: 'dis_disp1'},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/display/*',
|
||||
}).as('putRequest');
|
||||
|
||||
cy.visit('/display/view');
|
||||
|
||||
// Filter for the created display
|
||||
cy.get('#Filter input[name="display"]')
|
||||
.type('dis_disp1');
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#displays tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#displays tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#displays tr:first-child .display_button_edit').click({force: true});
|
||||
cy.contains('Details').click();
|
||||
|
||||
cy.get('.modal input#latitude').type('1234');
|
||||
|
||||
// edit test display
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check error message
|
||||
cy.contains('The latitude entered is not valid.');
|
||||
|
||||
cy.get('.modal input#latitude').clear();
|
||||
cy.get('.modal input#longitude').type('1234');
|
||||
|
||||
// edit test display
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check error message
|
||||
cy.contains('The longitude entered is not valid.');
|
||||
});
|
||||
});
|
||||
166
cypress/e2e/Display/displaysettings.cy.js
Normal file
166
cypress/e2e/Display/displaysettings.cy.js
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Display Settings', function() {
|
||||
let testRun = '';
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
testRun = Cypress._.random(0, 1e9);
|
||||
});
|
||||
|
||||
it('should and edit a display setting', function() {
|
||||
// Intercept the POST request
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: '/displayprofile',
|
||||
}).as('postRequest');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/displayprofile/*',
|
||||
}).as('putRequest');
|
||||
|
||||
cy.visit('/displayprofile/view');
|
||||
|
||||
// Click on the Add Display Setting button
|
||||
cy.contains('Add Profile').click();
|
||||
|
||||
cy.get('.modal input#name')
|
||||
.type('Cypress Test Display Setting ' + testRun);
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@postRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
|
||||
// assertion on the "tag" value
|
||||
expect(responseData.name).to.eq('Cypress Test Display Setting ' + testRun);
|
||||
|
||||
cy.get('.modal input#name').clear()
|
||||
.type('Cypress Test Display Setting Edited ' + testRun);
|
||||
|
||||
// Select the option with the value "10 minutes"
|
||||
cy.get('.modal #collectInterval').select('600');
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
|
||||
// assertion on the "tag" value
|
||||
expect(responseData.name).to.eq('Cypress Test Display Setting Edited ' + testRun);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('searches and edit existing display setting', function() {
|
||||
// Create a new tag and then search for it and delete it
|
||||
cy.createDisplayProfile('Cypress Test Display Setting ' + testRun, 'android').then((id) => {
|
||||
cy.intercept({
|
||||
url: '/displayprofile?*',
|
||||
query: {displayProfile: 'Cypress Test Display Setting ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/displayprofile/*',
|
||||
}).as('putRequest');
|
||||
|
||||
cy.visit('/displayprofile/view');
|
||||
|
||||
// Filter for the created tag
|
||||
cy.get('#Filter input[name="displayProfile"]')
|
||||
.type('Cypress Test Display Setting ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#displayProfiles tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#displayProfiles tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#displayProfiles tr:first-child .displayprofile_button_edit').click({force: true});
|
||||
|
||||
cy.get('.modal input#name').clear()
|
||||
.type('Cypress Test Display Setting Edited ' + testRun);
|
||||
|
||||
// edit test tag
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
|
||||
// assertion on the "tag" value
|
||||
expect(responseData.name).to.eq('Cypress Test Display Setting Edited ' + testRun);
|
||||
});
|
||||
|
||||
// Delete the user and assert success
|
||||
cy.deleteDisplayProfile(id).then((res) => {
|
||||
expect(res.status).to.equal(204);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('searches and delete existing display setting', function() {
|
||||
// Create a new tag and then search for it and delete it
|
||||
cy.createDisplayProfile('Cypress Test Display Setting ' + testRun, 'android').then((id) => {
|
||||
cy.intercept({
|
||||
url: '/displayprofile?*',
|
||||
query: {displayProfile: 'Cypress Test Display Setting ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
cy.visit('/displayprofile/view');
|
||||
|
||||
// Filter for the created tag
|
||||
cy.get('#Filter input[name="displayProfile"]')
|
||||
.type('Cypress Test Display Setting ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#displayProfiles tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#displayProfiles tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#displayProfiles tr:first-child .displayprofile_button_delete').click({force: true});
|
||||
|
||||
// Delete test tag
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check if tag is deleted in toast message
|
||||
cy.get('.toast').contains('Deleted Cypress Test Display Setting');
|
||||
});
|
||||
});
|
||||
});
|
||||
97
cypress/e2e/Display/syncgroups.cy.js
Normal file
97
cypress/e2e/Display/syncgroups.cy.js
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Sync Groups', function() {
|
||||
let testRun = '';
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
testRun = Cypress._.random(0, 1e9);
|
||||
});
|
||||
|
||||
it('should add one empty syncgroups', function() {
|
||||
cy.visit('/syncgroup/view');
|
||||
|
||||
// Click on the Add Sync Group button
|
||||
cy.contains('Add Sync Group').click();
|
||||
|
||||
cy.get('.modal input#name')
|
||||
.type('Cypress Test Sync Group ' + testRun);
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Check if syncgroup is added in toast message
|
||||
cy.contains('Added Cypress Test Sync Group ' + testRun);
|
||||
});
|
||||
|
||||
it('searches and delete existing syncgroup', function() {
|
||||
// Create a new syncgroup and then search for it and delete it
|
||||
cy.createSyncGroup('Cypress Test Sync Group ' + testRun).then((res) => {
|
||||
cy.intercept({
|
||||
url: '/syncgroup?*',
|
||||
query: {name: 'Cypress Test Sync Group ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
cy.visit('/syncgroup/view');
|
||||
|
||||
// Filter for the created syncgroup
|
||||
cy.get('#Filter input[name="name"]')
|
||||
.type('Cypress Test Sync Group ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#syncgroups tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#syncgroups tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#syncgroups tr:first-child .syncgroup_button_group_delete').click({force: true});
|
||||
|
||||
// Delete test syncgroup
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check if syncgroup is deleted in toast message
|
||||
cy.get('.toast').contains('Deleted Cypress Test Sync Group');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------
|
||||
// Tests - Error handling
|
||||
it.only('should not add a syncgroup without publisher port', function() {
|
||||
cy.visit('/syncgroup/view');
|
||||
|
||||
// Click on the Add Sync Group button
|
||||
cy.contains('Add Sync Group').click();
|
||||
|
||||
cy.get('.modal input#name')
|
||||
.type('Cypress Test Sync Group ' + testRun);
|
||||
|
||||
cy.get('#syncPublisherPort').clear();
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Check if syncgroup is added in toast message
|
||||
cy.contains('Sync Publisher Port cannot be empty');
|
||||
});
|
||||
});
|
||||
134
cypress/e2e/Layout/Editor/layout-action-menu.cy.js
Normal file
134
cypress/e2e/Layout/Editor/layout-action-menu.cy.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Layout Editor Toolbar (Back button, Interactive Mode, Layout jump list)', () => {
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
cy.intercept('GET', '/user/pref?preference=toolbar').as('toolbarPrefsLoad');
|
||||
cy.intercept('GET', '/user/pref?preference=editor').as('editorPrefsLoad');
|
||||
|
||||
cy.visit('/layout/view');
|
||||
cy.get('button.layout-add-button').click();
|
||||
cy.get('#layout-viewer').should('be.visible');
|
||||
cy.wait('@toolbarPrefsLoad');
|
||||
cy.wait('@editorPrefsLoad');
|
||||
});
|
||||
|
||||
it('Back button should be present and navigate correctly', () => {
|
||||
cy.get('#backBtn')
|
||||
.should('have.class', 'btn btn-lg')
|
||||
.and('have.attr', 'href', '/layout/view')
|
||||
.click({force: true});
|
||||
cy.url().should('include', '/layout/view');
|
||||
});
|
||||
|
||||
it('should display Interactive Mode with OFF status initially', () => { // done
|
||||
cy.get('li.interactive-control')
|
||||
.should('have.attr', 'data-status', 'off')
|
||||
.within(() => {
|
||||
cy.contains('.interactive-control-label', 'Interactive Mode');
|
||||
cy.get('.interactive-control-status-off').should('be.visible').and('contain.text', 'OFF');
|
||||
cy.get('.interactive-control-status-on').should('not.be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('should toggle Interactive Mode status on click', () => { // done
|
||||
cy.get('li.nav-item.interactive-control[data-status="off"]')
|
||||
.should(($el) => {
|
||||
expect($el).to.be.visible;
|
||||
})
|
||||
.click({force: true});
|
||||
cy.get('.interactive-control-status-off').should('not.be.visible');
|
||||
});
|
||||
|
||||
it.only('should open and close the layout jump list dropdown safely', () => {
|
||||
cy.intercept('GET', '/layout?onlyMyLayouts=*').as('onlyMyLayouts');
|
||||
|
||||
const layoutName = 'Audio-Video-PDF';
|
||||
|
||||
cy.get('#select2-layoutJumpList-container')
|
||||
.should('be.visible');
|
||||
|
||||
// Force click because the element intermittently detaches in CI environment
|
||||
cy.get('#layoutJumpListContainer .select2-selection')
|
||||
.should('be.visible')
|
||||
.click({force: true});
|
||||
|
||||
// Check for status
|
||||
cy.wait('@onlyMyLayouts').then((interception) => {
|
||||
const result = interception.response.body.data[0];
|
||||
cy.log('result:', result.layoutId);
|
||||
});
|
||||
|
||||
// Type into the search input
|
||||
cy.get('.select2-search__field')
|
||||
.should('be.visible')
|
||||
.clear()
|
||||
.type(layoutName, {delay: 100});
|
||||
|
||||
// Click the matching option
|
||||
cy.get('.select2-results__option')
|
||||
.contains(layoutName)
|
||||
.click();
|
||||
});
|
||||
|
||||
it('Options dropdown menu toggles and contains expected items', () => {
|
||||
cy.get('#optionsContainerTop').should('be.visible');
|
||||
cy.get('#optionsContainerTop').click({force: true});
|
||||
|
||||
cy.get('.navbar-submenu-options-container')
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
cy.get('#publishLayout').should('be.visible');
|
||||
cy.get('#checkoutLayout').should('have.class', 'd-none');
|
||||
cy.get('#discardLayout').should('be.visible');
|
||||
cy.get('#newLayout').should('be.visible');
|
||||
cy.get('#deleteLayout').should('have.class', 'd-none');
|
||||
cy.get('#saveTemplate').should('have.class', 'd-none');
|
||||
cy.get('#scheduleLayout').should('have.class', 'd-none');
|
||||
cy.get('#clearLayout').should('be.visible');
|
||||
cy.get('#displayTooltips').should('be.checked');
|
||||
cy.get('#deleteConfirmation').should('be.checked');
|
||||
});
|
||||
});
|
||||
|
||||
it('Tooltips and popovers appear on hover', () => {
|
||||
// Tooltip
|
||||
cy.get('.layout-info-name')
|
||||
.should('be.visible')
|
||||
.trigger('mouseover');
|
||||
cy.get('.tooltip').should('be.visible');
|
||||
cy.get('.layout-info-name')
|
||||
.should('be.visible')
|
||||
.trigger('mouseout');
|
||||
|
||||
// Popover
|
||||
cy.get('#layout-info-status')
|
||||
.should('be.visible')
|
||||
.trigger('mouseover');
|
||||
cy.get('.popover').should('be.visible');
|
||||
cy.get('#layout-info-status')
|
||||
.should('be.visible')
|
||||
.trigger('mouseout');
|
||||
});
|
||||
});
|
||||
85
cypress/e2e/Layout/Editor/layout_editor_background.cy.js
Normal file
85
cypress/e2e/Layout/Editor/layout_editor_background.cy.js
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Layout Editor Background', function() {
|
||||
const SELECTORS = {
|
||||
layoutAddButton: 'button.layout-add-button',
|
||||
layoutViewer: '#layout-viewer',
|
||||
propertiesPanel: '#properties-panel',
|
||||
colorPickerTrigger: '.input-group-prepend',
|
||||
colorPickerSaturation: '.colorpicker-saturation',
|
||||
backgroundColorInput: '#input_backgroundColor',
|
||||
backgroundzIndex: '#input_backgroundzIndex',
|
||||
resolutionDropdown: '#input_resolutionId',
|
||||
select2Selection: '.select2-selection',
|
||||
select2SearchInput: '.select2-container--open input[type="search"]',
|
||||
layoutInfoDimensions: '.layout-info-dimensions span',
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
cy.visit('/layout/view');
|
||||
cy.get(SELECTORS.layoutAddButton).click();
|
||||
cy.get(SELECTORS.layoutViewer).should('be.visible'); // Assert that the URL has changed to the layout editor
|
||||
});
|
||||
|
||||
it('should update the background according to the colour set via colour picker', function() {
|
||||
cy.get(SELECTORS.propertiesPanel).should('be.visible'); // Verify properties panel is present
|
||||
cy.get(SELECTORS.colorPickerTrigger).click(); // Open colour picker
|
||||
cy.get(SELECTORS.colorPickerSaturation).click(68, 28); // Select on a specific saturation
|
||||
cy.get(SELECTORS.propertiesPanel).click(30, 60); // Click outside color picker to close
|
||||
|
||||
// Verify the selected color is applied to the background
|
||||
cy.get(SELECTORS.layoutViewer).should('have.css', 'background-color', 'rgb(243, 248, 255)');
|
||||
});
|
||||
|
||||
it('should update the background according to the colour set via hex input', function() {
|
||||
cy.get(SELECTORS.propertiesPanel).should('be.visible');
|
||||
cy.get(SELECTORS.backgroundColorInput).clear().type('#b53939{enter}');
|
||||
|
||||
// Verify the selected color is applied to the background
|
||||
cy.get(SELECTORS.layoutViewer).should('have.css', 'background-color', 'rgb(243, 248, 255)');
|
||||
});
|
||||
|
||||
it('should update the layer according to the input', function() {
|
||||
cy.get(SELECTORS.propertiesPanel).should('be.visible');
|
||||
cy.get(SELECTORS.backgroundzIndex).clear().type('1{enter}');
|
||||
|
||||
// Verify the selected number is applied to the layer
|
||||
cy.get(SELECTORS.backgroundzIndex).should('have.value', '1');
|
||||
});
|
||||
|
||||
// This is failing and a bug reported
|
||||
it.skip('should update the layout resolution', function() {
|
||||
cy.get(SELECTORS.propertiesPanel).should('be.visible');
|
||||
const resName = 'cinema';
|
||||
|
||||
cy.get(SELECTORS.resolutionDropdown).parent().find(SELECTORS.select2Selection).click();
|
||||
cy.get(SELECTORS.select2SearchInput).type(resName);
|
||||
cy.selectOption(resName);
|
||||
|
||||
cy.get(SELECTORS.layoutInfoDimensions)
|
||||
.should('be.visible')
|
||||
.and('contain', '4096x2304');
|
||||
});
|
||||
});
|
||||
|
||||
164
cypress/e2e/Layout/Editor/layout_editor_empty.cy.js
Normal file
164
cypress/e2e/Layout/Editor/layout_editor_empty.cy.js
Normal file
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Layout Designer (Empty)', function() {
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
context('Unexisting Layout', function() {
|
||||
it('show layout not found if layout does not exist', function() {
|
||||
// Use a huge id to test a layout not found
|
||||
cy.visit({
|
||||
url: '/layout/designer/111111111111',
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
|
||||
// See page not found message
|
||||
cy.contains('Layout not found');
|
||||
});
|
||||
});
|
||||
|
||||
context('Empty layout (published)', function() {
|
||||
const layoutTempName = '';
|
||||
|
||||
beforeEach(function() {
|
||||
// Import a layout and go to the Layout's designer page - we need a Layout in a Published state
|
||||
cy.importLayout('../assets/export_test_layout.zip').as('testLayoutId').then((res) => {
|
||||
cy.goToLayoutAndLoadPrefs(res);
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('goes into draft mode when checked out', function() {
|
||||
// Get the done button from the checkout modal
|
||||
cy.get('[data-test="welcomeModal"] button.btn-bb-checkout').click();
|
||||
|
||||
// Check if campaign is deleted in toast message
|
||||
cy.contains('Checked out ' + layoutTempName);
|
||||
});
|
||||
|
||||
it.skip('should prevent a layout edit action, and show a toast message', function() {
|
||||
// Should contain widget options form
|
||||
cy.get('#properties-panel-form-container').contains('Edit Layout');
|
||||
|
||||
// The save button should not be visible
|
||||
cy.get('#properties-panel-form-container [data-action="save"]').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
context('Empty layout (draft)', function() {
|
||||
beforeEach(function() {
|
||||
// Create random name
|
||||
const uuid = Cypress._.random(0, 1e9);
|
||||
|
||||
// Create a new layout and go to the layout's designer page, then load toolbar prefs
|
||||
cy.createLayout(uuid).as('testLayoutId').then((res) => {
|
||||
cy.goToLayoutAndLoadPrefs(res);
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should create a new region from within the navigator edit', () => {
|
||||
// Open navigator edit
|
||||
cy.get('.editor-bottom-bar #navigator-edit-btn').click();
|
||||
|
||||
// Click on add region button
|
||||
cy.get('.editor-bottom-bar #add-btn').click();
|
||||
|
||||
// Check if there are 2 regions in the timeline ( there was 1 by default )
|
||||
cy.get('#layout-timeline [data-type="region"]').should('have.length', 2);
|
||||
});
|
||||
|
||||
it.skip('should delete a region using the toolbar bin', () => {
|
||||
cy.intercept('GET', '/layout?layoutId=*').as('reloadLayout');
|
||||
|
||||
// Open navigator edit
|
||||
cy.get('.editor-bottom-bar #navigator-edit-btn').click();
|
||||
|
||||
// Select a region from the navigator
|
||||
cy.get('#layout-navigator-content [data-type="region"]:first-child').click().then(($el) => {
|
||||
const regionId = $el.attr('id');
|
||||
|
||||
// Click trash container
|
||||
cy.get('.editor-bottom-bar #delete-btn').click();
|
||||
|
||||
// Confirm delete on modal
|
||||
cy.get('[data-test="deleteObjectModal"] button.btn-bb-confirm').click();
|
||||
|
||||
// Check toast message
|
||||
cy.get('.toast-success').contains('Deleted');
|
||||
|
||||
// Wait for the layout to reload
|
||||
cy.wait('@reloadLayout');
|
||||
|
||||
// Check that region is not on timeline
|
||||
cy.get('#layout-timeline [data-type="region"]#' + regionId).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('creates a new widget by selecting a searched media from the toolbar to layout-navigator region', () => {
|
||||
cy.populateLibraryWithMedia();
|
||||
|
||||
// Create and alias for reload Layout
|
||||
cy.intercept('GET', '/layout?layoutId=*').as('reloadLayout');
|
||||
cy.intercept('GET', '/library/search?*').as('mediaLoad');
|
||||
|
||||
// Open library search tab
|
||||
cy.get('.editor-main-toolbar #btn-menu-0').should('be.visible').click({force: true});
|
||||
cy.get('.editor-main-toolbar #btn-menu-1').should('be.visible').click({force: true});
|
||||
|
||||
cy.wait('@mediaLoad');
|
||||
|
||||
cy.get('.editor-bottom-bar #navigator-edit-btn').click({force: true});
|
||||
|
||||
cy.get('.editor-main-toolbar #media-content-1 .toolbar-card:nth-of-type(2)').find('img').should('be.visible');
|
||||
|
||||
// Get a table row, select it and add to the region
|
||||
cy.get('.editor-main-toolbar #media-content-1 .toolbar-card:nth-of-type(2) .select-button').click({force: true}).then(() => {
|
||||
cy.get('#layout-navigator [data-type="region"]:first-child').click().then(() => {
|
||||
// Wait for the layout to reload
|
||||
cy.wait('@reloadLayout');
|
||||
|
||||
// Check if there is just one widget in the timeline
|
||||
cy.get('#layout-timeline [data-type="region"] [data-type="widget"]').then(($widgets) => {
|
||||
expect($widgets.length).to.eq(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('shows the file upload form by adding a uploadable media from the toolbar to layout-navigator region', () => {
|
||||
cy.populateLibraryWithMedia();
|
||||
|
||||
// Open toolbar Widgets tab
|
||||
cy.get('.editor-main-toolbar #btn-menu-1').should('be.visible').click({force: true});
|
||||
cy.get('.editor-main-toolbar #btn-menu-2').should('be.visible').click({force: true});
|
||||
|
||||
cy.get('.editor-bottom-bar #navigator-edit-btn').click();
|
||||
|
||||
cy.get('.editor-main-toolbar #content-2 .toolbar-pane-content .toolbar-card.upload-card').should('be.visible').then(() => {
|
||||
cy.get('.editor-main-toolbar #content-2 .toolbar-pane-content .toolbar-card.upload-card .select-upload').click({force: true});
|
||||
cy.get('#layout-navigator [data-type="region"]:first-child').click({force: true});
|
||||
cy.get('[data-test="uploadFormModal"]').contains('Upload media');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
348
cypress/e2e/Layout/Editor/layout_editor_options.cy.js
Normal file
348
cypress/e2e/Layout/Editor/layout_editor_options.cy.js
Normal file
@@ -0,0 +1,348 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Layout Editor Options', function() {
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
cy.visit('/layout/view');
|
||||
});
|
||||
|
||||
it.skip('should be able to publish, checkout and discard layout', function() {
|
||||
let layoutName;
|
||||
|
||||
cy.intercept('GET', '/layout?layoutId=*').as('layoutStatus');
|
||||
cy.intercept('PUT', '/layout/discard/*').as('discardLayout');
|
||||
|
||||
cy.get('button.layout-add-button').click();
|
||||
cy.get('#layout-viewer').should('be.visible');
|
||||
|
||||
// Publish layout
|
||||
cy.openOptionsMenu();
|
||||
cy.get('#publishLayout').click();
|
||||
cy.get('button.btn-bb-Publish').click();
|
||||
|
||||
cy.wait('@layoutStatus').then((interception) => {
|
||||
expect(interception.response.statusCode).to.eq(200);
|
||||
// Check if the publishedStatus is "Published"
|
||||
const layoutData = interception.response.body.data[0];
|
||||
expect(layoutData).to.have.property('publishedStatus', 'Published');
|
||||
});
|
||||
|
||||
// Checkout published layout
|
||||
cy.openOptionsMenu();
|
||||
cy.get('#checkoutLayout').click();
|
||||
|
||||
cy.wait('@layoutStatus').then((interception) => {
|
||||
expect(interception.response.statusCode).to.eq(200);
|
||||
// Check if the publishedStatus is back to "Draft"
|
||||
const layoutData = interception.response.body.data[0];
|
||||
expect(layoutData).to.have.property('publishedStatus', 'Draft');
|
||||
});
|
||||
|
||||
// Capture layout name before discarding draft layout
|
||||
cy.get('.layout-info-name span')
|
||||
.invoke('text')
|
||||
.then((name) => {
|
||||
layoutName = name.trim().replace(/^"|"$/g, ''); // Remove double quotes
|
||||
cy.log(`Layout Name: ${layoutName}`);
|
||||
|
||||
cy.openOptionsMenu();
|
||||
cy.get('#discardLayout').click();
|
||||
cy.get('button.btn-bb-Discard').click();
|
||||
|
||||
// Verify that the layout has been discarded
|
||||
cy.wait('@discardLayout').then((interception) => {
|
||||
expect(interception.response.statusCode).to.equal(200);
|
||||
});
|
||||
|
||||
// Check if the user is redirected to the layouts page
|
||||
cy.url().should('include', '/layout/view');
|
||||
|
||||
// Search for the layout name
|
||||
cy.get('input[name="layout"]').clear().type(`${layoutName}{enter}`);
|
||||
|
||||
// Check status of the layout with matching layout name
|
||||
cy.get('#layouts tbody')
|
||||
.find('tr')
|
||||
.should('contain', layoutName)
|
||||
.should('contain', 'Published');
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should display an error when publishing an invalid layout', function() {
|
||||
cy.intercept('GET', '/playlist/widget/form/edit/*').as('addElement');
|
||||
cy.intercept('PUT', '/layout/publish/*').as('publishLayout');
|
||||
|
||||
cy.get('button.layout-add-button').click();
|
||||
cy.get('#layout-viewer').should('be.visible');
|
||||
|
||||
// Open widgets toolbox
|
||||
cy.openToolbarMenu(0, false);
|
||||
cy.get('[data-sub-type="ics-calendar"]').click();
|
||||
cy.get('[data-template-id="daily_light"]').click();
|
||||
cy.get('.viewer-object').click();
|
||||
|
||||
// Wait for element to be loaded on layout
|
||||
cy.wait('@addElement').then((interception) => {
|
||||
expect(interception.response.statusCode).to.eq(200);
|
||||
});
|
||||
|
||||
// Publish layout
|
||||
cy.openOptionsMenu();
|
||||
cy.get('#publishLayout').click();
|
||||
cy.get('button.btn-bb-Publish').click();
|
||||
|
||||
// Verify response
|
||||
cy.wait('@publishLayout').then((interception) => {
|
||||
expect(interception.response.statusCode).to.eq(200);
|
||||
expect(interception.response.body).to.have.property('message', 'There is an error with this Layout: Missing required property Feed URL');
|
||||
});
|
||||
|
||||
// Verify that a toast message is displayed
|
||||
cy.get('.toast-message')
|
||||
.should('be.visible')
|
||||
.and('contain.text', 'There is an error with this Layout');
|
||||
});
|
||||
|
||||
it.skip('should be able to create new layout', function() {
|
||||
cy.intercept('GET', '/layout?layoutId=*').as('newLayout');
|
||||
|
||||
cy.get('button.layout-add-button').click();
|
||||
cy.get('#layout-viewer').should('be.visible');
|
||||
|
||||
// Capture the layout ID of the initial layout loaded
|
||||
cy.get('#layout-editor')
|
||||
.invoke('attr', 'data-layout-id')
|
||||
.then((initialLayoutId) => {
|
||||
// Create new layout
|
||||
cy.wait(1000);
|
||||
cy.openOptionsMenu();
|
||||
cy.get('#newLayout').click();
|
||||
|
||||
cy.wait('@newLayout').then((interception) => {
|
||||
expect(interception.response.statusCode).to.eq(200); // Check if the request was successful
|
||||
|
||||
// Get the new layout ID
|
||||
cy.get('#layout-editor')
|
||||
.invoke('attr', 'data-layout-id')
|
||||
.then((newLayoutId) => {
|
||||
// Assert that the new layout ID is different from the initial layout ID
|
||||
expect(newLayoutId).to.not.eq(initialLayoutId);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should be able to unlock layout', function() {
|
||||
let layoutName;
|
||||
|
||||
cy.intercept('GET', '/layout?layoutId=*').as('checkLockStatus');
|
||||
cy.intercept('GET', '/playlist/widget/form/edit/*').as('addElement');
|
||||
|
||||
cy.get('button.layout-add-button').click();
|
||||
cy.get('#layout-viewer').should('be.visible');
|
||||
|
||||
// Capture layout name to navigate back to it after unlocking
|
||||
cy.get('.layout-info-name span')
|
||||
.invoke('text')
|
||||
.then((name) => {
|
||||
layoutName = name.trim().replace(/^"|"$/g, '');
|
||||
cy.log(`Layout Name: ${layoutName}`);
|
||||
|
||||
// Open global elements toolbox
|
||||
cy.openToolbarMenu(1, false);
|
||||
cy.get('[data-template-id="text"]').click();
|
||||
cy.get('.viewer-object').click();
|
||||
|
||||
// Wait for element to be loaded on layout
|
||||
cy.wait('@addElement').then((interception) => {
|
||||
expect(interception.response.statusCode).to.eq(200);
|
||||
});
|
||||
|
||||
// Check for lock status
|
||||
cy.wait('@checkLockStatus').then((interception) => {
|
||||
const isLocked = interception.response.body.data[0].isLocked;
|
||||
expect(isLocked).to.not.be.empty;
|
||||
cy.log('isLocked:', isLocked);
|
||||
});
|
||||
|
||||
cy.intercept('PUT', '/layout/lock/release/*').as('unlock');
|
||||
|
||||
// Unlock layout
|
||||
cy.wait(1000);
|
||||
cy.openOptionsMenu();
|
||||
cy.get('#unlockLayout').should('be.visible').click();
|
||||
cy.get('button.btn-bb-unlock').click();
|
||||
|
||||
// Wait for the release lock request to complete
|
||||
cy.wait('@unlock').then((interception) => {
|
||||
expect(interception.response.statusCode).to.equal(200);
|
||||
});
|
||||
|
||||
// Check if the user is redirected to the /layout/view page
|
||||
cy.url().should('include', '/layout/view');
|
||||
|
||||
// Search for the layout name
|
||||
cy.get('input[name="layout"]').clear().type(`${layoutName}{enter}`);
|
||||
cy.get('#layouts tbody tr').should('contain.text', layoutName);
|
||||
cy.get('#layouts tbody tr').should('have.length', 1);
|
||||
|
||||
cy.openRowMenu();
|
||||
cy.get('#layout_button_design').click();
|
||||
cy.get('#layout-viewer').should('be.visible');
|
||||
|
||||
// Check for lock status
|
||||
cy.wait('@checkLockStatus').then((interception) => {
|
||||
const isLocked = interception.response.body.data[0].isLocked;
|
||||
expect(isLocked).be.empty;
|
||||
cy.log('isLocked:', isLocked);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should enable tooltips', function() {
|
||||
cy.intercept('POST', '/user/pref').as('updatePreferences');
|
||||
|
||||
cy.get('button.layout-add-button').click();
|
||||
cy.get('#layout-viewer').should('be.visible');
|
||||
cy.openOptionsMenu();
|
||||
|
||||
// Enable tooltips
|
||||
// Check the current state of the tooltips checkbox
|
||||
cy.get('#displayTooltips').then(($checkbox) => {
|
||||
if (!$checkbox.is(':checked')) {
|
||||
// Check the checkbox if it is currently unchecked
|
||||
cy.wrap($checkbox).click();
|
||||
cy.wait('@updatePreferences');
|
||||
|
||||
// Confirm the checkbox is checked
|
||||
cy.get('#displayTooltips').should('be.checked');
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that tooltips are present
|
||||
cy.get('.navbar-nav .btn-menu-option[data-toggle="tooltip"]').each(($element) => {
|
||||
// Trigger hover to show tooltip
|
||||
cy.wrap($element).trigger('mouseover');
|
||||
|
||||
// Check that the tooltip is visible for each button
|
||||
cy.get('.tooltip').should('be.visible'); // Expect tooltip to be present
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should disable tooltips', function() {
|
||||
cy.intercept('POST', '/user/pref').as('updatePreferences');
|
||||
|
||||
cy.get('button.layout-add-button').click();
|
||||
cy.get('#layout-viewer').should('be.visible');
|
||||
cy.openOptionsMenu();
|
||||
|
||||
// Disable tooltips
|
||||
// Check the current state of the tooltips checkbox
|
||||
cy.get('#displayTooltips').then(($checkbox) => {
|
||||
if ($checkbox.is(':checked')) {
|
||||
// Uncheck the checkbox if it is currently checked
|
||||
cy.wrap($checkbox).click();
|
||||
cy.wait('@updatePreferences');
|
||||
|
||||
// Confirm the checkbox is now unchecked
|
||||
cy.get('#displayTooltips').should('not.be.checked');
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that tooltips are gone
|
||||
cy.get('.navbar-nav .btn-menu-option[data-toggle="tooltip"]').each(($element) => {
|
||||
cy.wrap($element).trigger('mouseover'); // Trigger hover to show tooltip
|
||||
cy.get('.tooltip').should('not.exist'); // Check if tooltip is gone for each button on the toolbox
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should enable delete confirmation', function() {
|
||||
cy.intercept('POST', '/user/pref').as('updatePreferences');
|
||||
|
||||
cy.get('button.layout-add-button').click();
|
||||
cy.get('#layout-viewer').should('be.visible');
|
||||
cy.openOptionsMenu();
|
||||
|
||||
// Check the current state of the delete confirmation checkbox
|
||||
cy.get('#deleteConfirmation').then(($checkbox) => {
|
||||
if (!$checkbox.is(':checked')) {
|
||||
// Check the checkbox if it is currently unchecked
|
||||
cy.wrap($checkbox).click();
|
||||
cy.wait('@updatePreferences');
|
||||
|
||||
// Confirm the checkbox is checked
|
||||
cy.get('#deleteConfirmation').should('be.checked');
|
||||
}
|
||||
});
|
||||
|
||||
// Add an element then attempt to delete
|
||||
cy.openToolbarMenu(0, false);
|
||||
cy.get('[data-sub-type="clock"]').click();
|
||||
cy.get('[data-sub-type="clock-analogue"]').click();
|
||||
cy.get('.viewer-object').click();
|
||||
cy.get('#delete-btn').click();
|
||||
|
||||
// Verify that delete confirmation modal appears
|
||||
cy.get('.modal-content')
|
||||
.should('be.visible')
|
||||
.and('contain.text', 'Delete Widget');
|
||||
});
|
||||
|
||||
it.skip('should disable delete confirmation', function() {
|
||||
cy.intercept('POST', '/user/pref').as('updatePreferences');
|
||||
|
||||
cy.get('button.layout-add-button').click();
|
||||
cy.get('#layout-viewer').should('be.visible');
|
||||
cy.openOptionsMenu();
|
||||
|
||||
// Check the current state of the delete confirmation checkbox
|
||||
cy.get('#deleteConfirmation').then(($checkbox) => {
|
||||
if ($checkbox.is(':checked')) {
|
||||
// Uncheck the checkbox if it is currently checked
|
||||
cy.wrap($checkbox).click();
|
||||
cy.wait('@updatePreferences');
|
||||
|
||||
// Confirm the checkbox is now unchecked
|
||||
cy.get('#displayTooltips').should('not.be.checked');
|
||||
}
|
||||
});
|
||||
|
||||
cy.intercept('DELETE', '/region/*').as('deleteElement');
|
||||
|
||||
// Add an element then attempt to delete
|
||||
cy.openToolbarMenu(0, false);
|
||||
cy.get('[data-sub-type="clock"]').click();
|
||||
cy.get('[data-sub-type="clock-analogue"]').click();
|
||||
cy.get('.viewer-object').click();
|
||||
cy.get('#delete-btn').click();
|
||||
|
||||
// Verify that the widget is immediately deleted without confirmation
|
||||
cy.wait('@deleteElement').then((interception) => {
|
||||
expect(interception.response.statusCode).to.equal(200);
|
||||
});
|
||||
|
||||
cy.get('.viewer-object').within(() => {
|
||||
cy.get('[data-type="region"]').should('not.exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
240
cypress/e2e/Layout/Editor/layout_editor_populated.cy.js
Normal file
240
cypress/e2e/Layout/Editor/layout_editor_populated.cy.js
Normal file
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Layout Designer (Populated)', function() {
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
// Import existing
|
||||
cy.importLayout('../assets/export_test_layout.zip').as('testLayoutId').then((res) => {
|
||||
cy.checkoutLayout(res);
|
||||
|
||||
cy.goToLayoutAndLoadPrefs(res);
|
||||
});
|
||||
});
|
||||
|
||||
// Open widget form, change the name and duration, save, and see the name change result
|
||||
it.skip('changes and saves widget properties', () => {
|
||||
// Create and alias for reload widget
|
||||
cy.intercept('GET', '/playlist/widget/form/edit/*').as('reloadWidget');
|
||||
|
||||
// Select the first widget from the first region on timeline ( image )
|
||||
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').click();
|
||||
|
||||
// Type the new name in the input
|
||||
cy.get('#properties-panel input[name="name"]').clear().type('newName');
|
||||
|
||||
// Set a duration
|
||||
cy.get('#properties-panel #useDuration').check();
|
||||
cy.get('#properties-panel input[name="duration"]').clear().type(12);
|
||||
|
||||
// Save form
|
||||
cy.get('#properties-panel button[data-action="save"]').click();
|
||||
|
||||
// Should show a notification for the name change
|
||||
cy.get('.toast-success').contains('newName');
|
||||
|
||||
// Check if the values are the same entered after reload
|
||||
cy.wait('@reloadWidget').then(() => {
|
||||
cy.get('#properties-panel input[name="name"]').should('have.attr', 'value').and('equal', 'newName');
|
||||
cy.get('#properties-panel input[name="duration"]').should('have.attr', 'value').and('equal', '12');
|
||||
});
|
||||
});
|
||||
|
||||
// On layout edit form, change background color and layer, save and check the changes
|
||||
it.skip('changes and saves layout properties', () => {
|
||||
// Create and alias for reload layout
|
||||
|
||||
cy.intercept('GET', '/layout?layoutId=*').as('reloadLayout');
|
||||
|
||||
// Change background color
|
||||
cy.get('#properties-panel input[name="backgroundColor"]').clear().type('#ccc');
|
||||
|
||||
// Change layer
|
||||
cy.get('#properties-panel input[name="backgroundzIndex"]').clear().type(1);
|
||||
|
||||
// Save form
|
||||
cy.get('#properties-panel button[data-action="save"]').click();
|
||||
|
||||
// Should show a notification for the successful save
|
||||
cy.get('.toast-success').contains('Edited');
|
||||
|
||||
// Check if the values are the same entered after reload
|
||||
cy.wait('@reloadLayout').then(() => {
|
||||
cy.get('#properties-panel input[name="backgroundColor"]').should('have.attr', 'value').and('equal', '#cccccc');
|
||||
cy.get('#properties-panel input[name="backgroundzIndex"]').should('have.value', '1');
|
||||
});
|
||||
});
|
||||
|
||||
// On layout edit form, change background image check the changes
|
||||
it.skip('should change layout´s background image', () => {
|
||||
// Create and alias for reload layout
|
||||
|
||||
cy.intercept('GET', '/layout?layoutId=*').as('reloadLayout');
|
||||
cy.intercept('GET', '/library/search?*').as('mediaLoad');
|
||||
|
||||
cy.get('#properties-panel #backgroundRemoveButton').click();
|
||||
|
||||
// Open library search tab
|
||||
cy.get('.editor-main-toolbar #btn-menu-0').click();
|
||||
cy.get('.editor-main-toolbar #btn-menu-1').click();
|
||||
|
||||
cy.wait('@mediaLoad');
|
||||
|
||||
cy.get('.editor-bottom-bar #navigator-edit-btn').click();
|
||||
|
||||
cy.get('.editor-main-toolbar #media-content-1 .toolbar-card:nth-of-type(2)').find('img').should('be.visible');
|
||||
|
||||
// Get a table row, select it and add to the region
|
||||
cy.get('.editor-main-toolbar #media-content-1 .toolbar-card:nth-of-type(2) .select-button').click({force: true}).then(() => {
|
||||
cy.get('#properties-panel-form-container .background-image-drop').click().then(() => {
|
||||
// Save form
|
||||
cy.get('#properties-panel button[data-action="save"]').click();
|
||||
|
||||
// Should show a notification for the successful save
|
||||
cy.get('.toast-success').contains('Edited');
|
||||
|
||||
// Check if the background field has an image
|
||||
cy.get('#properties-panel .background-image-add img#bg_image_image').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Navigator
|
||||
it.skip('should change and save the region´s position', () => {
|
||||
// Create and alias for position save and reload layout
|
||||
|
||||
cy.intercept('GET', '/layout?layoutId=*').as('reloadLayout');
|
||||
cy.intercept('GET', '/region/form/edit/*').as('reloadRegion');
|
||||
cy.intercept('GET', '**/region/preview/*').as('regionPreview');
|
||||
|
||||
// Open navigator edit
|
||||
cy.get('.editor-bottom-bar #navigator-edit-btn').click();
|
||||
|
||||
// Wait for the region to preview
|
||||
cy.wait('@regionPreview');
|
||||
|
||||
cy.get('#layout-navigator [data-type="region"]:first').then(($originalRegion) => {
|
||||
const regionId = $originalRegion.attr('id');
|
||||
|
||||
// Select region
|
||||
cy.get('#layout-navigator-content #' + regionId).click();
|
||||
|
||||
// Move region 50px for each dimension
|
||||
cy.get('#layout-navigator-content #' + regionId).then(($movedRegion) => {
|
||||
const regionOriginalPosition = {
|
||||
top: Math.round($movedRegion.position().top),
|
||||
left: Math.round($movedRegion.position().left),
|
||||
};
|
||||
|
||||
const offsetToAdd = 50;
|
||||
|
||||
// Move the region
|
||||
cy.get('#layout-navigator-content #' + regionId)
|
||||
.trigger('mousedown', {
|
||||
which: 1,
|
||||
})
|
||||
.trigger('mousemove', {
|
||||
which: 1,
|
||||
pageX: $movedRegion.width() / 2 + $movedRegion.offset().left + offsetToAdd,
|
||||
pageY: $movedRegion.height() / 2 + $movedRegion.offset().top + offsetToAdd,
|
||||
})
|
||||
.trigger('mouseup');
|
||||
|
||||
// Close the navigator edit
|
||||
cy.wait('@reloadRegion');
|
||||
|
||||
// Save
|
||||
cy.get('#properties-panel button#save').click();
|
||||
|
||||
// Wait for the layout to reload
|
||||
cy.wait('@reloadLayout');
|
||||
|
||||
// Check if the region´s position are not the original
|
||||
cy.get('#layout-navigator-content #' + regionId).then(($changedRegion) => {
|
||||
expect(Math.round($changedRegion.position().top)).to.not.eq(regionOriginalPosition.top);
|
||||
expect(Math.round($changedRegion.position().left)).to.not.eq(regionOriginalPosition.left);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should delete a widget using the toolbar bin', () => {
|
||||
cy.intercept('GET', '/layout?layoutId=*').as('reloadLayout');
|
||||
cy.intercept('GET', '/region/preview/*').as('regionPreview');
|
||||
|
||||
// Select a widget from the timeline
|
||||
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').click().then(($el) => {
|
||||
const widgetId = $el.attr('id');
|
||||
|
||||
// Wait for the widget to be loaded
|
||||
cy.wait('@regionPreview');
|
||||
|
||||
// Click trash container
|
||||
cy.get('.editor-bottom-bar button#delete-btn').click({force: true});
|
||||
|
||||
// Confirm delete on modal
|
||||
cy.get('[data-test="deleteObjectModal"] button.btn-bb-confirm').click();
|
||||
|
||||
// Check toast message
|
||||
cy.get('.toast-success').contains('Deleted');
|
||||
|
||||
// Wait for the layout to reload
|
||||
cy.wait('@reloadLayout');
|
||||
|
||||
// Check that widget is not on timeline
|
||||
cy.get('#layout-timeline [data-type="widget"]#' + widgetId).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('saves the widgets order when sorting by dragging', () => {
|
||||
cy.intercept('GET', 'POST', '**/playlist/order/*').as('saveOrder');
|
||||
cy.intercept('GET', '/layout?layoutId=*').as('reloadLayout');
|
||||
|
||||
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').then(($oldWidget) => {
|
||||
const offsetX = 50;
|
||||
|
||||
// Move to the second widget position ( plus offset )
|
||||
cy.wrap($oldWidget)
|
||||
.trigger('mousedown', {
|
||||
which: 1,
|
||||
})
|
||||
.trigger('mousemove', {
|
||||
which: 1,
|
||||
pageX: $oldWidget.offset().left + $oldWidget.width() * 1.5 + offsetX,
|
||||
})
|
||||
.trigger('mouseup', {force: true});
|
||||
|
||||
cy.wait('@saveOrder');
|
||||
|
||||
// Should show a notification for the order change
|
||||
cy.get('.toast-success').contains('Order Changed');
|
||||
|
||||
// Reload layout and check if the new first widget has a different Id
|
||||
cy.wait('@reloadLayout');
|
||||
|
||||
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').then(($newWidget) => {
|
||||
expect($oldWidget.attr('id')).not.to.eq($newWidget.attr('id'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
70
cypress/e2e/Layout/Editor/layout_editor_status_bar.cy.js
Normal file
70
cypress/e2e/Layout/Editor/layout_editor_status_bar.cy.js
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Layout Editor Status Bar', function() {
|
||||
const layoutStatusSelector = '#layout-info-status';
|
||||
const layoutNameSelector = '.layout-info-name span';
|
||||
const layoutDurationSelector = '.layout-info-duration .layout-info-duration-value';
|
||||
const layoutDimensionsSelector = '.layout-info-dimensions span';
|
||||
const tooltipSelector = '.popover';
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
cy.visit('/layout/view');
|
||||
cy.get('button.layout-add-button').click();
|
||||
cy.get('#layout-viewer').should('be.visible');
|
||||
});
|
||||
|
||||
it('should display the correct Layout status icon and tooltip', function() {
|
||||
cy.get(layoutStatusSelector)
|
||||
.should('be.visible')
|
||||
.and('have.class', 'badge-danger')
|
||||
.trigger('mouseover');
|
||||
|
||||
cy.get(tooltipSelector)
|
||||
.should('be.visible')
|
||||
.and('contain', 'This Layout is invalid');
|
||||
|
||||
cy.get(layoutStatusSelector).trigger('mouseout');
|
||||
});
|
||||
|
||||
it('should display the correct Layout name', () => {
|
||||
// Verify the Layout name text
|
||||
cy.get(layoutNameSelector)
|
||||
.should('be.visible')
|
||||
.and('contain', 'Untitled');
|
||||
});
|
||||
|
||||
it('should display the correct Layout duration', () => {
|
||||
// Verify the duration is correctly displayed
|
||||
cy.get(layoutDurationSelector)
|
||||
.should('be.visible')
|
||||
.and('contain', '00:00');
|
||||
});
|
||||
|
||||
it('should display the correct Layout dimensions', () => {
|
||||
// Verify the dimensions are correctly displayed
|
||||
cy.get(layoutDimensionsSelector)
|
||||
.should('be.visible')
|
||||
.and('contain', '1920x1080');
|
||||
});
|
||||
});
|
||||
181
cypress/e2e/Layout/Editor/layout_editor_toolbar.cy.js
Normal file
181
cypress/e2e/Layout/Editor/layout_editor_toolbar.cy.js
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Layout Editor Toolbar', function() {
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it.skip('should expand and close the toolbox', function() {
|
||||
cy.visit('/layout/view');
|
||||
cy.get('button[href="/layout"]').click();
|
||||
|
||||
cy.openToolbarMenu(0);
|
||||
cy.get('.close-content').filter(':visible').click();
|
||||
});
|
||||
|
||||
const setZoomLevel = (level) => {
|
||||
cy.intercept('POST', '/user/pref').as('updatePreferences');
|
||||
|
||||
cy.openToolbarMenu(0);
|
||||
|
||||
cy.get('.toolbar-level-control-menu').click();
|
||||
cy.get('nav.navbar').then(($toolbar) => {
|
||||
if ($toolbar.hasClass(`toolbar-level-${level}`)) return;
|
||||
cy.get(`i[data-level="${level}"]`).click();
|
||||
cy.wait('@updatePreferences');
|
||||
});
|
||||
cy.get('nav.navbar').should('have.class', `toolbar-level-${level}`);
|
||||
};
|
||||
|
||||
it.skip('should be able to set zoom level to 1', function() {
|
||||
cy.visit('/layout/view');
|
||||
cy.get('button[href="/layout"]').click();
|
||||
setZoomLevel(1);
|
||||
});
|
||||
|
||||
it.skip('should be able to set zoom level to 2', function() {
|
||||
cy.visit('/layout/view');
|
||||
cy.get('button[href="/layout"]').click();
|
||||
setZoomLevel(2);
|
||||
});
|
||||
|
||||
function searchAndAddElement(tabIndex, keyword, elementSelector, subTypeSelector, paneSelector) {
|
||||
cy.intercept('POST', '/region/*').as('addRegion');
|
||||
|
||||
// Search for the element
|
||||
cy.toolbarSearch(keyword);
|
||||
cy.get(paneSelector + '.active')
|
||||
.find('.toolbar-pane-content')
|
||||
.find('.toolbar-card')
|
||||
.should('have.length.greaterThan', 0)
|
||||
.each(($card) => {
|
||||
cy.wrap($card)
|
||||
.find('.card-title')
|
||||
.should('include.text', keyword);
|
||||
});
|
||||
|
||||
// Add the widget to layout
|
||||
cy.get(elementSelector).click();
|
||||
if (subTypeSelector) {
|
||||
cy.get(subTypeSelector).click();
|
||||
}
|
||||
cy.get('.viewer-object').click();
|
||||
cy.wait('@addRegion').then((interception) => { // todo: error here
|
||||
expect(interception.response.statusCode).to.eq(200);
|
||||
});
|
||||
}
|
||||
|
||||
it.skip('should navigate to Widgets tab, search and add a widget', function() {
|
||||
|
||||
cy.visit('/layout/view');
|
||||
cy.get('button[href="/layout"]').click();
|
||||
|
||||
// Open the respective toolbar tab
|
||||
cy.openToolbarMenu(0);
|
||||
|
||||
searchAndAddElement(0, 'Clock', '[data-sub-type="clock"]', '[data-sub-type="clock-analogue"]', '.toolbar-widgets-pane');
|
||||
});
|
||||
|
||||
it.skip('should navigate to Global Elements tab, search and add an element', function() {
|
||||
|
||||
cy.visit('/layout/view');
|
||||
cy.get('button[href="/layout"]').click();
|
||||
|
||||
searchAndAddElement(1, 'Text', '[data-template-id="text"]', '', '.toolbar-global-pane');
|
||||
});
|
||||
|
||||
function testLibrarySearchAndAddMedia(mediaType, tabIndex, keyword, folderName, mediaTitle) {
|
||||
cy.intercept('POST', '/user/pref').as('updatePreferences');
|
||||
cy.intercept('GET', '/folders?start=0&length=10').as('loadFolders');
|
||||
cy.intercept('GET', '/library/search*').as('librarySearch');
|
||||
cy.intercept('POST', '/region/*').as('addRegion');
|
||||
|
||||
cy.openToolbarMenu(tabIndex);
|
||||
|
||||
// Conditionally filter media by Folder if folderName is provided
|
||||
if (folderName) {
|
||||
cy.get(`.toolbar-pane.toolbar-${mediaType}-pane.active`)
|
||||
.find('#input-folder')
|
||||
.parent()
|
||||
.find('.select2-selection')
|
||||
.click();
|
||||
cy.wait('@loadFolders');
|
||||
cy.get('.select2-container--open')
|
||||
.contains(folderName)
|
||||
.click();
|
||||
cy.wait('@updatePreferences');
|
||||
cy.wait('@librarySearch');
|
||||
}
|
||||
|
||||
// Search for a media
|
||||
cy.toolbarSearchWithActiveFilter(keyword);
|
||||
cy.get(`.toolbar-pane.toolbar-${mediaType}-pane.active`)
|
||||
.find('.toolbar-pane-content')
|
||||
.find('.toolbar-card[data-type="media"]')
|
||||
.each(($card) => {
|
||||
cy.wrap($card)
|
||||
.find('span.media-title')
|
||||
.should('include.text', keyword);
|
||||
});
|
||||
|
||||
cy.wait('@librarySearch');
|
||||
cy.wait('@updatePreferences');
|
||||
|
||||
// Add media to layout
|
||||
cy.get(`.toolbar-pane.toolbar-${mediaType}-pane.active`)
|
||||
.find(`[data-card-title="${mediaTitle}"]`)
|
||||
.should('exist')
|
||||
.click();
|
||||
cy.get('.viewer-object').click();
|
||||
cy.wait('@addRegion');
|
||||
}
|
||||
|
||||
// Test cases
|
||||
it.skip('should navigate to Library Image Search tab, filter, search and add media', function() {
|
||||
testLibrarySearchAndAddMedia('image', 2, 'media_for_search', 'FolderWithImage', 'media_for_search_in_folder');
|
||||
});
|
||||
|
||||
it.skip('should navigate to Library Audio Search tab, filter, search and add media', function() {
|
||||
testLibrarySearchAndAddMedia('audio', 3, 'test-audio', null, 'test-audio.mp3');
|
||||
});
|
||||
|
||||
it.skip('should navigate to Library Video Search tab, filter, search and add media', function() {
|
||||
testLibrarySearchAndAddMedia('video', 4, 'test-video', null, 'test-video.mp4');
|
||||
});
|
||||
|
||||
it.skip('should navigate to Interactive Actions tab and search for actions', function() {
|
||||
const keyword = 'Next';
|
||||
|
||||
cy.visit('/layout/view');
|
||||
cy.get('button[href="/layout"]').click();
|
||||
|
||||
cy.openToolbarMenu(7, false);
|
||||
cy.toolbarSearch(keyword);
|
||||
cy.get('.toolbar-pane.toolbar-actions-pane.active')
|
||||
.find('.toolbar-pane-content .toolbar-card')
|
||||
.each(($card) => {
|
||||
cy.wrap($card).find('.card-title').should('include.text', keyword);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
168
cypress/e2e/Layout/Editor/layout_editor_unchanged.cy.js
Normal file
168
cypress/e2e/Layout/Editor/layout_editor_unchanged.cy.js
Normal file
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
describe('Layout Designer (Populated/Unchanged)', function() {
|
||||
before(function() {
|
||||
// Import existing
|
||||
// cy.importLayout('../assets/export_test_layout.zip').as('testLayoutId').then((res) => {
|
||||
// cy.checkoutLayout(res);
|
||||
// });
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
// cy.goToLayoutAndLoadPrefs(this.testLayoutId);
|
||||
});
|
||||
|
||||
it.skip('should load all the layout designer elements', function() {
|
||||
// Check if the basic elements of the designer loaded
|
||||
cy.get('#layout-editor').should('be.visible');
|
||||
cy.get('.timeline-panel').should('be.visible');
|
||||
cy.get('#layout-viewer-container').should('be.visible');
|
||||
cy.get('#properties-panel').should('be.visible');
|
||||
});
|
||||
|
||||
it.skip('shows widget properties in the properties panel when clicking on a widget in the timeline', function() {
|
||||
// Select the first widget from the first region on timeline ( image )
|
||||
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').click();
|
||||
|
||||
// Check if the properties panel title is Edit Image
|
||||
cy.get('#properties-panel').contains('Edit Image');
|
||||
});
|
||||
|
||||
it.skip('should open the playlist editor and be able to show modals', function() {
|
||||
cy.intercept('GET', '/playlist/widget/form/edit/*').as('reloadWidget');
|
||||
|
||||
// Open the playlist editor
|
||||
cy.get('#layout-timeline .designer-region-info:first .open-playlist-editor').click();
|
||||
|
||||
// Wait for the widget to load
|
||||
cy.wait('@reloadWidget');
|
||||
|
||||
// Right click on the first widget in the playlist editor
|
||||
cy.get('.editor-modal #timeline-container .playlist-widget:first').rightclick();
|
||||
|
||||
// Open the delete modal for the first widget
|
||||
cy.get('.context-menu-overlay .context-menu-widget .deleteBtn').should('be.visible').click();
|
||||
|
||||
// Modal should be visible
|
||||
cy.get('[data-test="deleteObjectModal"]').should('be.visible');
|
||||
});
|
||||
|
||||
it.skip('should revert a saved form to a previous state', () => {
|
||||
let oldName;
|
||||
|
||||
// Create and alias for reload widget
|
||||
|
||||
cy.intercept('GET', '/playlist/widget/form/edit/*').as('reloadWidget');
|
||||
cy.intercept('PUT', '/playlist/widget/*').as('saveWidget');
|
||||
|
||||
// Select the first widget on timeline ( image )
|
||||
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').click();
|
||||
|
||||
// Wait for the widget to load
|
||||
cy.wait('@reloadWidget');
|
||||
|
||||
// Get the input field
|
||||
cy.get('#properties-panel input[name="name"]').then(($input) => {
|
||||
// Save old name
|
||||
oldName = $input.val();
|
||||
|
||||
// Type the new name in the input
|
||||
cy.get('#properties-panel input[name="name"]').clear().type('newName');
|
||||
|
||||
// Save form
|
||||
cy.get('#properties-panel button[data-action="save"]').click();
|
||||
|
||||
// Should show a notification for the name change
|
||||
cy.get('.toast-success');
|
||||
|
||||
// Wait for the widget to save
|
||||
cy.wait('@reloadWidget');
|
||||
|
||||
// Click the revert button
|
||||
cy.get('.editor-bottom-bar #undo-btn').click();
|
||||
|
||||
// Wait for the widget to save
|
||||
cy.wait('@saveWidget');
|
||||
|
||||
// Test if the revert made the name go back to the old name
|
||||
cy.get('#properties-panel input[name="name"]').should('have.attr', 'value').and('equal', oldName);
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should revert the widgets order when using the undo feature', () => {
|
||||
cy.intercept('POST', '**/playlist/order/*').as('saveOrder');
|
||||
cy.intercept('GET', '/layout?layoutId=*').as('reloadLayout');
|
||||
|
||||
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').then(($oldWidget) => {
|
||||
const offsetX = 50;
|
||||
|
||||
// Move to the second widget position ( plus offset )
|
||||
cy.wrap($oldWidget)
|
||||
.trigger('mousedown', {
|
||||
which: 1,
|
||||
})
|
||||
.trigger('mousemove', {
|
||||
which: 1,
|
||||
pageX: $oldWidget.offset().left + $oldWidget.width() * 1.5 + offsetX,
|
||||
})
|
||||
.trigger('mouseup', {force: true});
|
||||
|
||||
cy.wait('@saveOrder');
|
||||
|
||||
// Should show a notification for the order change
|
||||
cy.get('.toast-success').contains('Order Changed');
|
||||
|
||||
// Reload layout and check if the new first widget has a different Id
|
||||
cy.wait('@reloadLayout');
|
||||
|
||||
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').then(($newWidget) => {
|
||||
expect($oldWidget.attr('id')).not.to.eq($newWidget.attr('id'));
|
||||
});
|
||||
|
||||
// Click the revert button
|
||||
cy.get('.editor-bottom-bar #undo-btn').click();
|
||||
|
||||
// Wait for the order to save
|
||||
cy.wait('@saveOrder');
|
||||
cy.wait('@reloadLayout');
|
||||
|
||||
// Test if the revert made the name go back to the first widget
|
||||
cy.get('#layout-timeline .designer-region:first [data-type="widget"]:first-child').then(($newWidget) => {
|
||||
expect($oldWidget.attr('id')).to.eq($newWidget.attr('id'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should play a preview in the viewer', () => {
|
||||
cy.intercept('GET', '**/region/preview/*').as('loadRegion');
|
||||
// Wait for the viewer and region to load
|
||||
cy.get('#layout-viewer-container .viewer-object.layout-player').should('be.visible');
|
||||
cy.wait('@loadRegion');
|
||||
|
||||
// Click play
|
||||
cy.get('.editor-bottom-bar #play-btn').click();
|
||||
|
||||
// Check if the fullscreen iframe has loaded
|
||||
cy.get('#layout-viewer-container #layout-viewer .viewer-object > iframe').should('be.visible');
|
||||
});
|
||||
});
|
||||
56
cypress/e2e/Layout/IA/toggle_mode_on_off.cy.js
Normal file
56
cypress/e2e/Layout/IA/toggle_mode_on_off.cy.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
describe('Test IA: Toggle Mode ON/OFF', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
|
||||
// Navigate to Layouts page
|
||||
cy.visit('/layout/view');
|
||||
|
||||
//Click the Add Layout button
|
||||
cy.get('button.layout-add-button').click();
|
||||
cy.get('#layout-viewer').should('be.visible');
|
||||
});
|
||||
|
||||
it('should verify default status = OFF and checks the status of IA Mode when toggled to ON or OFF', () => {
|
||||
|
||||
//check default IA Mode = OFF
|
||||
cy.get('li.nav-item.interactive-control')
|
||||
.should('have.attr', 'data-status', 'off')
|
||||
.then(($el) => {
|
||||
cy.wrap($el).click({ force: true })
|
||||
})
|
||||
|
||||
//Toggle Mode = ON
|
||||
cy.get('li.nav-item.interactive-control')
|
||||
.should('have.attr', 'data-status', 'on')
|
||||
.and('contain.text', 'ON')
|
||||
|
||||
//Toggle OFF back to Layout Editor
|
||||
cy.get('li.nav-item.interactive-control').click({ force: true })
|
||||
cy.get('li.nav-item.interactive-control')
|
||||
.should('have.attr', 'data-status', 'off')
|
||||
.and('contain.text', 'OFF')
|
||||
});
|
||||
});
|
||||
97
cypress/e2e/Layout/Widget/layout_editor_clock.cy.js
Normal file
97
cypress/e2e/Layout/Widget/layout_editor_clock.cy.js
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Clock Analogue Widget', function() {
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('should create a new layout and be redirected to the layout designer, add/delete analogue clock', function() {
|
||||
cy.intercept('/playlist/widget/*').as('saveWidget');
|
||||
cy.intercept('DELETE', '**/region/**').as('deleteWidget');
|
||||
cy.intercept('POST', '/user/pref').as('userPref');
|
||||
|
||||
cy.visit('/layout/view');
|
||||
cy.get('button[href="/layout"]').click();
|
||||
|
||||
// Open widget menu
|
||||
cy.openToolbarMenu(0);
|
||||
|
||||
cy.get('[data-sub-type="clock"]')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.wait('@userPref');
|
||||
|
||||
cy.get('[data-sub-type="clock-analogue"] > .toolbar-card-thumb')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.wait('@userPref');
|
||||
|
||||
cy.get('.viewer-object.layout.ui-droppable-active')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
|
||||
// Check if the widget is in the viewer
|
||||
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_clock-analogue"]').should('exist');
|
||||
|
||||
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();
|
||||
|
||||
// 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.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-object').click({force: true});
|
||||
cy.get('[name="backgroundColor"]').clear().type('#ffffff').trigger('change');
|
||||
|
||||
// Validate background color changed wo white
|
||||
cy.get('.viewer-object').should('have.css', 'background-color', 'rgb(255, 255, 255)');
|
||||
|
||||
// Check if the name and duration values are the same entered
|
||||
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_clock-analogue"]').parents('.designer-region').click();
|
||||
cy.get('.widget-form .nav-link[href="#advancedTab"]').click();
|
||||
cy.get('#advancedTab input[name="name"]').should('have.attr', 'value').and('equal', 'newName');
|
||||
cy.get('#advancedTab input[name="duration"]').should('have.attr', 'value').and('equal', '12');
|
||||
|
||||
// Delete
|
||||
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_clock-analogue"]')
|
||||
.parents('.designer-region')
|
||||
.rightclick();
|
||||
|
||||
// todo -investigate further why this is not working in ci/cdk mode
|
||||
// cy.get('[data-title="Delete"]').click().then(() => {
|
||||
// cy.wait('@deleteWidget').its('response.statusCode').should('eq', 200);
|
||||
// cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_clock-analogue"]')
|
||||
// .should('not.exist');
|
||||
// });
|
||||
});
|
||||
});
|
||||
103
cypress/e2e/Layout/Widget/layout_editor_dataset.cy.js
Normal file
103
cypress/e2e/Layout/Widget/layout_editor_dataset.cy.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Dataset', function() {
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('should create a new layout, add/delete dataset widget', function() {
|
||||
cy.intercept('/dataset?start=*').as('loadDatasets');
|
||||
cy.intercept('DELETE', '**/region/**').as('deleteWidget');
|
||||
cy.intercept('POST', '/user/pref').as('userPref');
|
||||
|
||||
cy.visit('/layout/view');
|
||||
cy.get('button[href="/layout"]').click();
|
||||
|
||||
// Open widget menu and add dataset widget
|
||||
cy.openToolbarMenu(0);
|
||||
|
||||
cy.get('[data-sub-type="dataset"]')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.wait('@userPref');
|
||||
|
||||
cy.get('[data-template-id="dataset_table_1"]')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.wait('@userPref');
|
||||
|
||||
cy.get('.viewer-object.layout.ui-droppable-active')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
|
||||
// Verify widget exists in the layout viewer
|
||||
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_dataset"]').should('exist');
|
||||
|
||||
// Select and configure the dataset
|
||||
cy.get('#configureTab .select2-selection').click();
|
||||
cy.wait('@loadDatasets');
|
||||
cy.get('.select2-container--open input[type="search"]').type('8 items');
|
||||
cy.get('.select2-container--open').contains('8 items').click();
|
||||
|
||||
cy.get('[name="lowerLimit"]').clear().type('1');
|
||||
cy.get('[name="upperLimit"]').clear().type('10');
|
||||
cy.get('.order-clause-row > :nth-child(2) > .form-control').select('Col1', {force: true});
|
||||
cy.get('.order-clause-row > .btn').click();
|
||||
cy.get(':nth-child(2) > :nth-child(2) > .form-control').select('Col2', {force: true});
|
||||
|
||||
// Open Appearance Tab
|
||||
cy.get('.nav-link[href="#appearanceTab"]').click();
|
||||
|
||||
// Ensure dataset has exactly two columns
|
||||
cy.get('#columnsOut li').should('have.length', 2);
|
||||
|
||||
// Move columns to "Columns Selected"
|
||||
cy.get('#columnsOut li:first').trigger('mousedown', {which: 1}).trigger('mousemove', {which: 1, pageX: 583, pageY: 440});
|
||||
cy.get('#columnsIn').click();
|
||||
cy.get('#columnsOut li:first').trigger('mousedown', {which: 1}).trigger('mousemove', {which: 1, pageX: 583, pageY: 440});
|
||||
cy.get('#columnsIn').click();
|
||||
|
||||
// Customize appearance settings
|
||||
cy.get('[name="showHeadings"]').check();
|
||||
cy.get('[name="rowsPerPage"]').clear().type('5');
|
||||
cy.get('[name="fontSize"]').clear().type('48');
|
||||
cy.get('[name="backgroundColor"]').clear().type('#333333');
|
||||
|
||||
// Delete widget
|
||||
// The .moveable-control-box overlay obstructing the right-click interaction on the designer region, causing the test to fail.
|
||||
// By invoking .hide(), we remove the overlay temporarily to allow uninterrupted interaction with the underlying elements.
|
||||
cy.get('.moveable-control-box').invoke('hide');
|
||||
|
||||
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_dataset"]')
|
||||
.parents('.designer-region')
|
||||
.scrollIntoView()
|
||||
.should('be.visible')
|
||||
.rightclick();
|
||||
// Wait until the widget has been deleted
|
||||
// cy.get('[data-title="Delete"]').click().then(() => {
|
||||
// cy.wait('@deleteWidget').its('response.statusCode').should('eq', 200);
|
||||
// cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_dataset"]')
|
||||
// .should('not.exist');
|
||||
// });
|
||||
});
|
||||
});
|
||||
91
cypress/e2e/Layout/Widget/layout_editor_mastodon.cy.js
Normal file
91
cypress/e2e/Layout/Widget/layout_editor_mastodon.cy.js
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Mastodon', function() {
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('should create a new layout and be redirected to the layout designer, add/delete Mastodon widget', function() {
|
||||
cy.intercept('DELETE', '**/region/**').as('deleteWidget');
|
||||
cy.intercept('POST', '/user/pref').as('userPref');
|
||||
|
||||
cy.visit('/layout/view');
|
||||
cy.get('button[href="/layout"]').click();
|
||||
|
||||
// Open widget menu
|
||||
cy.openToolbarMenu(0);
|
||||
|
||||
cy.get('[data-sub-type="mastodon"]')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.wait('@userPref');
|
||||
|
||||
cy.get('[data-template-id="social_media_static_1"] > .toolbar-card-thumb')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.wait('@userPref');
|
||||
|
||||
cy.get('.viewer-object.layout.ui-droppable-active')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
|
||||
// Check if the widget is in the viewer
|
||||
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_mastodon"]')
|
||||
.should('exist');
|
||||
|
||||
cy.get('[name="hashtag"]').clear();
|
||||
cy.get('[name="hashtag"]').type('#cat');
|
||||
cy.get('[name="searchOn"]').select('local', {force: true});
|
||||
cy.get('[name="numItems"]').clear().type('10').trigger('change');
|
||||
cy.get('[name="onlyMedia"]').check();
|
||||
|
||||
// Click on Appearance Tab
|
||||
cy.get('.nav-link[href="#appearanceTab"]').click();
|
||||
cy.get('[name="itemsPerPage"]').clear().type('2').trigger('change');
|
||||
|
||||
// Vertical/Fade/100/Right/Bottom
|
||||
cy.get('[name="displayDirection"]').select('Vertical', {force: true});
|
||||
cy.get('[name="effect"]').select('Fade', {force: true});
|
||||
cy.get('[name="speed"]').clear().type('100').trigger('change');
|
||||
cy.get('[name="alignmentH"]').select('Right', {force: true});
|
||||
cy.get('[name="alignmentV"]').select('Bottom', {force: true});
|
||||
|
||||
// Delete widget
|
||||
// The .moveable-control-box overlay obstructing the right-click interaction on the designer region, causing the test to fail.
|
||||
// By invoking .hide(), we remove the overlay temporarily to allow uninterrupted interaction with the underlying elements.
|
||||
cy.get('.moveable-control-box').invoke('hide');
|
||||
|
||||
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_mastodon"]')
|
||||
.parents('.designer-region')
|
||||
.scrollIntoView()
|
||||
.should('be.visible')
|
||||
.rightclick();
|
||||
|
||||
// Wait until the widget has been deleted
|
||||
// cy.get('[data-title="Delete"]').click().then(() => {
|
||||
// cy.wait('@deleteWidget').its('response.statusCode').should('eq', 200);
|
||||
// cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_mastodon"]')
|
||||
// .should('not.exist');
|
||||
// });
|
||||
});
|
||||
});
|
||||
101
cypress/e2e/Layout/Widget/layout_editor_rss_ticker.cy.js
Normal file
101
cypress/e2e/Layout/Widget/layout_editor_rss_ticker.cy.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('RSS Ticker', function() {
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('should create a new layout and be redirected to the layout designer, add/delete RSS ticker widget', function() {
|
||||
cy.intercept('DELETE', '**/region/**').as('deleteWidget');
|
||||
cy.intercept('POST', '/user/pref').as('userPref');
|
||||
|
||||
cy.visit('/layout/view');
|
||||
cy.get('button[href="/layout"]').click();
|
||||
|
||||
// Open widget menu
|
||||
cy.openToolbarMenu(0);
|
||||
|
||||
cy.get('[data-sub-type="rss-ticker"]')
|
||||
.scrollIntoView()
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.wait('@userPref');
|
||||
|
||||
cy.get('[data-template-id="article_image_only"] > .toolbar-card-thumb')
|
||||
.scrollIntoView()
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.wait('@userPref');
|
||||
|
||||
cy.get('.viewer-object.layout.ui-droppable-active')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
|
||||
// Check if the widget is in the viewer
|
||||
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_rss-ticker"]').should('exist');
|
||||
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_rss-ticker"]').parents('.designer-region').click();
|
||||
|
||||
// Validate if uri is not provide we show an error message
|
||||
cy.get('[name="numItems"]').clear().type('10').trigger('change');
|
||||
cy.get('.form-container').contains('Missing required property Feed URL');
|
||||
|
||||
cy.get('[name="uri"]').clear();
|
||||
cy.get('[name="uri"]').type('http://xibo.org.uk/feed');
|
||||
cy.get('[name="numItems"]').clear().type('10').trigger('change');
|
||||
cy.get('[name="durationIsPerItem"]').check();
|
||||
cy.get('[name="takeItemsFrom"]').select('End of the Feed', {force: true});
|
||||
cy.get('[name="reverseOrder"]').check();
|
||||
cy.get('[name="randomiseItems"]').check();
|
||||
|
||||
cy.get('[name="userAgent"]').clear().type('Mozilla/5.0');
|
||||
cy.get('[name="updateInterval"]').clear().type('10').trigger('change');
|
||||
|
||||
// Click on Appearance Tab
|
||||
cy.get('.nav-link[href="#appearanceTab"]').click();
|
||||
cy.get('[name="backgroundColor"]').clear().type('#dddddd');
|
||||
cy.get('[name="itemImageFit"]').select('Fill', {force: true});
|
||||
cy.get('[name="effect"]').select('Fade', {force: true});
|
||||
cy.get('[name="speed"]').clear().type('500');
|
||||
// Update CKEditor value
|
||||
cy.updateCKEditor('noDataMessage', 'No data to show');
|
||||
cy.get('[name="copyright"]').clear().type('Xibo').trigger('change');
|
||||
|
||||
// Delete widget
|
||||
// The .moveable-control-box overlay obstructing the right-click interaction on the designer region, causing the test to fail.
|
||||
// By invoking .hide(), we remove the overlay temporarily to allow uninterrupted interaction with the underlying elements.
|
||||
cy.get('.moveable-control-box').invoke('hide');
|
||||
|
||||
cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_rss-ticker"]')
|
||||
.parents('.designer-region')
|
||||
.scrollIntoView()
|
||||
.should('be.visible')
|
||||
.rightclick();
|
||||
|
||||
// Wait until the widget has been deleted
|
||||
// cy.get('[data-title="Delete"]').click().then(() => {
|
||||
// cy.wait('@deleteWidget').its('response.statusCode').should('eq', 200);
|
||||
// cy.get('#layout-viewer .designer-region .widget-preview[data-type="widget_rss-ticker"]')
|
||||
// .should('not.exist');
|
||||
// });
|
||||
});
|
||||
});
|
||||
56
cypress/e2e/Layout/layout_view.cy.js
Normal file
56
cypress/e2e/Layout/layout_view.cy.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Layout View', function() {
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('searches and delete existing layout', function() {
|
||||
// Create random name
|
||||
const uuid = Cypress._.random(0, 1e10);
|
||||
|
||||
// Create a new layout and go to the layout's designer page, then load toolbar prefs
|
||||
cy.createLayout(uuid).as('testLayoutId').then((res) => {
|
||||
cy.intercept('GET', '/layout?draw=2&*').as('layoutGridLoad');
|
||||
|
||||
cy.visit('/layout/view');
|
||||
|
||||
// Filter for the created layout
|
||||
cy.get('#Filter input[name="layout"]')
|
||||
.type(uuid);
|
||||
|
||||
// Wait for the layout grid reload
|
||||
cy.wait('@layoutGridLoad');
|
||||
|
||||
// Click on the first row element to open the designer
|
||||
cy.get('#layouts tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#layouts tr:first-child .layout_button_delete').click({force: true});
|
||||
|
||||
// Delete test layout
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check if layout is deleted in toast message
|
||||
cy.get('.toast').contains('Deleted ' + uuid);
|
||||
});
|
||||
});
|
||||
});
|
||||
322
cypress/e2e/Library/datasets.cy.js
Normal file
322
cypress/e2e/Library/datasets.cy.js
Normal file
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Datasets', function() {
|
||||
let testRun = '';
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
testRun = Cypress._.random(0, 1e9);
|
||||
});
|
||||
|
||||
it('should add one empty dataset', function() {
|
||||
cy.visit('/dataset/view');
|
||||
|
||||
// Click on the Add Dataset button
|
||||
cy.contains('Add DataSet').click();
|
||||
|
||||
cy.get('.modal input#dataSet')
|
||||
.type('Cypress Test Dataset ' + testRun + '_1');
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Check if dataset is added in toast message
|
||||
cy.contains('Added Cypress Test Dataset ' + testRun + '_1');
|
||||
});
|
||||
|
||||
it('should be able to cancel creating dataset', function() {
|
||||
cy.visit('/dataset/view');
|
||||
|
||||
// Click on the Add Dataset button
|
||||
cy.contains('Add DataSet').click();
|
||||
|
||||
cy.get('.modal input#dataSet')
|
||||
.type('Cypress Test Dataset ' + testRun + '_1');
|
||||
|
||||
// Click cancel
|
||||
cy.get('.modal #dialog_btn_1').click();
|
||||
|
||||
// Check if you are back to the view page
|
||||
cy.url().should('include', '/dataset/view');
|
||||
});
|
||||
|
||||
it('searches and edit existing dataset', function() {
|
||||
// Create a new dataset and then search for it and delete it
|
||||
cy.createDataset('Cypress Test Dataset ' + testRun).then((id) => {
|
||||
cy.intercept({
|
||||
url: '/dataset?*',
|
||||
query: {dataSet: 'Cypress Test Dataset ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/dataset/*',
|
||||
}).as('putRequest');
|
||||
|
||||
cy.visit('/dataset/view');
|
||||
|
||||
// Filter for the created dataset
|
||||
cy.get('#Filter input[name="dataSet"]')
|
||||
.type('Cypress Test Dataset ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#datasets tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#datasets tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#datasets tr:first-child .dataset_button_edit').click({force: true});
|
||||
|
||||
cy.get('.modal input#dataSet').clear()
|
||||
.type('Cypress Test Dataset Edited ' + testRun);
|
||||
|
||||
// edit test dataset
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
|
||||
// assertion on the "dataset" value
|
||||
expect(responseData.dataSet).to.eq('Cypress Test Dataset Edited ' + testRun);
|
||||
});
|
||||
|
||||
// Delete the dataset and assert success
|
||||
cy.deleteDataset(id).then((res) => {
|
||||
expect(res.status).to.equal(204);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('add row/column to an existing dataset', function() {
|
||||
// Create a new dataset and then search for it and delete it
|
||||
cy.createDataset('Cypress Test Dataset ' + testRun).then((id) => {
|
||||
cy.intercept({
|
||||
url: '/dataset?*',
|
||||
query: {dataSet: 'Cypress Test Dataset ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: /\/dataset\/\d+\/column$/,
|
||||
}).as('postRequestAddColumn');
|
||||
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: /\/dataset\/data\/\d+/,
|
||||
}).as('postRequestAddRow');
|
||||
|
||||
cy.visit('/dataset/view');
|
||||
|
||||
// Filter for the created dataset
|
||||
cy.get('#Filter input[name="dataSet"]')
|
||||
.type('Cypress Test Dataset ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#datasets tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the View data
|
||||
cy.get('#datasets tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#datasets tr:first-child .dataset_button_viewcolumns').click({force: true});
|
||||
|
||||
cy.get('#datasets').contains('No data available in table');
|
||||
|
||||
// Add data row to dataset
|
||||
cy.contains('Add Column').click();
|
||||
cy.get('.modal input#heading').type('Col1');
|
||||
|
||||
// Save
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@postRequestAddColumn').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
|
||||
// assertion on the "dataset" value
|
||||
expect(responseData.heading).to.eq('Col1');
|
||||
|
||||
cy.contains('View Data').click();
|
||||
cy.get('#datasets').contains('No data available in table');
|
||||
|
||||
// Add data row to dataset
|
||||
cy.contains('Add Row').click();
|
||||
cy.get('#dataSetDataAdd').within(() => {
|
||||
cy.get('input:first').type('Your text goes here');
|
||||
});
|
||||
|
||||
// Save
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted request and check data
|
||||
cy.wait('@postRequestAddRow').then((interception) => {
|
||||
cy.contains('Added Row');
|
||||
});
|
||||
});
|
||||
|
||||
// Now try to delete the dataset
|
||||
cy.visit('/dataset/view');
|
||||
|
||||
// Filter for the created dataset
|
||||
cy.get('#Filter input[name="dataSet"]')
|
||||
.type('Cypress Test Dataset ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#datasets tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the View data
|
||||
cy.get('#datasets tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#datasets tr:first-child .dataset_button_delete').click({force: true});
|
||||
});
|
||||
});
|
||||
|
||||
it('copy an existing dataset', function() {
|
||||
// Create a new dataset and then search for it and copy it
|
||||
cy.createDataset('Cypress Test Dataset ' + testRun).then((res) => {
|
||||
cy.intercept({
|
||||
url: '/dataset?*',
|
||||
query: {dataSet: 'Cypress Test Dataset ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the POST request
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: /\/dataset\/copy\/\d+/,
|
||||
}).as('postRequest');
|
||||
|
||||
cy.visit('/dataset/view');
|
||||
|
||||
// Filter for the created dataset
|
||||
cy.get('#Filter input[name="dataSet"]')
|
||||
.type('Cypress Test Dataset ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#datasets tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#datasets tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#datasets tr:first-child .dataset_button_copy').click({force: true});
|
||||
|
||||
// save
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted POST request and check the form data
|
||||
cy.wait('@postRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
expect(responseData.dataSet).to.include('Cypress Test Dataset ' + testRun + ' 2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('searches and delete existing dataset', function() {
|
||||
// Create a new dataset and then search for it and delete it
|
||||
cy.createDataset('Cypress Test Dataset ' + testRun).then((res) => {
|
||||
cy.intercept('GET', '/dataset?draw=2&*').as('datasetGridLoad');
|
||||
|
||||
cy.visit('/dataset/view');
|
||||
|
||||
// Filter for the created dataset
|
||||
cy.get('#Filter input[name="dataSet"]')
|
||||
.type('Cypress Test Dataset ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@datasetGridLoad');
|
||||
cy.get('#datasets tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#datasets tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#datasets tr:first-child .dataset_button_delete').click({force: true});
|
||||
|
||||
// Delete test dataset
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check if dataset is deleted in toast message
|
||||
cy.get('.toast').contains('Deleted Cypress Test Dataset');
|
||||
});
|
||||
});
|
||||
|
||||
it('selects multiple datasets and delete them', function() {
|
||||
// Create a new dataset and then search for it and delete it
|
||||
cy.createDataset('Cypress Test Dataset ' + testRun).then((res) => {
|
||||
cy.intercept('GET', '/dataset?draw=2&*').as('datasetGridLoad');
|
||||
|
||||
// Delete all test datasets
|
||||
cy.visit('/dataset/view');
|
||||
|
||||
// Clear filter
|
||||
cy.get('#Filter input[name="dataSet"]')
|
||||
.clear()
|
||||
.type('Cypress Test Dataset');
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@datasetGridLoad');
|
||||
|
||||
// Select all
|
||||
cy.get('button[data-toggle="selectAll"]').click();
|
||||
|
||||
// Delete all
|
||||
cy.get('.dataTables_info button[data-toggle="dropdown"]').click();
|
||||
cy.get('.dataTables_info a[data-button-id="dataset_button_delete"]').click();
|
||||
|
||||
cy.get('input#deleteData').check();
|
||||
cy.get('button.save-button').click();
|
||||
|
||||
// Modal should contain one successful delete at least
|
||||
cy.get('.modal-body').contains(': Success');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------
|
||||
// Tests - Error handling
|
||||
it('should not add a remote dataset without URI', function() {
|
||||
cy.visit('/dataset/view');
|
||||
|
||||
// Click on the Add Dataset button
|
||||
cy.contains('Add DataSet').click();
|
||||
|
||||
cy.get('.modal input#dataSet')
|
||||
.type('Cypress Test Dataset ' + testRun);
|
||||
|
||||
cy.get('.modal input#isRemote').check();
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Click on the "Remote" tab
|
||||
cy.get(':nth-child(2) > .nav-link').should('be.visible').click();
|
||||
|
||||
// Check that the error message is displayed for the missing URI field
|
||||
cy.get('#uri-error').should('have.text', 'This field is required.');
|
||||
});
|
||||
});
|
||||
111
cypress/e2e/Library/media.cy.js
Normal file
111
cypress/e2e/Library/media.cy.js
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Media Admin', function() {
|
||||
let testRun;
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
testRun = Cypress._.random(0, 1e9);
|
||||
});
|
||||
|
||||
it('should add a media via url', function() {
|
||||
cy.visit('/library/view');
|
||||
|
||||
// Click on the Add Playlist button
|
||||
cy.contains('Add media (URL)').click();
|
||||
|
||||
cy.get('#url')
|
||||
.type('https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4');
|
||||
|
||||
cy.get('#optionalName')
|
||||
.type('Cypress Test Media ' + testRun);
|
||||
|
||||
cy.get('.modal .save-button').click();
|
||||
cy.wait(24000);
|
||||
|
||||
// Filter for the created playlist
|
||||
cy.get('#media')
|
||||
.type('Cypress Test Media ' + testRun);
|
||||
|
||||
// Should have the added playlist
|
||||
cy.get('#libraryItems tbody tr').should('have.length', 1);
|
||||
cy.get('#libraryItems tbody tr:nth-child(1) td:nth-child(2)').contains('Cypress Test Media ' + testRun);
|
||||
});
|
||||
|
||||
it('should cancel adding a media', function() {
|
||||
cy.visit('/library/view');
|
||||
|
||||
// Click on the Add Playlist button
|
||||
cy.contains('Add media (URL)').click();
|
||||
|
||||
cy.get('#url')
|
||||
.type('https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4');
|
||||
|
||||
cy.get('#optionalName')
|
||||
.type('Cypress Test Media ' + testRun);
|
||||
|
||||
// Click cancel
|
||||
cy.get('#dialog_btn_1').click();
|
||||
|
||||
// Check if you are back to the view page
|
||||
cy.url().should('include', '/library/view');
|
||||
});
|
||||
|
||||
it('should show a list of Media', function() {
|
||||
// Wait for playlist grid reload
|
||||
cy.intercept('/library?draw=1&*').as('mediaGridLoad');
|
||||
|
||||
cy.visit('/library/view').then(function() {
|
||||
cy.wait('@mediaGridLoad');
|
||||
cy.get('#libraryItems');
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('selects media and delete them', function() {
|
||||
// Create a new playlist and then search for it and delete it
|
||||
cy.intercept('/library?draw=1&*').as('mediaGridLoad');
|
||||
|
||||
// Delete all test playlists
|
||||
cy.visit('/library/view');
|
||||
|
||||
// Clear filter and search for text playlists
|
||||
cy.get('#media')
|
||||
.clear()
|
||||
.type('Cypress Test Media');
|
||||
|
||||
// Wait for 1st playlist grid reload
|
||||
cy.wait('@mediaGridLoad');
|
||||
|
||||
// Select first entry
|
||||
cy.get('table#libraryItems').contains('Cypress Test Media').parents('tr.odd').should('be.visible').click();
|
||||
cy.get('button[data-toggle="dropdown"]').first().click();
|
||||
|
||||
// Click Delete
|
||||
cy.contains('Delete').click();
|
||||
cy.get('button.save-button').click();
|
||||
|
||||
// Modal should contain one successful delete at least
|
||||
cy.get('div[class="toast-message"]').should('contain', 'Deleted');
|
||||
});
|
||||
});
|
||||
439
cypress/e2e/Library/menuboards.cy.js
Normal file
439
cypress/e2e/Library/menuboards.cy.js
Normal file
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Menuboards', function() {
|
||||
let testRun = '';
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
testRun = Cypress._.random(0, 1e9);
|
||||
});
|
||||
|
||||
it('should add a menuboard', function() {
|
||||
cy.visit('/menuboard/view');
|
||||
|
||||
// Click on the Add Menuboard button
|
||||
cy.contains('Add Menu Board').click();
|
||||
|
||||
cy.get('.modal input#name')
|
||||
.type('Cypress Test Menuboard ' + testRun + '_1');
|
||||
cy.get('.modal input#code')
|
||||
.type('MENUBOARD');
|
||||
cy.get('.modal textarea#description')
|
||||
.type('Menuboard Description');
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Check if menuboard is added in toast message
|
||||
cy.contains('Added Menu Board');
|
||||
});
|
||||
|
||||
it('searches and edit existing menuboard', function() {
|
||||
// Create a new menuboard and then search for it and delete it
|
||||
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((res) => {
|
||||
cy.intercept({
|
||||
url: '/menuboard?*',
|
||||
query: {name: 'Cypress Test Menuboard ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/menuboard/*',
|
||||
}).as('putRequest');
|
||||
|
||||
cy.visit('/menuboard/view');
|
||||
|
||||
// Filter for the created menuboard
|
||||
cy.get('#Filter input[name="name"]')
|
||||
.type('Cypress Test Menuboard ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#menuBoards tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#menuBoards tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#menuBoards tr:first-child .menuBoard_edit_button').click({force: true});
|
||||
|
||||
cy.get('.modal input#name').clear()
|
||||
.type('Cypress Test Menuboard Edited ' + testRun);
|
||||
|
||||
// edit test menuboard
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
|
||||
// assertion on the "menuboard" value
|
||||
expect(responseData.name).to.eq('Cypress Test Menuboard Edited ' + testRun);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('searches and delete existing menuboard', function() {
|
||||
// Create a new menuboard and then search for it and delete it
|
||||
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((res) => {
|
||||
cy.intercept({
|
||||
url: '/menuboard?*',
|
||||
query: {name: 'Cypress Test Menuboard ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
cy.visit('/menuboard/view');
|
||||
|
||||
// Filter for the created menuboard
|
||||
cy.get('#Filter input[name="name"]')
|
||||
.type('Cypress Test Menuboard ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#menuBoards tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#menuBoards tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#menuBoards tr:first-child .menuBoard_delete_button').click({force: true});
|
||||
|
||||
// Delete test menuboard
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check if menuboard is deleted in toast message
|
||||
cy.get('.toast').contains('Deleted Cypress Test Menuboard');
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------
|
||||
it('should add categories and products to a menuboard', function() {
|
||||
// Create a new menuboard and then search for it and delete it
|
||||
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((menuId) => {
|
||||
cy.intercept({
|
||||
url: '/menuboard?*',
|
||||
query: {name: 'Cypress Test Menuboard ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
cy.visit('/menuboard/view');
|
||||
|
||||
// Filter for the created menuboard
|
||||
cy.get('#Filter input[name="name"]')
|
||||
.type('Cypress Test Menuboard ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#menuBoards tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#menuBoards tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#menuBoards tr:first-child .menuBoard_button_viewcategories').click({force: true});
|
||||
|
||||
// Click on the Add Category button
|
||||
cy.contains('Add Category').click();
|
||||
|
||||
cy.get('.modal input#name')
|
||||
.type('Cypress Test Category ' + testRun + '_1');
|
||||
cy.get('.modal input#code')
|
||||
.type('MENUBOARDCAT');
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Check if menuboard is added in toast message
|
||||
cy.contains('Added Menu Board Category');
|
||||
|
||||
// Wait for the grid reload
|
||||
// cy.wait('@loadCategoryGridAfterSearch');
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#menuBoardCategories tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#menuBoardCategories tr:first-child .menuBoardCategory_button_viewproducts').click({force: true});
|
||||
|
||||
// Click on the Add Product button
|
||||
cy.contains('Add Product').click();
|
||||
|
||||
cy.get('.modal input#name')
|
||||
.type('Cypress Test Product ' + testRun + '_1');
|
||||
cy.get('.modal input#code')
|
||||
.type('MENUBOARDPROD');
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Check if menuboard is added in toast message
|
||||
cy.contains('Added Menu Board Product');
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------
|
||||
// Categories
|
||||
it('should add a category', function() {
|
||||
// Create a new menuboard and then search for it and delete it
|
||||
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((menuId) => {
|
||||
// GO to products page
|
||||
cy.visit('/menuboard/' + menuId + '/categories/view');
|
||||
// Click on the Add Category button
|
||||
cy.contains('Add Category').click();
|
||||
|
||||
cy.get('.modal input#name')
|
||||
.type('Cypress Test Category ' + testRun + '_1');
|
||||
cy.get('.modal input#code')
|
||||
.type('MENUBOARDCAT');
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Check toast message
|
||||
cy.contains('Added Menu Board Category');
|
||||
|
||||
// Delete the menuboard and assert success
|
||||
cy.deleteMenuboard(menuId).then((response) => {
|
||||
expect(response.status).to.equal(204);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('searches and edit existing category', function() {
|
||||
// Create a new menuboard and then search for it and delete it
|
||||
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((menuId) => {
|
||||
cy.createMenuboardCat('Cypress Test Category ' + testRun, menuId).then((menuCatId) => {
|
||||
cy.intercept({
|
||||
url: '/menuboard/' + menuId + '/categories?*',
|
||||
query: {name: 'Cypress Test Category ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/menuboard/' + menuCatId + '/category',
|
||||
}).as('putRequest');
|
||||
|
||||
// GO to products page
|
||||
cy.visit('/menuboard/' + menuId + '/categories/view');
|
||||
// Filter for the created menuboard
|
||||
cy.get('#Filter input[name="name"]')
|
||||
.type('Cypress Test Category ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#menuBoardCategories tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#menuBoardCategories tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#menuBoardCategories tr:first-child .menuBoardCategory_edit_button').click({force: true});
|
||||
|
||||
// EDIT
|
||||
cy.get('.modal input#name').clear()
|
||||
.type('Cypress Test Category Edited ' + testRun);
|
||||
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
|
||||
// assertion on the "menuboard" value
|
||||
expect(responseData.name).to.eq('Cypress Test Category Edited ' + testRun);
|
||||
});
|
||||
|
||||
// Delete the menuboard and assert success
|
||||
cy.deleteMenuboard(menuId).then((response) => {
|
||||
expect(response.status).to.equal(204);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('searches and delete existing category', function() {
|
||||
// Create a new menuboard and then search for it and delete it
|
||||
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((menuId) => {
|
||||
cy.createMenuboardCat('Cypress Test Category ' + testRun, menuId).then((menuCatId) => {
|
||||
cy.intercept({
|
||||
url: '/menuboard/' + menuId + '/categories?*',
|
||||
query: {name: 'Cypress Test Category ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/menuboard/' + menuCatId + '/category',
|
||||
}).as('putRequest');
|
||||
|
||||
// GO to products page
|
||||
cy.visit('/menuboard/' + menuId + '/categories/view');
|
||||
// Filter for the created menuboard
|
||||
cy.get('#Filter input[name="name"]')
|
||||
.type('Cypress Test Category ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#menuBoardCategories tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#menuBoardCategories tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#menuBoardCategories tr:first-child .menuBoardCategory_delete_button').click({force: true});
|
||||
|
||||
// Delete test category
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check toast message
|
||||
cy.get('.toast').contains('Deleted Cypress Test Category');
|
||||
|
||||
// Delete the menuboard and assert success
|
||||
cy.deleteMenuboard(menuId).then((response) => {
|
||||
expect(response.status).to.equal(204);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------
|
||||
// Products
|
||||
it('should add a product', function() {
|
||||
// Create a new menuboard and then search for it and delete it
|
||||
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((menuId) => {
|
||||
cy.createMenuboardCat('Cypress Test Category ' + testRun, menuId).then((menuCatId) => {
|
||||
// GO to products page
|
||||
cy.visit('/menuboard/' + menuCatId + '/products/view');
|
||||
// Click on the Add Product button
|
||||
cy.contains('Add Product').click();
|
||||
|
||||
cy.get('.modal input#name')
|
||||
.type('Cypress Test Product ' + testRun);
|
||||
cy.get('.modal input#code')
|
||||
.type('MENUBOARDPROD');
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Check if menuboard is added in toast message
|
||||
cy.contains('Added Menu Board Product');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('searches and edit existing product', function() {
|
||||
// Create a new menuboard and then search for it and delete it
|
||||
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((menuId) => {
|
||||
cy.log(menuId);
|
||||
cy.createMenuboardCat('Cypress Test Category ' + testRun, menuId).then((menuCatId) => {
|
||||
cy.log(menuCatId);
|
||||
cy.createMenuboardCatProd('Cypress Test Product ' + testRun, menuCatId).then((menuProdId) => {
|
||||
cy.log(menuProdId);
|
||||
cy.intercept({
|
||||
url: '/menuboard/' + menuCatId + '/products?*',
|
||||
query: {name: 'Cypress Test Product ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/menuboard/' + menuProdId + '/product',
|
||||
}).as('putRequest');
|
||||
|
||||
// GO to products page
|
||||
cy.visit('/menuboard/' + menuCatId + '/products/view');
|
||||
// Filter for the created menuboard
|
||||
cy.get('#Filter input[name="name"]')
|
||||
.type('Cypress Test Product ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#menuBoardProducts tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#menuBoardProducts tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#menuBoardProducts tr:first-child .menuBoardProduct_edit_button').click({force: true});
|
||||
|
||||
// EDIT
|
||||
cy.get('.modal input#name').clear()
|
||||
.type('Cypress Test Product Edited ' + testRun);
|
||||
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
|
||||
// assertion on the "menuboard" value
|
||||
expect(responseData.name).to.eq('Cypress Test Product Edited ' + testRun);
|
||||
});
|
||||
|
||||
// Delete the menuboard and assert success
|
||||
cy.deleteMenuboard(menuId).then((response) => {
|
||||
expect(response.status).to.equal(204);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('searches and delete existing product', function() {
|
||||
// Create a new menuboard and then search for it and delete it
|
||||
cy.createMenuboard('Cypress Test Menuboard ' + testRun).then((menuId) => {
|
||||
cy.createMenuboardCat('Cypress Test Category ' + testRun, menuId).then((menuCatId) => {
|
||||
cy.createMenuboardCatProd('Cypress Test Product ' + testRun, menuCatId).then((menuProdId) => {
|
||||
cy.intercept({
|
||||
url: '/menuboard/' + menuCatId + '/products?*',
|
||||
query: {name: 'Cypress Test Product ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/menuboard/' + menuProdId + '/product',
|
||||
}).as('putRequest');
|
||||
|
||||
// GO to products page
|
||||
cy.visit('/menuboard/' + menuCatId + '/products/view');
|
||||
// Filter for the created menuboard
|
||||
cy.get('#Filter input[name="name"]')
|
||||
.type('Cypress Test Product ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#menuBoardProducts tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#menuBoardProducts tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#menuBoardProducts tr:first-child .menuBoardProduct_delete_button').click({force: true});
|
||||
|
||||
// Delete test menuboard
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check toast message
|
||||
cy.get('.toast').contains('Deleted Cypress Test Product');
|
||||
|
||||
// Delete the menuboard and assert success
|
||||
cy.deleteMenuboard(menuId).then((response) => {
|
||||
expect(response.status).to.equal(204);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
41
cypress/e2e/Library/playlist_editor_empty.cy.js
Normal file
41
cypress/e2e/Library/playlist_editor_empty.cy.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
describe('Playlist Editor (Empty)', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
// Create random name
|
||||
let uuid = Cypress._.random(0, 1e9);
|
||||
|
||||
// Create a new layout and go to the layout's designer page
|
||||
cy.createNonDynamicPlaylist(uuid).as('testPlaylistId').then((res) => {
|
||||
cy.openPlaylistEditorAndLoadPrefs(res);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show the droppable zone and toolbar', function() {
|
||||
|
||||
cy.get('#playlist-editor-container').should('be.visible');
|
||||
cy.get('div[class="container-toolbar container-fluid flex-column flex-column justify-content-between"]').should('be.visible');
|
||||
});
|
||||
});
|
||||
208
cypress/e2e/Library/playlist_editor_populated.cy.js
Normal file
208
cypress/e2e/Library/playlist_editor_populated.cy.js
Normal file
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
describe('Playlist Editor (Populated)', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
// Create random name
|
||||
let uuid = Cypress._.random(0, 1e9);
|
||||
|
||||
// Create a new layout and go to the layout's designer page
|
||||
cy.createNonDynamicPlaylist(uuid).as('testPlaylistId').then((res) => {
|
||||
|
||||
// Populate playlist with some widgets and media
|
||||
cy.addWidgetToPlaylist(res, 'embedded', {
|
||||
name: 'Embedded Widget'
|
||||
});
|
||||
|
||||
cy.addMediaToLibrary("file/example.zip");
|
||||
|
||||
cy.addWidgetToPlaylist(res, 'clock', {
|
||||
name: 'Clock Widget'
|
||||
});
|
||||
|
||||
cy.openPlaylistEditorAndLoadPrefs(res);
|
||||
});
|
||||
});
|
||||
|
||||
it('changes and saves widget properties', () => {
|
||||
// Create and alias for reload widget
|
||||
// cy.intercept('GET','/playlist/widget/form/edit/*').as('reloadWidget');
|
||||
|
||||
// Select the first widget on timeline ( image )
|
||||
cy.get('#timeline-container [data-type="widget"]').first().click();
|
||||
|
||||
// Wait for the widget to load
|
||||
// cy.wait('@reloadWidget');
|
||||
|
||||
// Type the new name in the input
|
||||
cy.get('a[href="#advancedTab"]').click();
|
||||
cy.get('#properties-panel-form-container input[name="name"]').clear().type('newName');
|
||||
|
||||
// Set a duration
|
||||
cy.get('#properties-panel-form-container input[name="useDuration"]').check();
|
||||
cy.get('#properties-panel-form-container input[name="duration"]').clear().type(12);
|
||||
|
||||
// Save form
|
||||
cy.get('#properties-panel-form-container button[data-action="save"]').click();
|
||||
|
||||
// Should show a notification for the name change
|
||||
// cy.get('.toast-success');
|
||||
|
||||
// Wait for the widget to reload
|
||||
// cy.wait('@reloadWidget');
|
||||
|
||||
// Check if the values are the same entered after reload
|
||||
cy.get('#properties-panel-form-container input[name="name"]').should('have.prop', 'value').and('equal', 'newName');
|
||||
cy.get('#properties-panel-form-container input[name="duration"]').should('have.prop', 'value').and('equal', '12');
|
||||
|
||||
});
|
||||
|
||||
it.skip('should revert a saved form to a previous state', () => {
|
||||
|
||||
let oldName;
|
||||
|
||||
// Create and alias for reload widget
|
||||
// cy.intercept('GET', '/playlist/widget/form/edit/*').as('reloadWidget');
|
||||
// cy.intercept('PUT', '/playlist/widget/*').as('saveWidget');
|
||||
|
||||
// Select the first widget on timeline ( image )
|
||||
cy.get('#timeline-container [data-type="widget"]').first().click();
|
||||
|
||||
// Wait for the widget to load
|
||||
// cy.wait('@reloadWidget');
|
||||
|
||||
// Get the input field
|
||||
cy.get('a[href="#advancedTab"]').click();
|
||||
cy.get('#properties-panel-form-container input[name="name"]').then(($input) => {
|
||||
|
||||
// Save old name
|
||||
oldName = $input.val();
|
||||
|
||||
//Type the new name in the input
|
||||
cy.get('#properties-panel-form-container input[name="name"]').clear().type('newName');
|
||||
|
||||
// Save form
|
||||
cy.get('#properties-panel-form-container button[data-action="save"]').click();
|
||||
|
||||
// Should show a notification for the name change
|
||||
// cy.get('.toast-success');
|
||||
|
||||
// Wait for the widget to save
|
||||
// cy.wait('@reloadWidget');
|
||||
|
||||
// Click the revert button
|
||||
cy.get('#playlist-editor-toolbar #undoContainer').click();
|
||||
|
||||
// Wait for the widget to save
|
||||
// cy.wait('@saveWidget');
|
||||
|
||||
// Test if the revert made the name go back to the old name
|
||||
cy.get('#properties-panel-form-container input[name="name"]').should('have.prop', 'value').and('equal', oldName);
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete a widget using the toolbar bin', () => {
|
||||
// cy.intercept('/playlist?playlistId=*').as('reloadPlaylist');
|
||||
|
||||
// Select a widget from the navigator
|
||||
cy.get('#playlist-timeline [data-type="widget"]').first().click().then(($el) => {
|
||||
|
||||
const widgetId = $el.attr('id');
|
||||
|
||||
// Click trash container
|
||||
cy.get('div[class="widgetDelete"]').first().click({force: true});
|
||||
|
||||
// Confirm delete on modal
|
||||
cy.get('button[class*="btn-bb-confirm"]').click();
|
||||
|
||||
// Check toast message
|
||||
// cy.get('.toast-success').contains('Deleted');
|
||||
|
||||
// Wait for the layout to reload
|
||||
// cy.wait('@reloadPlaylist');
|
||||
|
||||
// Check that widget is not on timeline
|
||||
cy.get('#playlist-timeline [data-type="widget"]#' + widgetId).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should add an audio clip to a widget by the context menu, and adds a link to open the form in the timeline', () => {
|
||||
|
||||
cy.populateLibraryWithMedia();
|
||||
|
||||
// Create and alias for reload playlist
|
||||
cy.intercept('/playlist?playlistId=*').as('reloadPlaylist');
|
||||
|
||||
// Right click to open the context menu and select add audio
|
||||
cy.get('#timeline-container [data-type="widget"]').first().should('be.visible').rightclick();
|
||||
cy.get('.context-menu-btn[data-property="Audio"]').should('be.visible').click();
|
||||
|
||||
// Select the 1st option
|
||||
cy.get('[data-test="widgetPropertiesForm"] #mediaId > option').eq(1).then(($el) => {
|
||||
cy.get('[data-test="widgetPropertiesForm"] #mediaId').select($el.val());
|
||||
});
|
||||
|
||||
// Save and close the form
|
||||
cy.get('[data-test="widgetPropertiesForm"] .btn-bb-done').click();
|
||||
|
||||
// Check if the widget has the audio icon
|
||||
// cy.wait('@reloadPlaylist');
|
||||
cy.get('#timeline-container [data-type="widget"]:first-child')
|
||||
.find('i[data-property="Audio"]').should('exist').click({force: true});
|
||||
|
||||
cy.get('[data-test="widgetPropertiesForm"]').contains('Audio for');
|
||||
});
|
||||
|
||||
// Skip test for now ( it's failing in the test suite and being tested already in layout designer spec )
|
||||
it.skip('attaches expiry dates to a widget by the context menu, and adds a link to open the form in the timeline', () => {
|
||||
// Create and alias for reload playlist
|
||||
// cy.intercept('/playlist?playlistId=*').as('reloadPlaylist');
|
||||
|
||||
// Right click to open the context menu and select add audio
|
||||
cy.get('#timeline-container [data-type="widget"]').first().should('be.visible').rightclick();
|
||||
cy.get('.context-menu-btn[data-property="Expiry"]').should('be.visible').click();
|
||||
|
||||
// Add dates
|
||||
cy.get('[data-test="widgetPropertiesForm"] .starttime-control .date-clear-button').click();
|
||||
// cy.get('[data-test="widgetPropertiesForm"] #fromDt').find('input[class="datePickerHelper form-control dateControl dateTime active"]').click();
|
||||
cy.get('div[class="flatpickr-wrapper"]').first().click();
|
||||
cy.get('.flatpickr-calendar.open .dayContainer .flatpickr-day:first').click();
|
||||
|
||||
cy.get('[data-test="widgetPropertiesForm"] .endtime-control .date-clear-button').click();
|
||||
// cy.get('[data-test="widgetPropertiesForm"] #toDt').find('input[class="datePickerHelper form-control dateControl dateTime active"]').click();
|
||||
cy.get('div[class="flatpickr-wrapper"]').last().click();
|
||||
cy.get('.flatpickr-calendar.open .dayContainer .flatpickr-day:first').click();
|
||||
|
||||
|
||||
// Save and close the form
|
||||
cy.get('[data-test="widgetPropertiesForm"] .btn-bb-done').click();
|
||||
|
||||
// Check if the widget has the expiry dates icon
|
||||
// cy.wait('@reloadPlaylist');
|
||||
cy.get('#timeline-container [data-type="widget"]:first-child')
|
||||
.find('i[data-property="Expiry"]').should('exist').click({force: true});
|
||||
|
||||
cy.get('[data-test="widgetPropertiesForm"]').contains('Expiry for');
|
||||
});
|
||||
});
|
||||
110
cypress/e2e/Library/playlist_editor_populated_unchanged.cy.js
Normal file
110
cypress/e2e/Library/playlist_editor_populated_unchanged.cy.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
describe('Playlist Editor (Populated/Unchanged)', function() {
|
||||
|
||||
before(function() {
|
||||
cy.login();
|
||||
|
||||
// Create random name
|
||||
let uuid = Cypress._.random(0, 1e9);
|
||||
|
||||
// Create a new layout and go to the layout's designer page
|
||||
cy.createNonDynamicPlaylist(uuid).as('testPlaylistId').then((res) => {
|
||||
|
||||
// Populate playlist with some widgets and media
|
||||
cy.addWidgetToPlaylist(res, 'embedded', {
|
||||
name: 'Embedded Widget'
|
||||
});
|
||||
|
||||
// TODO skip so that the test success
|
||||
// cy.addRandomMediaToPlaylist(res);
|
||||
|
||||
cy.addWidgetToPlaylist(res, 'clock', {
|
||||
name: 'Clock Widget'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
cy.openPlaylistEditorAndLoadPrefs(this.testPlaylistId);
|
||||
});
|
||||
|
||||
it('opens a media tab in the toolbar and searches for items', () => {
|
||||
|
||||
// cy.intercept('/library/search?*').as('mediaLoad');
|
||||
|
||||
cy.populateLibraryWithMedia();
|
||||
|
||||
// Open audio tool tab
|
||||
cy.get('a[id="btn-menu-3"]').should('be.visible').click();
|
||||
|
||||
// cy.wait('@mediaLoad');
|
||||
|
||||
// Check if there are audio items in the search content
|
||||
cy.get('div[class="toolbar-card-preview"]').last().should('be.visible');
|
||||
});
|
||||
|
||||
it('creates a new widget by selecting a searched media from the toolbar to the editor, and then reverts the change', () => {
|
||||
cy.populateLibraryWithMedia();
|
||||
|
||||
// Create and alias for reload playlist
|
||||
// cy.intercept('/playlist?playlistId=*').as('reloadPlaylist');
|
||||
// cy.intercept('DELETE', '/playlist/widget/*').as('deleteWidget');
|
||||
// cy.intercept('/library/search?*').as('mediaLoad');
|
||||
|
||||
// Open library search tab
|
||||
cy.get('a[id="btn-menu-0"]').should('be.visible').click();
|
||||
|
||||
// cy.wait('@mediaLoad');
|
||||
cy.wait(1000);
|
||||
|
||||
// Get a table row, select it and add to the dropzone
|
||||
cy.get('div[class="toolbar-card ui-draggable ui-draggable-handle"]').eq(2).should('be.visible').click({force: true}).then(() => {
|
||||
cy.get('#timeline-overlay-container').click({force: true}).then(() => {
|
||||
|
||||
// Wait for the layout to reload
|
||||
// cy.wait('@reloadPlaylist');
|
||||
cy.wait(3000);
|
||||
|
||||
// Check if there is just one widget in the timeline
|
||||
cy.get('#timeline-container [data-type="widget"]').then(($widgets) => {
|
||||
expect($widgets.length).to.eq(3);
|
||||
});
|
||||
|
||||
// Click the revert button
|
||||
cy.get('#timeline-container [id^="widget_"]').last().click();
|
||||
cy.get('button[data-action="undo"]').click({force: true});
|
||||
|
||||
// Wait for the widget to be deleted and for the playlist to reload
|
||||
// cy.wait('@deleteWidget');
|
||||
// cy.wait('@reloadPlaylist');
|
||||
cy.wait(3000);
|
||||
|
||||
// Check if there is just one widget in the timeline
|
||||
cy.get('#timeline-container [data-type="widget"]').then(($widgets) => {
|
||||
expect($widgets.length).to.eq(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
107
cypress/e2e/Library/playlists.cy.js
Normal file
107
cypress/e2e/Library/playlists.cy.js
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Playlists Admin', function() {
|
||||
let testRun;
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
testRun = Cypress._.random(0, 1e9);
|
||||
});
|
||||
|
||||
it('should add a non-dynamic playlist', function() {
|
||||
cy.visit('/playlist/view');
|
||||
|
||||
// Click on the Add Playlist button
|
||||
cy.contains('Add Playlist').click();
|
||||
|
||||
cy.get('.modal input#name')
|
||||
.type('Cypress Test Playlist ' + testRun);
|
||||
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Filter for the created playlist
|
||||
cy.get('#Filter input[name="name"]')
|
||||
.type('Cypress Test Playlist ' + testRun);
|
||||
|
||||
// Should have the added playlist
|
||||
cy.get('#playlists tbody tr').should('have.length', 1);
|
||||
cy.get('#playlists tbody tr:nth-child(1) td:nth-child(2)').contains('Cypress Test Playlist ' + testRun);
|
||||
});
|
||||
|
||||
it('should cancel adding a non-dynamic playlist', function() {
|
||||
cy.visit('/playlist/view');
|
||||
|
||||
// Click on the Add Playlist button
|
||||
cy.contains('Add Playlist').click();
|
||||
|
||||
cy.get('.modal input#name')
|
||||
.type('Cypress Test Playlist ' + testRun);
|
||||
|
||||
// Click cancel
|
||||
cy.get('#dialog_btn_1').click();
|
||||
|
||||
// Check if you are back to the view page
|
||||
cy.url().should('include', '/playlist/view');
|
||||
});
|
||||
|
||||
it('should show a list of Playlists', function() {
|
||||
// Wait for playlist grid reload
|
||||
cy.intercept('/playlist?draw=1&*').as('playlistGridLoad');
|
||||
|
||||
cy.visit('/playlist/view').then(function() {
|
||||
cy.wait('@playlistGridLoad');
|
||||
cy.get('#playlists');
|
||||
});
|
||||
});
|
||||
|
||||
it('selects multiple playlists and delete them', function() {
|
||||
// Create a new playlist and then search for it and delete it
|
||||
cy.createNonDynamicPlaylist('Cypress Test Playlist ' + testRun).then(() => {
|
||||
cy.intercept('/playlist?draw=2&*').as('playlistGridLoad');
|
||||
|
||||
// Delete all test playlists
|
||||
cy.visit('/playlist/view');
|
||||
|
||||
// Clear filter and search for text playlists
|
||||
cy.get('#Filter input[name="name"]')
|
||||
.clear()
|
||||
.type('Cypress Test Playlist');
|
||||
|
||||
// Wait for 2nd playlist grid reload
|
||||
cy.wait('@playlistGridLoad');
|
||||
|
||||
// Select all
|
||||
cy.get('button[data-toggle="selectAll"]').click();
|
||||
|
||||
// Delete all
|
||||
cy.get('.dataTables_info button[data-toggle="dropdown"]').click({force: true});
|
||||
cy.get('.dataTables_info a[data-button-id="playlist_button_delete"]').click({force: true});
|
||||
|
||||
cy.get('button.save-button').click();
|
||||
|
||||
// Modal should contain one successful delete at least
|
||||
cy.get('.modal-body').contains(': Success');
|
||||
});
|
||||
});
|
||||
});
|
||||
61
cypress/e2e/Reporting/report_bandwidth.cy.js
Normal file
61
cypress/e2e/Reporting/report_bandwidth.cy.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Bandwidth', function() {
|
||||
const display1 = 'POP Display 1';
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('should load tabular data and charts', () => {
|
||||
// Create and alias for load Display
|
||||
cy.intercept({
|
||||
url: '/display?start=*',
|
||||
query: {display: display1},
|
||||
}).as('loadDisplayAfterSearch');
|
||||
|
||||
cy.intercept('/report/data/bandwidth?*').as('reportData');
|
||||
|
||||
cy.visit('/report/form/bandwidth');
|
||||
|
||||
// Click on the select2 selection
|
||||
cy.get('#displayId + span .select2-selection').click();
|
||||
cy.get('.select2-container--open input[type="search"]').type(display1);
|
||||
cy.wait('@loadDisplayAfterSearch');
|
||||
cy.selectOption(display1);
|
||||
|
||||
// Click on the Apply button
|
||||
cy.contains('Apply').should('be.visible').click();
|
||||
|
||||
cy.get('.chart-container').should('be.visible');
|
||||
|
||||
// Click on Tabular
|
||||
cy.contains('Tabular').should('be.visible').click();
|
||||
cy.wait('@reportData');
|
||||
|
||||
// Should have media stats
|
||||
cy.get('#bandwidthTbl tbody tr:nth-child(1) td:nth-child(1)').contains('Submit Stats');
|
||||
cy.get('#bandwidthTbl tbody tr:nth-child(1) td:nth-child(2)').contains(200); // Bandwidth
|
||||
cy.get('#bandwidthTbl tbody tr:nth-child(1) td:nth-child(3)').contains('bytes'); // Unit
|
||||
});
|
||||
});
|
||||
130
cypress/e2e/Reporting/report_distribution.cy.js
Normal file
130
cypress/e2e/Reporting/report_distribution.cy.js
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Distribution by Layout, Media or Event', function() {
|
||||
const display1 = 'POP Display 1';
|
||||
const layout1 = 'POP Layout 1';
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('Range: Today, Checks duration and count of a layout stat', () => {
|
||||
// Create and alias for load layout
|
||||
cy.intercept({
|
||||
url: '/display?start=*',
|
||||
query: {display: display1},
|
||||
}).as('loadDisplayAfterSearch');
|
||||
|
||||
cy.intercept({
|
||||
url: '/layout?start=*',
|
||||
query: {layout: layout1},
|
||||
}).as('loadLayoutAfterSearch');
|
||||
|
||||
cy.intercept('/report/data/distributionReport?*').as('reportData');
|
||||
|
||||
cy.visit('/report/form/distributionReport');
|
||||
|
||||
// Click on the select2 selection
|
||||
cy.get('#displayId + span .select2-selection').click();
|
||||
cy.get('.select2-container--open input[type="search"]').type(display1);
|
||||
cy.wait('@loadDisplayAfterSearch');
|
||||
cy.selectOption(display1);
|
||||
|
||||
// Click on the select2 selection
|
||||
cy.get('#layoutId + span .select2-selection').click();
|
||||
cy.get('.select2-container--open input[type="search"]').type(layout1);
|
||||
cy.wait('@loadLayoutAfterSearch');
|
||||
cy.selectOption(layout1);
|
||||
|
||||
// Click on the Apply button
|
||||
cy.contains('Apply').should('be.visible').click();
|
||||
|
||||
cy.get('.chart-container').should('be.visible');
|
||||
|
||||
// Click on Tabular
|
||||
cy.contains('Tabular').should('be.visible').click();
|
||||
cy.contains('Next').should('be.visible').click();
|
||||
cy.wait('@reportData');
|
||||
|
||||
// Should have media stats
|
||||
cy.get('#distributionTbl tbody tr:nth-child(3) td:nth-child(1)').contains('12:00 PM'); // Period
|
||||
cy.get('#distributionTbl tbody tr:nth-child(3) td:nth-child(2)').contains(60); // Duration
|
||||
cy.get('#distributionTbl tbody tr:nth-child(3) td:nth-child(3)').contains(1); // Count
|
||||
});
|
||||
|
||||
it.skip('Create/Delete a Daily Distribution Report Schedule', () => {
|
||||
const reportschedule = 'Daily Distribution by Layout 1 and Display 1';
|
||||
|
||||
// Create and alias for load layout
|
||||
cy.intercept({
|
||||
url: '/display?start=*',
|
||||
query: {display: display1},
|
||||
}).as('loadDisplayAfterSearch');
|
||||
|
||||
cy.intercept({
|
||||
url: '/layout?start=*',
|
||||
query: {layout: layout1},
|
||||
}).as('loadLayoutAfterSearch');
|
||||
|
||||
cy.intercept({
|
||||
url: '/report/reportschedule?*',
|
||||
query: {name: reportschedule},
|
||||
}).as('loadReportScheduleAfterSearch');
|
||||
|
||||
cy.visit('/report/form/distributionReport');
|
||||
|
||||
// Click on the select2 selection
|
||||
cy.get('#layoutId + span .select2-selection').click();
|
||||
cy.get('.select2-container--open input[type="search"]').type(layout1);
|
||||
cy.wait('@loadLayoutAfterSearch');
|
||||
cy.selectOption(layout1);
|
||||
|
||||
// ------
|
||||
// ------
|
||||
// Create a Daily Distribution Report Schedule
|
||||
cy.get('#reportAddBtn').click();
|
||||
cy.get('#reportScheduleAddForm #name ').type(reportschedule);
|
||||
|
||||
// Click on the select2 selection
|
||||
cy.get('#reportScheduleAddForm #displayId + span .select2-selection').click();
|
||||
cy.get('.select2-container--open input[type="search"]').type(display1);
|
||||
cy.wait('@loadDisplayAfterSearch');
|
||||
cy.selectOption(display1);
|
||||
|
||||
cy.get('#dialog_btn_2').should('be.visible').click();
|
||||
|
||||
cy.visit('/report/reportschedule/view');
|
||||
cy.get('#name').type(reportschedule);
|
||||
cy.wait('@loadReportScheduleAfterSearch');
|
||||
|
||||
// Click on the first row element to open the designer
|
||||
cy.get('#reportschedules_wrapper tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#reportschedules_wrapper tr:first-child .reportschedule_button_delete').click({force: true});
|
||||
|
||||
// Delete test campaign
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check if layout is deleted in toast message
|
||||
cy.get('.toast').contains('Deleted ' + reportschedule);
|
||||
});
|
||||
});
|
||||
35
cypress/e2e/Reporting/report_library_usage.cy.js
Normal file
35
cypress/e2e/Reporting/report_library_usage.cy.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Library Usage', function() {
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('should load tabular data and charts', () => {
|
||||
cy.visit('/report/form/libraryusage');
|
||||
|
||||
cy.get('#libraryUsage_wrapper').should('be.visible');
|
||||
cy.get('#libraryChart').should('be.visible');
|
||||
cy.get('#userChart').should('be.visible');
|
||||
});
|
||||
});
|
||||
183
cypress/e2e/Reporting/report_proofofplay.cy.js
Normal file
183
cypress/e2e/Reporting/report_proofofplay.cy.js
Normal file
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Proof of Play', function() {
|
||||
const display1 = 'POP Display 1';
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('Range: Test export', function() {
|
||||
// Create and alias for load displays
|
||||
cy.intercept({
|
||||
url: '/display?start=*',
|
||||
query: {display: display1},
|
||||
}).as('loadDisplayAfterSearch');
|
||||
|
||||
cy.visit('/report/view');
|
||||
cy.contains('Export').click();
|
||||
|
||||
cy.get(':nth-child(1) > .col-sm-10 > .input-group > .flatpickr-wrapper > .datePickerHelper').click();
|
||||
cy.get('.open > .flatpickr-innerContainer > .flatpickr-rContainer > .flatpickr-days > .dayContainer > .today').click();
|
||||
cy.get(':nth-child(2) > .col-sm-10 > .input-group > .flatpickr-wrapper > .datePickerHelper').click();
|
||||
cy.get('.open > .flatpickr-innerContainer > .flatpickr-rContainer > .flatpickr-days > .dayContainer > .today').next().click();
|
||||
|
||||
// Click on the select2 selection
|
||||
cy.get('#displayId + span .select2-selection').click();
|
||||
cy.get('.select2-container--open input[type="search"]').type(display1);
|
||||
cy.wait('@loadDisplayAfterSearch');
|
||||
cy.selectOption(display1);
|
||||
|
||||
cy.get('.total-stat').contains('Total number of records to be exported 5');
|
||||
});
|
||||
|
||||
it('Range: Today - Test layout/media stats for a layout and a display', function() {
|
||||
cy.intercept('/report/data/proofofplayReport?*').as('reportData');
|
||||
|
||||
cy.visit('/report/form/proofofplayReport');
|
||||
|
||||
cy.get('#type').select('media');
|
||||
|
||||
// Click on the Apply button
|
||||
cy.contains('Apply').click();
|
||||
|
||||
// Wait for
|
||||
cy.wait('@reportData');
|
||||
cy.get('#stats tbody').contains('media');
|
||||
|
||||
// Should have media stats - Test media stats for a layout and a display
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(1)').contains('media'); // stat type
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(3)').contains('POP Display 1'); // display
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(6)').contains('POP Layout 1'); // layout
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(8)').contains('child_folder_media'); // media
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(10)').contains(2); // number of plays
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(12)').contains(120); // total duration
|
||||
|
||||
|
||||
cy.get('#type').select('layout');
|
||||
|
||||
// Click on the Apply button
|
||||
cy.contains('Apply').click();
|
||||
|
||||
// Wait for
|
||||
cy.wait('@reportData');
|
||||
cy.contains('Tabular').should('be.visible').click();
|
||||
|
||||
cy.get('#stats tbody').contains('layout');
|
||||
|
||||
// Should have layout stat - Test a layout stat for an ad campaign, a layout and a display
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(1)').contains('layout'); // stat type
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(3)').contains('POP Display 1'); // display
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(4)').contains('POP Ad Campaign 1'); // ad campaign
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(6)').contains('POP Layout 1'); // layout
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(10)').contains(1); // number of plays
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(12)').contains(60); // total duration
|
||||
});
|
||||
|
||||
it('Range: Lastweek - Test media stats for a layout and a display', function() {
|
||||
cy.intercept('/report/data/proofofplayReport?*').as('reportData');
|
||||
|
||||
cy.visit('/report/form/proofofplayReport');
|
||||
|
||||
// Range: Lastweek
|
||||
cy.get('#reportFilter').select('lastweek');
|
||||
|
||||
cy.get('#type').select('media');
|
||||
|
||||
// Click on the Apply button
|
||||
cy.contains('Apply').click();
|
||||
|
||||
// Wait for
|
||||
cy.wait('@reportData');
|
||||
|
||||
cy.get('#stats').within(() => {
|
||||
// Check if the "No data available in table" message is not present
|
||||
cy.contains('No data available in table').should('not.exist');
|
||||
cy.get('tbody tr').should('have.length', 1);
|
||||
// Should have media stats
|
||||
cy.get('tbody td').eq(0).should('contain', 'media'); // stat type
|
||||
cy.get('tbody td').eq(2).contains('POP Display 1'); // display
|
||||
cy.get('tbody td').eq(5).contains('POP Layout 1'); // layout
|
||||
cy.get('tbody td').eq(7).contains('child_folder_media'); // media
|
||||
cy.get('tbody td').eq(9).contains(2); // number of plays
|
||||
cy.get('tbody td').eq(11).contains(120); // total duration
|
||||
});
|
||||
|
||||
cy.get('#type').select('layout');
|
||||
|
||||
// Click on the Apply button
|
||||
cy.contains('Apply').click();
|
||||
|
||||
// Wait for
|
||||
cy.wait('@reportData');
|
||||
|
||||
cy.get('#stats').within(() => {
|
||||
// Check if the "No data available in table" message is not present
|
||||
cy.contains('No data available in table').should('not.exist');
|
||||
cy.get('tbody tr').should('have.length', 1);
|
||||
// Should have layout stat - Test a layout stat for an ad campaign, a layout and a display
|
||||
cy.get('tbody td').eq(0).contains('layout'); // stat type
|
||||
cy.get('tbody td').eq(2).contains('POP Display 1'); // display
|
||||
cy.get('tbody td').eq(3).contains('POP Ad Campaign 1'); // ad campaign
|
||||
cy.get('tbody td').eq(5).contains('POP Layout 1'); // layout
|
||||
cy.get('tbody td').eq(9).contains(1); // number of plays
|
||||
cy.get('tbody td').eq(11).contains(60); // total duration
|
||||
});
|
||||
});
|
||||
|
||||
it('Range: Today - Test event/widget stats for a layout and a display', function() {
|
||||
cy.intercept('/report/data/proofofplayReport?*').as('reportData');
|
||||
|
||||
cy.visit('/report/form/proofofplayReport');
|
||||
|
||||
cy.get('#type').select('event');
|
||||
|
||||
// Click on the Apply button
|
||||
cy.contains('Apply').click();
|
||||
|
||||
// Wait for
|
||||
cy.wait('@reportData');
|
||||
|
||||
// Should have media stats - Test media stats for a layout and a display
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(1)').contains('event'); // stat type
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(3)').contains('POP Display 1'); // display
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(9)').contains('Event123'); // tag/eventname
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(10)').contains(1); // number of plays
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(12)').contains(60); // total duration
|
||||
|
||||
cy.get('#type').select('widget');
|
||||
|
||||
// Click on the Apply button
|
||||
cy.contains('Apply').click();
|
||||
|
||||
// Wait for
|
||||
cy.wait('@reportData');
|
||||
cy.contains('Tabular').should('be.visible').click();
|
||||
|
||||
// Should have layout stat - Test a layout stat for an ad campaign, a layout and a display
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(1)').contains('widget'); // stat type
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(3)').contains('POP Display 1'); // display
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(6)').contains('POP Layout 1'); // layout
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(10)').contains(1); // number of plays
|
||||
cy.get('#stats tbody tr:nth-child(1) td:nth-child(12)').contains(60); // total duration
|
||||
});
|
||||
});
|
||||
134
cypress/e2e/Reporting/report_summary.cy.js
Normal file
134
cypress/e2e/Reporting/report_summary.cy.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Summary by Layout, Media or Event', function() {
|
||||
const display1 = 'POP Display 1';
|
||||
const layout1 = 'POP Layout 1';
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('Range: Today, Checks duration and count of a layout stat', () => {
|
||||
// Create alias
|
||||
cy.intercept({
|
||||
url: '/display?start=*',
|
||||
query: {display: display1},
|
||||
}).as('loadDisplayAfterSearch');
|
||||
|
||||
cy.intercept({
|
||||
url: '/layout?start=*',
|
||||
query: {layout: layout1},
|
||||
}).as('loadLayoutAfterSearch');
|
||||
|
||||
cy.intercept('/report/data/summaryReport?*').as('reportData');
|
||||
|
||||
cy.visit('/report/form/summaryReport');
|
||||
|
||||
// Click on the select2 selection
|
||||
cy.get('#displayId + span .select2-selection').click();
|
||||
cy.get('.select2-container--open input[type="search"]').type(display1);
|
||||
cy.wait('@loadDisplayAfterSearch');
|
||||
cy.selectOption(display1);
|
||||
|
||||
// Click on the select2 selection
|
||||
cy.get('#layoutId + span .select2-selection').click();
|
||||
cy.get('.select2-container--open input[type="search"]').type(layout1);
|
||||
cy.wait('@loadLayoutAfterSearch');
|
||||
cy.selectOption(layout1);
|
||||
|
||||
// Click on the Apply button
|
||||
cy.contains('Apply').should('be.visible').click();
|
||||
// Wait for report data
|
||||
cy.wait('@reportData');
|
||||
|
||||
cy.get('.chart-container').should('be.visible');
|
||||
|
||||
// Click on Tabular
|
||||
cy.contains('Tabular').should('be.visible').click();
|
||||
cy.contains('Next').should('be.visible').click();
|
||||
|
||||
// Should have media stats
|
||||
cy.get('#summaryTbl tbody tr:nth-child(3) td:nth-child(1)').contains('12:00 PM'); // Period
|
||||
cy.get('#summaryTbl tbody tr:nth-child(3) td:nth-child(2)').contains(60); // Duration
|
||||
cy.get('#summaryTbl tbody tr:nth-child(3) td:nth-child(3)').contains(1); // Count
|
||||
});
|
||||
|
||||
it('Create/Delete a Daily Summary Report Schedule', () => {
|
||||
const reportschedule = 'Daily Summary by Layout 1 and Display 1';
|
||||
|
||||
// Create and alias for load layout
|
||||
cy.intercept('/report/reportschedule/form/add*').as('reportScheduleAddForm');
|
||||
cy.intercept({
|
||||
url: '/display?start=*',
|
||||
query: {display: display1},
|
||||
}).as('loadDisplayAfterSearch');
|
||||
|
||||
cy.intercept({
|
||||
url: '/layout?start=*',
|
||||
query: {layout: layout1},
|
||||
}).as('loadLayoutAfterSearch');
|
||||
|
||||
cy.intercept({
|
||||
url: '/report/reportschedule?*',
|
||||
query: {name: reportschedule},
|
||||
}).as('loadReportScheduleAfterSearch');
|
||||
|
||||
cy.visit('/report/form/summaryReport');
|
||||
|
||||
// Click on the select2 selection
|
||||
cy.get('#layoutId + span .select2-selection').click();
|
||||
cy.get('.select2-container--open input[type="search"]').type(layout1);
|
||||
cy.wait('@loadLayoutAfterSearch');
|
||||
cy.selectOption(layout1);
|
||||
|
||||
// ------
|
||||
// ------
|
||||
// Create a Daily Summary Report Schedule
|
||||
cy.get('#reportAddBtn').click();
|
||||
cy.wait('@reportScheduleAddForm');
|
||||
cy.get('#reportScheduleAddForm #name ').type(reportschedule);
|
||||
|
||||
// Click on the select2 selection
|
||||
cy.get('#reportScheduleAddForm #displayId + span .select2-selection').click();
|
||||
cy.get('.select2-container--open input[type="search"]').type(display1);
|
||||
cy.wait('@loadDisplayAfterSearch');
|
||||
cy.selectOption(display1);
|
||||
|
||||
|
||||
cy.get('#dialog_btn_2').should('be.visible').click();
|
||||
|
||||
cy.visit('/report/reportschedule/view');
|
||||
cy.get('#name').type(reportschedule);
|
||||
cy.wait('@loadReportScheduleAfterSearch');
|
||||
|
||||
// Click on the first row element to open the designer
|
||||
cy.get('#reportschedules_wrapper tr:first-child .dropdown-toggle').click({force: true});
|
||||
|
||||
cy.get('#reportschedules_wrapper tr:first-child .reportschedule_button_delete').click({force: true});
|
||||
|
||||
// Delete test campaign
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check if layout is deleted in toast message
|
||||
cy.get('.toast').contains('Deleted ' + reportschedule);
|
||||
});
|
||||
});
|
||||
45
cypress/e2e/Reporting/report_timeconnected.cy.js
Normal file
45
cypress/e2e/Reporting/report_timeconnected.cy.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Time Connected', function() {
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('should load time connected data of displays', () => {
|
||||
cy.visit('/report/form/timeconnected');
|
||||
|
||||
// Click on the select2 selection
|
||||
cy.get('.select2-search__field').click();
|
||||
|
||||
// Type the display name
|
||||
cy.get('.select2-container--open textarea[type="search"]').type('POP Display Group');
|
||||
cy.get('.select2-container--open .select2-results > ul').contains('POP Display Group').click();
|
||||
|
||||
// Click on the Apply button
|
||||
cy.contains('Apply').should('be.visible').click();
|
||||
|
||||
// Should have media stats
|
||||
cy.get('#records_table tr:nth-child(1) th:nth-child(1)').contains('POP Display 1');
|
||||
cy.get('#records_table tr:nth-child(2) td:nth-child(2)').contains('100%');
|
||||
});
|
||||
});
|
||||
59
cypress/e2e/Reporting/report_timeconnectedsummary.cy.js
Normal file
59
cypress/e2e/Reporting/report_timeconnectedsummary.cy.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Time Connected', function() {
|
||||
const display1 = 'POP Display 1';
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('should load time connected data of displays', () => {
|
||||
// Create and alias for load display
|
||||
cy.intercept({
|
||||
url: '/display?start=*',
|
||||
query: {display: display1},
|
||||
}).as('loadDisplayAfterSearch');
|
||||
|
||||
cy.visit('/report/form/timedisconnectedsummary');
|
||||
|
||||
// Click on the select2 selection
|
||||
cy.get('#displayId + span .select2-selection').click();
|
||||
cy.get('.select2-container--open input[type="search"]').type(display1);
|
||||
cy.wait('@loadDisplayAfterSearch');
|
||||
cy.selectOption(display1);
|
||||
|
||||
// Select "Yesterday" from the dropdown
|
||||
cy.get('#reportFilter').select('yesterday');
|
||||
|
||||
// Click on the Apply button
|
||||
cy.contains('Apply').should('be.visible').click();
|
||||
|
||||
cy.get('.chart-container').should('be.visible');
|
||||
|
||||
// Click on Tabular
|
||||
cy.contains('Tabular').should('be.visible').click();
|
||||
|
||||
// Should have media stats
|
||||
cy.get('#timeDisconnectedTbl tr:nth-child(1) td:nth-child(2)').contains('POP Display 1');
|
||||
cy.get('#timeDisconnectedTbl tr:nth-child(1) td:nth-child(3)').contains('10');
|
||||
});
|
||||
});
|
||||
220
cypress/e2e/Schedule/dayparts.cy.js
Normal file
220
cypress/e2e/Schedule/dayparts.cy.js
Normal file
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Dayparts', function() {
|
||||
let testRun = '';
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
|
||||
testRun = Cypress._.random(0, 1e9);
|
||||
});
|
||||
|
||||
it('should add a daypart', function() {
|
||||
cy.visit('/daypart/view');
|
||||
|
||||
// Click on the Add Daypart button
|
||||
cy.contains('Add Daypart').click();
|
||||
|
||||
cy.get('.modal input#name')
|
||||
.type('Cypress Test Daypart ' + testRun + '_1');
|
||||
|
||||
cy.get(':nth-child(3) > .col-sm-10 > .input-group > .flatpickr-wrapper > .datePickerHelper').click();
|
||||
// cy.get('.open > .flatpickr-time > :nth-child(1) > .arrowUp').click();
|
||||
cy.get('.open > .flatpickr-time > :nth-child(1) > .numInput').type('8');
|
||||
cy.get(':nth-child(4) > .col-sm-10 > .input-group > .flatpickr-wrapper > .datePickerHelper').click();
|
||||
cy.get('.open > .flatpickr-time > :nth-child(1) > .numInput').type('17');
|
||||
|
||||
// Add first by clicking next
|
||||
cy.get('.modal .save-button').click();
|
||||
|
||||
// Check if daypart is added in toast message
|
||||
cy.contains('Added Cypress Test Daypart ' + testRun + '_1');
|
||||
});
|
||||
|
||||
it('searches and edit existing daypart', function() {
|
||||
// Create a new daypart and then search for it and edit it
|
||||
cy.createDayPart('Cypress Test Daypart ' + testRun).then((id) => {
|
||||
cy.intercept({
|
||||
url: '/daypart?*',
|
||||
query: {name: 'Cypress Test Daypart ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Intercept the PUT request
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '/daypart/*',
|
||||
}).as('putRequest');
|
||||
|
||||
cy.visit('/daypart/view');
|
||||
|
||||
// Filter for the created daypart
|
||||
cy.get('#Filter input[name="name"]')
|
||||
.type('Cypress Test Daypart ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#dayparts tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#dayparts tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#dayparts tr:first-child .daypart_button_edit').click({force: true});
|
||||
|
||||
cy.get('.modal input#name').clear()
|
||||
.type('Cypress Test Daypart Edited ' + testRun);
|
||||
|
||||
// edit test daypart
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Wait for the intercepted PUT request and check the form data
|
||||
cy.wait('@putRequest').then((interception) => {
|
||||
// Get the request body (form data)
|
||||
const response = interception.response;
|
||||
const responseData = response.body.data;
|
||||
|
||||
// assertion on the "daypart" value
|
||||
expect(responseData.name).to.eq('Cypress Test Daypart Edited ' + testRun);
|
||||
});
|
||||
|
||||
// Delete the daypart and assert success
|
||||
cy.deleteDayPart(id).then((res) => {
|
||||
expect(res.status).to.equal(204);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('searches and delete existing daypart', function() {
|
||||
// Create a new daypart and then search for it and delete it
|
||||
cy.createDayPart('Cypress Test Daypart ' + testRun).then((res) => {
|
||||
cy.intercept({
|
||||
url: '/daypart?*',
|
||||
query: {name: 'Cypress Test Daypart ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
cy.visit('/daypart/view');
|
||||
|
||||
// Filter for the created daypart
|
||||
cy.get('#Filter input[name="name"]')
|
||||
.type('Cypress Test Daypart ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#dayparts tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#dayparts tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#dayparts tr:first-child .daypart_button_delete').click({force: true});
|
||||
|
||||
// Delete test daypart
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check if daypart is deleted in toast message
|
||||
cy.get('.toast').contains('Deleted Cypress Test Daypart');
|
||||
});
|
||||
});
|
||||
|
||||
it('searches and share existing daypart', function() {
|
||||
// Create a new daypart and then search for it and share it
|
||||
cy.createDayPart('Cypress Test Daypart ' + testRun).then((res) => {
|
||||
cy.intercept({
|
||||
url: '/daypart?*',
|
||||
query: {name: 'Cypress Test Daypart ' + testRun},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
cy.intercept({
|
||||
query: {name: 'Everyone'},
|
||||
url: /\/user\/permissions\/DayPart\/\d+\?draw=2/,
|
||||
}).as('draw2');
|
||||
|
||||
cy.intercept({
|
||||
query: {name: 'Everyone'},
|
||||
url: /\/user\/permissions\/DayPart\/\d+\?draw=3/,
|
||||
}).as('draw3');
|
||||
|
||||
cy.visit('/daypart/view');
|
||||
|
||||
// Filter for the created daypart
|
||||
cy.get('#Filter input[name="name"]')
|
||||
.type('Cypress Test Daypart ' + testRun);
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
cy.get('#dayparts tbody tr').should('have.length', 1);
|
||||
|
||||
// Click on the first row element to open the delete modal
|
||||
cy.get('#dayparts tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#dayparts tr:first-child .daypart_button_permissions').click({force: true});
|
||||
|
||||
cy.get('.modal #name').type('Everyone');
|
||||
cy.wait('@draw2');
|
||||
cy.get('#permissionsTable tbody tr').should('have.length', 1);
|
||||
|
||||
cy.get('#permissionsTable').within(() => {
|
||||
cy.get('input[type="checkbox"][data-permission="view"]').should('be.visible').check();
|
||||
|
||||
// DOM is refreshed at this point, so wait for it
|
||||
cy.wait('@draw3');
|
||||
// We have no other option but to put {force: true} here
|
||||
cy.get('input[type="checkbox"][data-permission="edit"]').check();
|
||||
});
|
||||
|
||||
// Save
|
||||
cy.get('.bootbox .save-button').click();
|
||||
|
||||
// Check if daypart is deleted in toast message
|
||||
cy.get('.toast').contains('Share option Updated');
|
||||
});
|
||||
});
|
||||
|
||||
it('selects multiple dayparts and delete them', function() {
|
||||
// Create a new daypart and then search for it and delete it
|
||||
cy.createDayPart('Cypress Test Daypart ' + testRun).then((res) => {
|
||||
cy.intercept({
|
||||
url: '/daypart?*',
|
||||
query: {name: 'Cypress Test Daypart'},
|
||||
}).as('loadGridAfterSearch');
|
||||
|
||||
// Delete all test dayparts
|
||||
cy.visit('/daypart/view');
|
||||
|
||||
// Clear filter
|
||||
cy.get('#Filter input[name="name"]')
|
||||
.clear()
|
||||
.type('Cypress Test Daypart');
|
||||
|
||||
// Wait for the grid reload
|
||||
cy.wait('@loadGridAfterSearch');
|
||||
|
||||
// Select all
|
||||
cy.get('button[data-toggle="selectAll"]').click();
|
||||
|
||||
// Delete all
|
||||
cy.get('.dataTables_info button[data-toggle="dropdown"]').click();
|
||||
cy.get('.dataTables_info a[data-button-id="daypart_button_delete"]').click();
|
||||
|
||||
cy.get('button.save-button').click();
|
||||
|
||||
// Modal should contain one successful delete at least
|
||||
cy.get('.modal-body').contains(': Success');
|
||||
});
|
||||
});
|
||||
});
|
||||
392
cypress/e2e/Schedule/schedule.cy.js
Normal file
392
cypress/e2e/Schedule/schedule.cy.js
Normal file
@@ -0,0 +1,392 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
describe('Schedule Events', function() {
|
||||
// Seeded Data
|
||||
const campaignSchedule1 = 'Campaign for Schedule 1';
|
||||
const layoutSchedule1 = 'Layout for Schedule 1';
|
||||
|
||||
const display1 = 'List Campaign Display 1';
|
||||
const display2 = 'List Campaign Display 2';
|
||||
const command1 = 'Set Timezone';
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('should list all scheduled events', function() {
|
||||
// Make a GET request to the API endpoint '/schedule/data/events'??
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: '/schedule/data/events',
|
||||
}).then((response) => {
|
||||
// Assertions on the response
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.body).to.have.property('result');
|
||||
});
|
||||
});
|
||||
|
||||
// TC-01
|
||||
it('should schedule an event layout that has no priority, no recurrence', function() {
|
||||
cy.intercept('GET', '/schedule/form/add?*').as('scheduleAddForm');
|
||||
|
||||
// Set up intercepts with aliases
|
||||
cy.intercept({
|
||||
url: '/display?start=*',
|
||||
query: {display: display1},
|
||||
}).as('loadDisplayAfterSearch');
|
||||
|
||||
cy.intercept({
|
||||
url: '/displaygroup?*',
|
||||
query: {displayGroup: display1},
|
||||
}).as('loadDisplaygroupAfterSearch');
|
||||
|
||||
cy.intercept({
|
||||
url: '/campaign?type=list*',
|
||||
query: {name: layoutSchedule1},
|
||||
}).as('loadListCampaignsAfterSearch');
|
||||
|
||||
// Click on the Add Event button
|
||||
cy.visit('/schedule/view');
|
||||
|
||||
cy.contains('Clear Filters').should('be.visible').click();
|
||||
cy.contains('Add Event').click();
|
||||
|
||||
cy.get('.bootbox.modal')
|
||||
.should('be.visible') // essential: Ensure the modal is visible
|
||||
.then(() => {
|
||||
cy.get('.modal-content #eventTypeId').select('Layout');
|
||||
// Select layout
|
||||
cy.selectFromDropdown('.layout-control .select2-selection', layoutSchedule1, layoutSchedule1, '@loadListCampaignsAfterSearch');
|
||||
|
||||
// Click Next and check toast message
|
||||
cy.get('.modal .modal-footer').contains('Next').click();
|
||||
|
||||
// Select display
|
||||
cy.selectFromDropdown('.display-group-control .select2-selection', display1, display1, '@loadDisplaygroupAfterSearch', 1);
|
||||
|
||||
// Click Next and check toast message
|
||||
cy.get('.modal .modal-footer').contains('Next').click();
|
||||
|
||||
// Select day part and set name
|
||||
cy.get('.modal-content [name="dayPartId"]').select('Always');
|
||||
|
||||
// Click Next and check toast message
|
||||
cy.get('.modal .modal-footer').contains('Next').click();
|
||||
cy.get('.modal-content [name="name"]').type('Always - Layout Event');
|
||||
|
||||
cy.get('.modal .modal-footer').contains('Finish').click();
|
||||
|
||||
cy.contains('Added Event');
|
||||
});
|
||||
|
||||
// Validate - schedule creation should be successful
|
||||
cy.visit('/schedule/view');
|
||||
cy.contains('Clear Filters').should('be.visible').click();
|
||||
|
||||
cy.get('#DisplayList + span .select2-selection').click();
|
||||
|
||||
// Type the display name
|
||||
cy.get('.select2-container--open textarea[type="search"]').type(display1);
|
||||
|
||||
// Wait for Display to load
|
||||
cy.wait('@loadDisplayAfterSearch');
|
||||
cy.get('.select2-container--open').contains(display1);
|
||||
cy.get('.select2-container--open .select2-results > ul > li').should('have.length', 1);
|
||||
|
||||
// Select the display from the dropdown
|
||||
cy.get('.select2-container--open .select2-results > ul > li:first').contains(display1).click();
|
||||
|
||||
// Verify that the schedule is successfully created and listed in the grid
|
||||
cy.get('#schedule-grid').contains(layoutSchedule1);
|
||||
|
||||
// Should have 1
|
||||
cy.get('#schedule-grid tbody tr').should('have.length', 1);
|
||||
});
|
||||
|
||||
// relies on TC-01
|
||||
it('should edit a scheduled event', function() {
|
||||
cy.intercept('GET', '/schedule/form/add?*').as('scheduleAddForm');
|
||||
|
||||
// Set up intercepts with aliases
|
||||
cy.intercept({
|
||||
url: '/display?start=*',
|
||||
query: {display: display1},
|
||||
}).as('loadDisplayAfterSearch');
|
||||
|
||||
cy.intercept({
|
||||
url: '/displaygroup?*',
|
||||
query: {displayGroup: display2},
|
||||
}).as('loadDisplaygroupAfterSearch');
|
||||
|
||||
cy.intercept({
|
||||
url: '/campaign?type=list*',
|
||||
query: {name: layoutSchedule1},
|
||||
}).as('loadListCampaignsAfterSearch');
|
||||
|
||||
// Click on the Add Event button
|
||||
cy.visit('/schedule/view');
|
||||
|
||||
cy.contains('Clear Filters').should('be.visible').click();
|
||||
|
||||
cy.get('#DisplayList + span .select2-selection').click();
|
||||
|
||||
// Type the display name
|
||||
cy.get('.select2-container--open textarea[type="search"]').type(display1);
|
||||
|
||||
// Wait for Display to load
|
||||
cy.wait('@loadDisplayAfterSearch');
|
||||
cy.get('.select2-container--open').contains(display1);
|
||||
cy.get('.select2-container--open .select2-results > ul > li').should('have.length', 1);
|
||||
|
||||
// Select the display from the dropdown
|
||||
cy.get('.select2-container--open .select2-results > ul > li:first').contains(display1).click();
|
||||
|
||||
// Verify that the schedule is successfully created and listed in the grid
|
||||
cy.get('#schedule-grid').contains(layoutSchedule1);
|
||||
|
||||
// Should have 1
|
||||
cy.get('#schedule-grid tbody tr').should('have.length', 1);
|
||||
cy.get('#schedule-grid tr:first-child .dropdown-toggle').click({force: true});
|
||||
cy.get('#schedule-grid tr:first-child .schedule_button_edit').click({force: true});
|
||||
|
||||
cy.contains('.stepwizard-step', 'Displays')
|
||||
.find('a')
|
||||
.click();
|
||||
|
||||
// Select display
|
||||
cy.get('.display-group-control > .col-sm-10 > .select2 > .selection > .select2-selection').type(display2);
|
||||
// Wait for the display group to load after search
|
||||
cy.wait('@loadDisplaygroupAfterSearch');
|
||||
cy.get('.select2-container--open .select2-dropdown .select2-results > ul')
|
||||
.should('contain', display2);
|
||||
cy.get('.select2-container--open .select2-dropdown .select2-results > ul > li')
|
||||
.should('have.length', 2)
|
||||
.last()
|
||||
.click();
|
||||
|
||||
cy.contains('.stepwizard-step', 'Optional')
|
||||
.find('a')
|
||||
.click();
|
||||
|
||||
cy.get('.modal-content [name="name"]').clear().type('Always - Layout Event Edited');
|
||||
|
||||
// Click Next and check toast message
|
||||
cy.get('.modal .modal-footer').contains('Save').click();
|
||||
cy.contains('Edited Event');
|
||||
});
|
||||
|
||||
it('should schedule an event campaign that has no priority, no recurrence', function() {
|
||||
cy.intercept('GET', '/schedule/form/add?*').as('scheduleAddForm');
|
||||
|
||||
// Set up intercepts with aliases
|
||||
cy.intercept({
|
||||
url: '/display?start=*',
|
||||
query: {display: display1},
|
||||
}).as('loadDisplayAfterSearch');
|
||||
|
||||
cy.intercept({
|
||||
url: '/displaygroup?*',
|
||||
query: {displayGroup: display1},
|
||||
}).as('loadDisplaygroupAfterSearch');
|
||||
|
||||
cy.intercept({
|
||||
url: '/campaign?type=list*',
|
||||
query: {name: campaignSchedule1},
|
||||
}).as('loadListCampaignsAfterSearch');
|
||||
|
||||
// Visit the page and click on the Add Event button
|
||||
cy.visit('/schedule/view');
|
||||
|
||||
cy.contains('Clear Filters').should('be.visible').click();
|
||||
cy.contains('Add Event').click();
|
||||
|
||||
cy.get('.bootbox.modal')
|
||||
.should('be.visible') // essential: Ensure the modal is visible
|
||||
.then(() => {
|
||||
cy.get('.modal-content #eventTypeId').select('Campaign');
|
||||
// Select campaign
|
||||
cy.selectFromDropdown('.layout-control .select2-selection', campaignSchedule1, campaignSchedule1, '@loadListCampaignsAfterSearch');
|
||||
|
||||
// Click Next and check toast message
|
||||
cy.get('.modal .modal-footer').contains('Next').click();
|
||||
|
||||
// Select display
|
||||
cy.selectFromDropdown('.display-group-control .select2-selection', display1, display1, '@loadDisplaygroupAfterSearch', 1);
|
||||
|
||||
// Click Next and check toast message
|
||||
cy.get('.modal .modal-footer').contains('Next').click();
|
||||
|
||||
// Select day part and campaign
|
||||
cy.get('.modal-content [name="dayPartId"]').select('Always');
|
||||
|
||||
// Click Next and check toast message
|
||||
cy.get('.modal .modal-footer').contains('Next').click();
|
||||
cy.get('.modal-content [name="name"]').type('Always - Campaign Event');
|
||||
|
||||
cy.get('.modal .modal-footer').contains('Finish').click();
|
||||
|
||||
cy.contains('Added Event');
|
||||
});
|
||||
|
||||
// Validate - schedule creation should be successful
|
||||
cy.visit('/schedule/view');
|
||||
cy.contains('Clear Filters').should('be.visible').click();
|
||||
|
||||
cy.get('#DisplayList + span .select2-selection').click();
|
||||
|
||||
// Type the display name
|
||||
cy.get('.select2-container--open textarea[type="search"]').type(display1);
|
||||
|
||||
// Wait for Display to load
|
||||
cy.wait('@loadDisplayAfterSearch');
|
||||
cy.get('.select2-container--open').contains(display1);
|
||||
cy.get('.select2-container--open .select2-results > ul > li').should('have.length', 1);
|
||||
|
||||
// Select the display from the dropdown
|
||||
cy.get('.select2-container--open .select2-results > ul > li:first').contains(display1).click();
|
||||
|
||||
// Verify that the schedule is successfully created and listed in the grid
|
||||
cy.get('#schedule-grid').contains(campaignSchedule1);
|
||||
});
|
||||
|
||||
it('should schedule an event command layout that has no priority, no recurrence', function() {
|
||||
cy.intercept('GET', '/schedule/form/add?*').as('scheduleAddForm');
|
||||
cy.intercept({
|
||||
url: '/displaygroup?*',
|
||||
query: {displayGroup: display1},
|
||||
}).as('loadDisplaygroupAfterSearch');
|
||||
|
||||
cy.intercept({
|
||||
url: '/command?*',
|
||||
query: {command: command1},
|
||||
}).as('loadCommandAfterSearch');
|
||||
|
||||
// Click on the Add Event button
|
||||
cy.visit('/schedule/view');
|
||||
|
||||
cy.contains('Clear Filters').should('be.visible').click();
|
||||
cy.contains('Add Event').click();
|
||||
|
||||
cy.get('.bootbox.modal')
|
||||
.should('be.visible') // essential: Ensure the modal is visible
|
||||
.then(() => {
|
||||
cy.get('.modal-content #eventTypeId').select('Command');
|
||||
// Select command
|
||||
cy.selectFromDropdown('.command-control .select2-selection', command1, command1, '@loadCommandAfterSearch');
|
||||
|
||||
// Click Next and check toast message
|
||||
cy.get('.modal .modal-footer').contains('Next').click();
|
||||
|
||||
// Select display
|
||||
cy.selectFromDropdown('.display-group-control .select2-selection', display1, display1, '@loadDisplaygroupAfterSearch', 1);
|
||||
|
||||
// Click Next and check toast message
|
||||
cy.get('.modal .modal-footer').contains('Next').click();
|
||||
|
||||
cy.get('.starttime-control > .col-sm-10 > .input-group > .flatpickr-wrapper > .datePickerHelper').click();
|
||||
cy.get('.open > .flatpickr-innerContainer > .flatpickr-rContainer > .flatpickr-days > .dayContainer > .today').click();
|
||||
cy.get('.open > .flatpickr-time > :nth-child(3) > .arrowUp').click();
|
||||
|
||||
cy.get('.modal .modal-footer').contains('Next').click();
|
||||
cy.get('.modal-content [name="name"]').type('Custom - Command Event');
|
||||
|
||||
cy.get('.modal .modal-footer').contains('Finish').click();
|
||||
});
|
||||
});
|
||||
|
||||
it('should schedule an event overlay layout that has no priority, no recurrence', function() {
|
||||
cy.intercept('GET', '/schedule/form/add?*').as('scheduleAddForm');
|
||||
cy.intercept({
|
||||
url: '/displaygroup?*',
|
||||
query: {displayGroup: display1},
|
||||
}).as('loadDisplaygroupAfterSearch');
|
||||
|
||||
cy.intercept({
|
||||
url: '/campaign?type=list*',
|
||||
query: {name: layoutSchedule1},
|
||||
}).as('loadListCampaignsAfterSearch');
|
||||
|
||||
// Click on the Add Event button
|
||||
cy.visit('/schedule/view');
|
||||
|
||||
cy.contains('Clear Filters').should('be.visible').click();
|
||||
cy.contains('Add Event').click();
|
||||
|
||||
cy.get('.bootbox.modal')
|
||||
.should('be.visible') // essential: Ensure the modal is visible
|
||||
.then(() => {
|
||||
cy.get('.modal-content #eventTypeId').select('Overlay Layout');
|
||||
// Select layout
|
||||
cy.selectFromDropdown('.layout-control .select2-selection', layoutSchedule1, layoutSchedule1, '@loadListCampaignsAfterSearch');
|
||||
|
||||
// Click Next and check toast message
|
||||
cy.get('.modal .modal-footer').contains('Next').click();
|
||||
|
||||
// Select display
|
||||
cy.selectFromDropdown('.display-group-control .select2-selection', display1, display1, '@loadDisplaygroupAfterSearch', 1);
|
||||
|
||||
// Click Next and check toast message
|
||||
cy.get('.modal .modal-footer').contains('Next').click();
|
||||
|
||||
// Select daypart - custom
|
||||
cy.get('#dayPartId').select('Custom');
|
||||
|
||||
|
||||
cy.get('.starttime-control > .col-sm-10 > .input-group > .flatpickr-wrapper > .datePickerHelper')
|
||||
.click() // Open the picker
|
||||
.then(() => {
|
||||
// Select today's date
|
||||
cy.get('.flatpickr-calendar.open .flatpickr-days .dayContainer .today')
|
||||
.click();
|
||||
|
||||
// Increment the hour (adjust time)
|
||||
cy.get('.flatpickr-calendar.open .flatpickr-time :nth-child(3) .arrowUp')
|
||||
.click();
|
||||
|
||||
// Close the picker by clicking outside
|
||||
cy.get('body').click(0, 0);
|
||||
});
|
||||
|
||||
cy.get('.endtime-control > .col-sm-10 > .input-group > .flatpickr-wrapper > .datePickerHelper')
|
||||
.click() // Open the picker
|
||||
.then(() => {
|
||||
// Select today's date
|
||||
cy.get('.flatpickr-calendar.open .flatpickr-days .dayContainer .today')
|
||||
.click();
|
||||
|
||||
// Increment the hour (adjust time)
|
||||
cy.get('.flatpickr-calendar.open .flatpickr-time :nth-child(3) .arrowUp')
|
||||
.click()
|
||||
.click();
|
||||
|
||||
// Close the picker by clicking outside
|
||||
cy.get('body').click(0, 0);
|
||||
});
|
||||
|
||||
cy.get('.modal .modal-footer').contains('Next').click();
|
||||
cy.get('.modal-content [name="name"]').type('Custom - Overlay Event');
|
||||
|
||||
cy.get('.modal .modal-footer').contains('Finish').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
136
cypress/e2e/Templates/templates.cy.js
Normal file
136
cypress/e2e/Templates/templates.cy.js
Normal file
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
describe('Template Test Suite', function () {
|
||||
|
||||
let templateName = '';
|
||||
|
||||
// create template flow
|
||||
function createTemplate(name) {
|
||||
cy.visit('/template/view');
|
||||
cy.contains('Add Template').click();
|
||||
cy.get('#name').clear().type(name);
|
||||
cy.get('#dialog_btn_2').should('be.visible').click();
|
||||
cy.get('#layout-editor').should('be.visible');
|
||||
cy.get('#backBtn').click({ force: true });
|
||||
}
|
||||
|
||||
// delete template flow
|
||||
function deleteATemplate(name) {
|
||||
cy.get('div[title="Row Menu"] button.dropdown-toggle').click({ force: true });
|
||||
cy.get('a.layout_button_delete[data-commit-method="delete"]').click({ force: true });
|
||||
|
||||
cy.get('#layoutDeleteForm').should('be.visible');
|
||||
cy.contains('p', 'Are you sure you want to delete this item?').should('be.visible');
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
cy.login();
|
||||
templateName = 'Template No. ' + Cypress._.random(0, 1e9);
|
||||
});
|
||||
|
||||
// Display Template List
|
||||
it('should display the template list', function () {
|
||||
cy.intercept('GET', '**/template*').as('templateList');
|
||||
cy.visit('/template/view');
|
||||
cy.wait('@templateList').its('response.statusCode').should('eq', 200);
|
||||
});
|
||||
|
||||
// Save Incomplete Form
|
||||
it('should prevent saving incomplete template', function () {
|
||||
cy.visit('/template/view');
|
||||
cy.contains('Add Template').click();
|
||||
cy.get('#dialog_btn_2').should('be.visible').click();
|
||||
cy.contains('Layout Name must be between 1 and 100 characters').should('be.visible');
|
||||
});
|
||||
|
||||
// Create a Template
|
||||
it('should create a template', function () {
|
||||
createTemplate(templateName);
|
||||
cy.contains('td', templateName).should('be.visible');
|
||||
});
|
||||
|
||||
// Duplicate Template
|
||||
it('should not allow duplicate template name', function () {
|
||||
createTemplate(templateName);
|
||||
|
||||
cy.contains('Add Template').click();
|
||||
cy.get('#name').clear().type(templateName);
|
||||
cy.get('#dialog_btn_2').should('be.visible').click();
|
||||
|
||||
cy.get('.modal-footer .form-error')
|
||||
.contains(`You already own a Layout called '${templateName}'. Please choose another name.`)
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
// Search and Delete a template
|
||||
it('should search template and delete it', function () {
|
||||
cy.intercept({
|
||||
url: '/template?*',
|
||||
query: { template: templateName },
|
||||
}).as('displayTemplateAfterSearch');
|
||||
|
||||
createTemplate(templateName);
|
||||
|
||||
cy.get('#template').clear().type(templateName);
|
||||
cy.wait('@displayTemplateAfterSearch');
|
||||
cy.get('table tbody tr').should('have.length', 1);
|
||||
cy.get('#templates tbody tr:nth-child(1) td:nth-child(1)').contains(templateName);
|
||||
|
||||
cy.get('#templates tbody tr')
|
||||
.should('have.length', 1)
|
||||
.first()
|
||||
.should('contain.text', templateName);
|
||||
|
||||
// delete template = no
|
||||
deleteATemplate(templateName);
|
||||
cy.get('#dialog_btn_2').click({ force: true });
|
||||
cy.contains(templateName).should('be.visible');
|
||||
|
||||
// delete template = yes
|
||||
deleteATemplate(templateName);
|
||||
cy.get('#dialog_btn_3').click({ force: true });
|
||||
cy.get('#toast-container .toast-message').contains(`Deleted ${templateName}`).should('be.visible');
|
||||
cy.contains(templateName).should('not.exist');
|
||||
});
|
||||
|
||||
// Multiple deleting of templates
|
||||
it('should delete multiple templates', function () {
|
||||
createTemplate(templateName);
|
||||
|
||||
cy.get('button[data-toggle="selectAll"]').click();
|
||||
cy.get('.dataTables_info button[data-toggle="dropdown"]').click();
|
||||
cy.get('a[data-button-id="layout_button_delete"]').click();
|
||||
|
||||
cy.get('.modal-footer').contains('Save').click();
|
||||
cy.get('.modal-body').contains(': Success');
|
||||
cy.get('.modal-footer').contains('Close').click();
|
||||
cy.contains('.dataTables_empty', 'No data available in table').should('be.visible');
|
||||
});
|
||||
|
||||
// Search for non-existing template
|
||||
it('should not return any entry for non-existing template', function () {
|
||||
cy.visit('/template/view');
|
||||
cy.get('#template').clear().type('This is a hardcoded template name just to make sure it doesnt exist in the record');
|
||||
cy.contains('.dataTables_empty', 'No data available in table').should('be.visible');
|
||||
});
|
||||
|
||||
});
|
||||
76
cypress/e2e/UserAccount/user_account.cy.js
Normal file
76
cypress/e2e/UserAccount/user_account.cy.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
describe('User Account Test Suite', function() {
|
||||
|
||||
beforeEach(function () {
|
||||
cy.login();
|
||||
cy.visit('/statusdashboard');
|
||||
});
|
||||
|
||||
it('navigates to edit profile', function() {
|
||||
cy.url().should('include', 'dashboard');
|
||||
cy.get('img.nav-avatar').should('be.visible');
|
||||
cy.get('#navbarUserMenu').click();
|
||||
cy.get('div[aria-labelledby="navbarUserMenu"]')
|
||||
.should('be.visible')
|
||||
.contains('Edit Profile');
|
||||
});
|
||||
|
||||
it('verifies all menu items are present and in order', function() {
|
||||
cy.get('#navbarUserMenu').click();
|
||||
cy.get('div[aria-labelledby="navbarUserMenu"] a')
|
||||
.should('have.length', 6)
|
||||
.then($items => {
|
||||
const texts = [...$items].map(el => el.innerText.trim());
|
||||
expect(texts).to.deep.equal([
|
||||
'Preferences',
|
||||
'Edit Profile',
|
||||
'My Applications',
|
||||
'Reshow welcome',
|
||||
'About',
|
||||
'Logout'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('validates edit profile', function() {
|
||||
cy.get('#navbarUserMenu').click();
|
||||
cy.get('div[aria-labelledby="navbarUserMenu"]')
|
||||
.contains('Edit Profile')
|
||||
.click();
|
||||
|
||||
cy.get('.modal-content').should('be.visible');
|
||||
cy.contains('label', 'User Name').should('be.visible');
|
||||
cy.contains('label', 'Password').should('be.visible');
|
||||
cy.contains('label', 'New Password').should('be.visible');
|
||||
cy.contains('label', 'Retype New Password').should('be.visible');
|
||||
cy.contains('label', 'Email').should('be.visible');
|
||||
cy.contains('label', 'Two Factor Authentication').should('be.visible');
|
||||
|
||||
// Ensure 2FA defaults to Off
|
||||
cy.get('#twoFactorTypeId')
|
||||
.should('be.visible')
|
||||
.find('option:selected')
|
||||
.should('have.text', 'Off');
|
||||
});
|
||||
|
||||
});
|
||||
76
cypress/e2e/User_Account/user_account.cy.js
Normal file
76
cypress/e2e/User_Account/user_account.cy.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
describe('User Account Test Suite', function() {
|
||||
|
||||
beforeEach(function () {
|
||||
cy.login();
|
||||
cy.visit('/statusdashboard');
|
||||
});
|
||||
|
||||
it('navigates to edit profile', function() {
|
||||
cy.url().should('include', 'dashboard');
|
||||
cy.get('img.nav-avatar').should('be.visible');
|
||||
cy.get('#navbarUserMenu').click();
|
||||
cy.get('div[aria-labelledby="navbarUserMenu"]')
|
||||
.should('be.visible')
|
||||
.contains('Edit Profile');
|
||||
});
|
||||
|
||||
it('verifies all menu items are present and in order', function() {
|
||||
cy.get('#navbarUserMenu').click();
|
||||
cy.get('div[aria-labelledby="navbarUserMenu"] a')
|
||||
.should('have.length', 6)
|
||||
.then($items => {
|
||||
const texts = [...$items].map(el => el.innerText.trim());
|
||||
expect(texts).to.deep.equal([
|
||||
'Preferences',
|
||||
'Edit Profile',
|
||||
'My Applications',
|
||||
'Reshow welcome',
|
||||
'About',
|
||||
'Logout'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('validates edit profile', function() {
|
||||
cy.get('#navbarUserMenu').click();
|
||||
cy.get('div[aria-labelledby="navbarUserMenu"]')
|
||||
.contains('Edit Profile')
|
||||
.click();
|
||||
|
||||
cy.get('.modal-content').should('be.visible');
|
||||
cy.contains('label', 'User Name').should('be.visible');
|
||||
cy.contains('label', 'Password').should('be.visible');
|
||||
cy.contains('label', 'New Password').should('be.visible');
|
||||
cy.contains('label', 'Retype New Password').should('be.visible');
|
||||
cy.contains('label', 'Email').should('be.visible');
|
||||
cy.contains('label', 'Two Factor Authentication').should('be.visible');
|
||||
|
||||
// Ensure 2FA defaults to Off
|
||||
cy.get('#twoFactorTypeId')
|
||||
.should('be.visible')
|
||||
.find('option:selected')
|
||||
.should('have.text', 'Off');
|
||||
});
|
||||
|
||||
});
|
||||
39
cypress/e2e/dashboard.cy.js
Normal file
39
cypress/e2e/dashboard.cy.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
describe('Dashboard', function() {
|
||||
beforeEach(function() {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('should be at the dashboard page', function() {
|
||||
cy.visit('/statusdashboard');
|
||||
|
||||
|
||||
cy.url().should('include', 'dashboard');
|
||||
|
||||
// Check for the dashboard elements
|
||||
cy.contains('Bandwidth Usage');
|
||||
cy.contains('Library Usage');
|
||||
cy.contains('Display Activity');
|
||||
cy.contains('Latest News');
|
||||
});
|
||||
});
|
||||
37
cypress/e2e/login.cy.js
Normal file
37
cypress/e2e/login.cy.js
Normal file
@@ -0,0 +1,37 @@
|
||||
describe('Login', function() {
|
||||
|
||||
it('should be able to login the default user', function () {
|
||||
|
||||
cy.visit('/login').then(() => {
|
||||
|
||||
cy.get('input#username')
|
||||
.type('xibo_admin');
|
||||
|
||||
cy.get('input#password')
|
||||
.type('password');
|
||||
|
||||
cy.get('button[type=submit]')
|
||||
.click();
|
||||
|
||||
cy.url().should('include', 'dashboard');
|
||||
|
||||
cy.contains('xibo_admin');
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to login an invalid user', function () {
|
||||
|
||||
cy.visit('/login').then(() => {
|
||||
cy.get('input#username')
|
||||
.type('xibo_admin');
|
||||
|
||||
cy.get('input#password')
|
||||
.type('wrongpassword');
|
||||
|
||||
cy.get('button[type=submit]')
|
||||
.click();
|
||||
|
||||
cy.contains('Username or Password incorrect');
|
||||
});
|
||||
});
|
||||
});
|
||||
19
cypress/e2e/unauthed.cy.js
Normal file
19
cypress/e2e/unauthed.cy.js
Normal file
@@ -0,0 +1,19 @@
|
||||
describe('Unauthenticated CMS access', function () {
|
||||
it('should visit the login page and check the version', function () {
|
||||
|
||||
cy.visit('/login').then(() => {
|
||||
|
||||
cy.url().should('include', '/login');
|
||||
|
||||
cy.contains('Version 4.');
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to login when an authenticated page is requested', function() {
|
||||
cy.visit('/logout').then(() => {
|
||||
cy.visit('/layout/view').then(() => {
|
||||
cy.url().should('include', '/login');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
5
cypress/fixtures/example.json
Normal file
5
cypress/fixtures/example.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
BIN
cypress/fixtures/file/example.zip
Normal file
BIN
cypress/fixtures/file/example.zip
Normal file
Binary file not shown.
17
cypress/plugins/index.js
Normal file
17
cypress/plugins/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// ***********************************************************
|
||||
// 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)
|
||||
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
455
cypress/support/commands.js
Normal file
455
cypress/support/commands.js
Normal file
@@ -0,0 +1,455 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// ***********************************************
|
||||
// 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 is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
/* eslint-disable max-len */
|
||||
Cypress.Commands.add('login', function(callbackRoute = '/login') {
|
||||
cy.session('saveSession', () => {
|
||||
cy.visit(callbackRoute);
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/login',
|
||||
form: true,
|
||||
body: {
|
||||
username: 'xibo_admin',
|
||||
password: 'password',
|
||||
},
|
||||
}).then((res) => {
|
||||
// Get access token and save it as a environment variable
|
||||
cy.getAccessToken().then(function() {
|
||||
cy.getCookie('PHPSESSID').should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('getAccessToken', function() {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/authorize/access_token',
|
||||
form: true,
|
||||
body: {
|
||||
client_id: Cypress.env('client_id'),
|
||||
client_secret: Cypress.env('client_secret'),
|
||||
grant_type: 'client_credentials',
|
||||
},
|
||||
}).then((res) => {
|
||||
Cypress.env('accessToken', res.body.access_token);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('tutorialClose', function() {
|
||||
const csrf_token = Cypress.$('meta[name="token"]').attr('content');
|
||||
|
||||
// Make the ajax request to hide the user welcome tutorial
|
||||
Cypress.$.ajax({
|
||||
url: '/user/welcome',
|
||||
type: 'PUT',
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': csrf_token,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('formRequest', (method, url, formData) => {
|
||||
return new Promise(function(resolve, reject) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open(method, url);
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + Cypress.env('accessToken'));
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status >= 200 && this.status < 300) {
|
||||
resolve(xhr.response);
|
||||
} else {
|
||||
reject({
|
||||
status: this.status,
|
||||
statusText: xhr.statusText,
|
||||
});
|
||||
}
|
||||
};
|
||||
xhr.onerror = function() {
|
||||
reject({
|
||||
status: this.status,
|
||||
statusText: xhr.statusText,
|
||||
});
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('addMediaToLibrary', (fileName) => {
|
||||
// Declarations
|
||||
const method = 'POST';
|
||||
const url = '/api/library';
|
||||
const fileType = '*/*';
|
||||
|
||||
// Get file from fixtures as binary
|
||||
return cy.fixture(fileName, 'binary').then((zipBin) => {
|
||||
// File in binary format gets converted to blob so it can be sent as Form data
|
||||
const fileBlob = Cypress.Blob.binaryStringToBlob(zipBin, fileType);
|
||||
|
||||
// Build up the form
|
||||
const formData = new FormData();
|
||||
|
||||
formData.set('files[]', fileBlob, fileName); // adding a file to the form
|
||||
|
||||
// Perform the request
|
||||
return cy.formRequest(method, url, formData).then((response) => {
|
||||
const { files } = JSON.parse(response);
|
||||
|
||||
// Return id
|
||||
return files[0].name;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Campaign
|
||||
Cypress.Commands.add('createCampaign', function(name) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/campaign',
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {
|
||||
name: name,
|
||||
},
|
||||
}).then((res) => {
|
||||
return res.body.campaignId;
|
||||
});
|
||||
});
|
||||
|
||||
// Dataset
|
||||
Cypress.Commands.add('createDataset', function(name) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/dataset',
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {
|
||||
dataSet: name,
|
||||
},
|
||||
}).then((res) => {
|
||||
return res.body.dataSetId;
|
||||
});
|
||||
});
|
||||
|
||||
// Delete Dataset
|
||||
Cypress.Commands.add('deleteDataset', function(id) {
|
||||
cy.request({
|
||||
method: 'DELETE',
|
||||
url: '/api/dataset/' + id,
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {},
|
||||
}).then((res) => {
|
||||
return res;
|
||||
});
|
||||
});
|
||||
|
||||
// Sync Group
|
||||
Cypress.Commands.add('createSyncGroup', function(name) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/syncgroup/add',
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {
|
||||
name: name,
|
||||
syncPublisherPort: 9590,
|
||||
},
|
||||
}).then((res) => {
|
||||
return res.body.datasetId;
|
||||
});
|
||||
});
|
||||
|
||||
// DayPart
|
||||
Cypress.Commands.add('createDayPart', function(name) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/daypart',
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {
|
||||
name: name,
|
||||
startTime: '01:00:00',
|
||||
endTime: '02:00:00',
|
||||
},
|
||||
}).then((res) => {
|
||||
return res.body.dayPartId;
|
||||
});
|
||||
});
|
||||
|
||||
// Delete DayPart
|
||||
Cypress.Commands.add('deleteDayPart', function(id) {
|
||||
cy.request({
|
||||
method: 'DELETE',
|
||||
url: '/api/daypart/' + id,
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {},
|
||||
}).then((res) => {
|
||||
return res;
|
||||
});
|
||||
});
|
||||
|
||||
// Tag
|
||||
Cypress.Commands.add('createTag', function(name) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/tag',
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {
|
||||
name: name,
|
||||
},
|
||||
}).then((res) => {
|
||||
return res.body.id;
|
||||
});
|
||||
});
|
||||
|
||||
// Application
|
||||
Cypress.Commands.add('createApplication', function(name) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/application',
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {
|
||||
name: name,
|
||||
},
|
||||
}).then((res) => {
|
||||
return res.body.key;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Open playlist editor modal and wait for toolbar user prefs to load
|
||||
* @param {String} playlistName
|
||||
*/
|
||||
Cypress.Commands.add('openPlaylistEditorAndLoadPrefs', function(playlistId) {
|
||||
cy.intercept('GET', '/user/pref?preference=toolbar').as('userPrefsLoad');
|
||||
|
||||
// Reload playlist table page
|
||||
cy.visit('/playlist/view');
|
||||
|
||||
// Clear toolbar preferences
|
||||
cy.clearToolbarPrefs();
|
||||
|
||||
cy.window().then((win) => {
|
||||
win.XiboCustomFormRender(win.$('<li class="XiboCustomFormButton playlist_timeline_button_edit" href="/playlist/form/timeline/' + playlistId + '"></li>'));
|
||||
|
||||
// Wait for user prefs to load
|
||||
cy.wait('@userPrefsLoad');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Add media items to library
|
||||
*/
|
||||
Cypress.Commands.add('populateLibraryWithMedia', function() {
|
||||
// Add audio media to library
|
||||
cy.addMediaToLibrary('../assets/audioSample.mp3');
|
||||
|
||||
// Add image media to library
|
||||
cy.addMediaToLibrary('../assets/imageSample.png');
|
||||
});
|
||||
|
||||
/**
|
||||
* Drag one element to another one
|
||||
* @param {string} draggableSelector
|
||||
* @param {string} dropableSelector
|
||||
*/
|
||||
Cypress.Commands.add('dragToElement', function(draggableSelector, dropableSelector) {
|
||||
return cy.get(dropableSelector).then(($el) => {
|
||||
const position = {
|
||||
x: $el.offset().left + $el.width() / 2 + window.scrollX,
|
||||
y: $el.offset().top + $el.height() / 2 + window.scrollY,
|
||||
};
|
||||
|
||||
cy.get(draggableSelector).invoke('show');
|
||||
|
||||
cy.get(draggableSelector)
|
||||
.trigger('mousedown', {
|
||||
which: 1,
|
||||
})
|
||||
.trigger('mousemove', {
|
||||
which: 1,
|
||||
pageX: position.x,
|
||||
pageY: position.y,
|
||||
})
|
||||
.trigger('mouseup');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Go to layout editor page and wait for toolbar user prefs to load
|
||||
* @param {number} layoutId
|
||||
*/
|
||||
Cypress.Commands.add('goToLayoutAndLoadPrefs', function(layoutId) {
|
||||
cy.intercept('GET', '/user/pref?preference=toolbar').as('userPrefsLoad');
|
||||
|
||||
cy.clearToolbarPrefs();
|
||||
|
||||
cy.visit('/layout/designer/' + layoutId);
|
||||
|
||||
// Wait for user prefs to load
|
||||
cy.wait('@userPrefsLoad');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('removeAllSelectedOptions', (select2) => {
|
||||
cy.get(select2)
|
||||
.as('select2Container');
|
||||
|
||||
cy.get('@select2Container')
|
||||
.then(($select2Container) => {
|
||||
if ($select2Container.find('.select2-selection__choice').length > 0) {
|
||||
cy.wrap($select2Container)
|
||||
.find('.select2-selection__choice')
|
||||
.each(($selectedOption) => {
|
||||
cy.wrap($selectedOption)
|
||||
.find('.select2-selection__choice__remove')
|
||||
.click(); // Click on the remove button for each selected option
|
||||
});
|
||||
} else {
|
||||
// No options are selected
|
||||
cy.log('No options are selected');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Select an item from the select2 dropdown
|
||||
Cypress.Commands.add('selectFromDropdown', (selector, searchText, expected, alias, index = 0) => {
|
||||
cy.get(selector).type(searchText);
|
||||
|
||||
if (alias) {
|
||||
cy.wait(alias).its('response.statusCode').should('eq', 200);
|
||||
}
|
||||
|
||||
cy.get('.select2-container--open .select2-dropdown .select2-results > ul')
|
||||
.should('contain', expected);
|
||||
cy.get('.select2-container--open .select2-dropdown .select2-results > ul > li')
|
||||
.eq(index)
|
||||
.click();
|
||||
});
|
||||
|
||||
// Select an option from the select2
|
||||
Cypress.Commands.add('selectOption', (content) => {
|
||||
cy.get('.select2-container--open').contains(content);
|
||||
cy.get('.select2-container--open .select2-results > ul > li').should('have.length', 1);
|
||||
cy.get('.select2-container--open .select2-results > ul > li:first').contains(content).click();
|
||||
});
|
||||
|
||||
// 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;
|
||||
});
|
||||
});
|
||||
|
||||
// Open Options Menu within the Layout Editor
|
||||
Cypress.Commands.add('openOptionsMenu', () => {
|
||||
cy.get('.navbar-submenu')
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
cy.get('#optionsContainerTop')
|
||||
.should('be.visible')
|
||||
.and('not.be.disabled')
|
||||
.click({force: true})
|
||||
.should('have.attr', 'aria-expanded', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
// Open Row Menu of the first item on the Layouts page
|
||||
Cypress.Commands.add('openRowMenu', () => {
|
||||
cy.get('#layouts tbody tr').first().within(() => {
|
||||
cy.get('.btn-group .btn.dropdown-toggle')
|
||||
.click()
|
||||
.should('have.attr', 'aria-expanded', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Update data on CKEditor instance
|
||||
* @param {string} ckeditorId
|
||||
* @param {string} value
|
||||
*/
|
||||
Cypress.Commands.add('updateCKEditor', function(ckeditorId, value) {
|
||||
cy.get('textarea[name="' + ckeditorId + '"]').invoke('prop', 'id').then((id) => {
|
||||
cy.window().then((win) => {
|
||||
win.formHelpers.getCKEditorInstance(
|
||||
id,
|
||||
).setData(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
156
cypress/support/displayCommands.js
Normal file
156
cypress/support/displayCommands.js
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// ***********************************************
|
||||
// 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 is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
/* eslint-disable max-len */
|
||||
// Display Group
|
||||
Cypress.Commands.add('createDisplaygroup', function(name, isDynamic = false, criteria) {
|
||||
// Define the request body object
|
||||
const requestBody = {
|
||||
displayGroup: name,
|
||||
};
|
||||
|
||||
// Add 'isDynamic' to the request body if it's true
|
||||
if (isDynamic) {
|
||||
requestBody.isDynamic = true;
|
||||
}
|
||||
// Add 'isDynamic' to the request body if it's true
|
||||
if (criteria) {
|
||||
requestBody.dynamicCriteria = criteria;
|
||||
}
|
||||
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/displaygroup',
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: requestBody,
|
||||
}).then((res) => {
|
||||
return res.body.displaygroupId;
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('deleteDisplaygroup', function(id) {
|
||||
cy.request({
|
||||
method: 'DELETE',
|
||||
url: '/api/displaygroup/' + id,
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {},
|
||||
}).then((res) => {
|
||||
return res;
|
||||
});
|
||||
});
|
||||
|
||||
// Display Profile
|
||||
Cypress.Commands.add('createDisplayProfile', function(name, type) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/displayprofile',
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {
|
||||
name: name,
|
||||
type: type,
|
||||
},
|
||||
}).then((res) => {
|
||||
return res.body.displayProfileId;
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('deleteDisplayProfile', function(id) {
|
||||
cy.request({
|
||||
method: 'DELETE',
|
||||
url: '/api/displayprofile/' + id,
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {},
|
||||
}).then((res) => {
|
||||
return res;
|
||||
});
|
||||
});
|
||||
|
||||
// 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;
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
});
|
||||
39
cypress/support/e2e.js
Normal file
39
cypress/support/e2e.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// ***********************************************************
|
||||
// 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';
|
||||
import './toolbarCommands';
|
||||
import './layoutCommands';
|
||||
import './playlistCommands';
|
||||
import './displayCommands';
|
||||
import './menuboardCommands';
|
||||
import './userCommands';
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
// Run before every test spec, to disable User Welcome tour
|
||||
before(function() {
|
||||
cy.login().then(() => {
|
||||
cy.tutorialClose();
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.on('uncaught:exception', (err, runnable) => {
|
||||
// returning false here prevents Cypress from
|
||||
// failing the test
|
||||
return false
|
||||
})
|
||||
116
cypress/support/layoutCommands.js
Normal file
116
cypress/support/layoutCommands.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// ***********************************************
|
||||
// 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 is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
/* eslint-disable max-len */
|
||||
// Layout
|
||||
Cypress.Commands.add('createLayout', function(name) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/layout',
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {
|
||||
name: name,
|
||||
resolutionId: 1, // HD landscape on the testing build
|
||||
},
|
||||
}).then((res) => {
|
||||
return res.body.layoutId;
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('checkoutLayout', function(id) {
|
||||
cy.request({
|
||||
method: 'PUT',
|
||||
url: '/api/layout/checkout/' + id,
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('importLayout', function(fileName) {
|
||||
// Declarations
|
||||
const method = 'POST';
|
||||
const url = '/api/layout/import';
|
||||
const fileType = 'application/zip';
|
||||
|
||||
// Get file from fixtures as binary
|
||||
cy.fixture(fileName, 'binary').then((zipBin) => {
|
||||
// File in binary format gets converted to blob so it can be sent as Form data
|
||||
const blob = Cypress.Blob.binaryStringToBlob(zipBin, fileType);
|
||||
|
||||
// Build up the form
|
||||
const formData = new FormData();
|
||||
|
||||
// Create random name
|
||||
const uuid = Cypress._.random(0, 1e9);
|
||||
|
||||
formData.set('files[]', blob, fileName); // adding a file to the form
|
||||
formData.set('name[]', uuid); // adding a name to the form
|
||||
|
||||
// Perform the request
|
||||
return cy.formRequest(method, url, formData).then((res) => {
|
||||
const parsedJSON = JSON.parse(res);
|
||||
// Return id
|
||||
return parsedJSON.files[0].id;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('deleteLayout', function(id) {
|
||||
cy.request({
|
||||
method: 'DELETE',
|
||||
failOnStatusCode: false,
|
||||
url: '/api/layout/' + id,
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
});
|
||||
});
|
||||
109
cypress/support/menuboardCommands.js
Normal file
109
cypress/support/menuboardCommands.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// ***********************************************
|
||||
// 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 is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
/* eslint-disable max-len */
|
||||
// Menuboard
|
||||
Cypress.Commands.add('createMenuboard', function(name) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/menuboard',
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {
|
||||
name: name,
|
||||
},
|
||||
}).then((res) => {
|
||||
return res.body.menuId;
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('createMenuboardCat', function(name, menuId) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/menuboard/' + menuId + '/' + 'category',
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {
|
||||
name: name,
|
||||
},
|
||||
}).then((res) => {
|
||||
return res.body.menuCategoryId;
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('createMenuboardCatProd', function(name, menuCatId) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/menuboard/' + menuCatId + '/' + 'product',
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {
|
||||
name: name,
|
||||
},
|
||||
}).then((res) => {
|
||||
return res.body.menuProductId;
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('deleteMenuboard', function(id) {
|
||||
cy.request({
|
||||
method: 'DELETE',
|
||||
url: '/api/menuboard/' + id,
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {},
|
||||
}).then((res) => {
|
||||
return res;
|
||||
});
|
||||
});
|
||||
110
cypress/support/playlistCommands.js
Normal file
110
cypress/support/playlistCommands.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// ***********************************************
|
||||
// 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 is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
/* eslint-disable max-len */
|
||||
// Playlist
|
||||
Cypress.Commands.add('createNonDynamicPlaylist', (name) => {
|
||||
return cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/playlist',
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cypress.env('accessToken')}`,
|
||||
},
|
||||
body: { name },
|
||||
form: true,
|
||||
}).then((response) => response.body.playlistId);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('addWidgetToPlaylist', function(playlistId, widgetType, widgetData) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/playlist/widget/' + widgetType + '/' + playlistId,
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: widgetData,
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('addRandomMediaToPlaylist', function(playlistId) {
|
||||
// Get media
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: '/api/library?retired=0&assignable=1&start=0&length=1',
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
}).then((res) => {
|
||||
const media = [];
|
||||
media.push(res.body[0].mediaId);
|
||||
|
||||
// Add media to playlist
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/playlist/library/assign/' + playlistId,
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {
|
||||
media: media,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('deletePlaylist', function(id) {
|
||||
cy.request({
|
||||
method: 'DELETE',
|
||||
url: '/api/playlist/' + id,
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
});
|
||||
});
|
||||
203
cypress/support/toolbarCommands.js
Normal file
203
cypress/support/toolbarCommands.js
Normal file
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// ***********************************************
|
||||
// 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 is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
/* eslint-disable max-len */
|
||||
Cypress.Commands.add('clearToolbarPrefs', function() {
|
||||
const preference = [];
|
||||
|
||||
preference[0] =
|
||||
{
|
||||
option: 'toolbar',
|
||||
value: JSON.stringify({
|
||||
menuItems: {},
|
||||
openedMenu: -1,
|
||||
}),
|
||||
};
|
||||
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/user/pref',
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {
|
||||
preference: preference,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Force open toolbar menu
|
||||
* @param {number} menuIdx
|
||||
* @param {boolean} load
|
||||
*/
|
||||
Cypress.Commands.add('openToolbarMenu', (menuIdx, load = true) => {
|
||||
cy.intercept('GET', '/user/pref?preference=toolbar').as('toolbarPrefsLoad');
|
||||
cy.intercept('GET', '/user/pref?preference=editor').as('editorPrefsLoad');
|
||||
cy.intercept('POST', '/user/pref?preference=toolbar').as('toolbarPrefsSave');
|
||||
|
||||
if (load) {
|
||||
cy.wait('@toolbarPrefsLoad');
|
||||
cy.wait('@editorPrefsLoad');
|
||||
}
|
||||
|
||||
cy.get('.editor-side-bar').then(($toolbar) => {
|
||||
const $submenu = $toolbar.find('#content-' + menuIdx + ' .close-submenu');
|
||||
const $menuButton = $toolbar.find('#btn-menu-' + menuIdx);
|
||||
|
||||
if ($submenu.length > 0) {
|
||||
cy.log('Just close sub-menu!');
|
||||
cy.get('#content-' + menuIdx + ' .close-submenu')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
} else if (!$menuButton.hasClass('active')) {
|
||||
cy.log('Open menu!');
|
||||
cy.get('[data-test="toolbarTabs"]').eq(menuIdx).click();
|
||||
} else {
|
||||
cy.log('Do nothing!');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Force open toolbar menu when we are on playlist editor
|
||||
* @param {number} menuIdx
|
||||
*/
|
||||
Cypress.Commands.add('openToolbarMenuForPlaylist', function(menuIdx) {
|
||||
cy.intercept('POST', '/user/pref').as('toolbarPrefsLoadForPlaylist');
|
||||
|
||||
// Wait for the toolbar to reload when getting prefs at start
|
||||
cy.wait('@toolbarPrefsLoadForPlaylist');
|
||||
|
||||
cy.get('.editor-toolbar').then(($toolbar) => {
|
||||
if ($toolbar.find('#content-' + menuIdx + ' .close-submenu').length > 0) {
|
||||
cy.log('Just close sub-menu!');
|
||||
cy.get('.close-submenu').click();
|
||||
} else if ($toolbar.find('#btn-menu-' + menuIdx + '.active').length == 0) {
|
||||
cy.log('Open menu!');
|
||||
cy.get('.editor-main-toolbar #btn-menu-' + menuIdx).click();
|
||||
} else {
|
||||
cy.log('Do nothing!');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('toolbarSearch', (textToType) => {
|
||||
cy.intercept('POST', '/user/pref').as('updatePreferences');
|
||||
|
||||
// Clear the search box first
|
||||
cy.get('input#input-name')
|
||||
.filter(':visible')
|
||||
.should('have.length', 1)
|
||||
.invoke('val')
|
||||
.then((value) => {
|
||||
if (value !== '') {
|
||||
cy.get('input#input-name')
|
||||
.filter(':visible')
|
||||
.clear();
|
||||
cy.wait('@updatePreferences');
|
||||
}
|
||||
});
|
||||
// Type keyword to search
|
||||
cy.get('input#input-name')
|
||||
.filter(':visible')
|
||||
.type(textToType);
|
||||
cy.wait('@updatePreferences');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('toolbarSearchWithActiveFilter', (textToType) => {
|
||||
cy.intercept('POST', '/user/pref').as('updatePreferences');
|
||||
cy.intercept('GET', '/library/search*').as('librarySearch');
|
||||
|
||||
// Clear the search box first
|
||||
cy.get('input#input-name')
|
||||
.filter(':visible')
|
||||
.should('have.length', 1)
|
||||
.invoke('val')
|
||||
.then((value) => {
|
||||
if (value !== '') {
|
||||
cy.get('input#input-name')
|
||||
.filter(':visible')
|
||||
.clear();
|
||||
cy.wait('@updatePreferences');
|
||||
cy.wait('@librarySearch');
|
||||
}
|
||||
});
|
||||
// Type keyword to search
|
||||
cy.get('input#input-name')
|
||||
.filter(':visible')
|
||||
.type(textToType);
|
||||
cy.wait('@updatePreferences');
|
||||
cy.wait('@librarySearch');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('toolbarFilterByFolder', (folderName, folderId) => {
|
||||
cy.intercept('POST', '/user/pref').as('updatePreferences');
|
||||
cy.intercept('GET', '/folders?start=0&length=10').as('loadFolders');
|
||||
cy.intercept('GET', '/library/search*').as('librarySearch');
|
||||
|
||||
// Open folder dropdown
|
||||
cy.get('#input-folder')
|
||||
.parent()
|
||||
.find('.select2-selection')
|
||||
.click();
|
||||
cy.wait('@loadFolders');
|
||||
|
||||
// Select the specified folder
|
||||
cy.get('.select2-results__option')
|
||||
.contains(folderName)
|
||||
.should('be.visible')
|
||||
.click();
|
||||
|
||||
cy.wait('@updatePreferences');
|
||||
|
||||
// Verify library search response
|
||||
cy.wait('@librarySearch').then(({response}) => {
|
||||
expect(response.statusCode).to.eq(200);
|
||||
expect(response.url).to.include(`folderId=${folderId}`);
|
||||
});
|
||||
});
|
||||
112
cypress/support/userCommands.js
Normal file
112
cypress/support/userCommands.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// ***********************************************
|
||||
// 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 is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
/* eslint-disable max-len */
|
||||
// User
|
||||
Cypress.Commands.add('createUser', function(name, password, userTypeId, homeFolderId) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/user',
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {
|
||||
userName: name,
|
||||
password: password,
|
||||
userTypeId: userTypeId,
|
||||
homeFolderId: homeFolderId,
|
||||
homePageId: 'icondashboard.view',
|
||||
},
|
||||
}).then((res) => {
|
||||
return res.body.userId;
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('deleteUser', function(id) {
|
||||
cy.request({
|
||||
method: 'DELETE',
|
||||
url: '/api/user/' + id,
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {},
|
||||
}).then((res) => {
|
||||
return res;
|
||||
});
|
||||
});
|
||||
|
||||
// User Group
|
||||
Cypress.Commands.add('createUsergroup', function(name) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/group',
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {
|
||||
group: name,
|
||||
},
|
||||
}).then((res) => {
|
||||
return res.body.groupId;
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('deleteUsergroup', function(id) {
|
||||
cy.request({
|
||||
method: 'DELETE',
|
||||
url: '/api/group/' + id,
|
||||
form: true,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + Cypress.env('accessToken'),
|
||||
},
|
||||
body: {},
|
||||
}).then((res) => {
|
||||
return res;
|
||||
});
|
||||
});
|
||||
1223
db/migrations/20180130073838_install_migration.php
Normal file
1223
db/migrations/20180130073838_install_migration.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
* Copyright (C) 2018 Spring Signage Ltd
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep85Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 85;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
$display = $this->table('display');
|
||||
|
||||
if (!$display->hasColumn('storageAvailableSpace')) {
|
||||
$display
|
||||
->addColumn('storageAvailableSpace', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_BIG, 'null' => true])
|
||||
->addColumn('storageTotalSpace', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_BIG, 'null' => true])
|
||||
->save();
|
||||
}
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep86Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 86;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$settings = $this->table('setting');
|
||||
$settings
|
||||
->insert([
|
||||
[
|
||||
'setting' => 'DASHBOARD_LATEST_NEWS_ENABLED',
|
||||
'title' => 'Enable Latest News?',
|
||||
'helptext' => 'Should the Dashboard show latest news? The address is provided by the theme.',
|
||||
'value' => '1',
|
||||
'fieldType' => 'checkbox',
|
||||
'options' => '',
|
||||
'cat' => 'general',
|
||||
'userChange' => '1',
|
||||
'type' => 'checkbox',
|
||||
'validation' => '',
|
||||
'ordering' => '110',
|
||||
'default' => '1',
|
||||
'userSee' => '1',
|
||||
],
|
||||
[
|
||||
'setting' => 'LIBRARY_MEDIA_DELETEOLDVER_CHECKB',
|
||||
'title' => 'Default for \"Delete old version of Media\" checkbox. Shown when Editing Library Media.',
|
||||
'helptext' => 'Default the checkbox for Deleting Old Version of media when a new file is being uploaded to the library.',
|
||||
'value' => 'Unchecked',
|
||||
'fieldType' => 'dropdown',
|
||||
'options' => 'Checked|Unchecked',
|
||||
'cat' => 'defaults',
|
||||
'userChange' => '1',
|
||||
'type' => 'dropdown',
|
||||
'validation' => '',
|
||||
'ordering' => '50',
|
||||
'default' => 'Unchecked',
|
||||
'userSee' => '1',
|
||||
]
|
||||
])
|
||||
->save();
|
||||
|
||||
// Update a setting
|
||||
$this->execute('UPDATE `setting` SET `type` = \'checkbox\', `fieldType` = \'checkbox\' WHERE setting = \'SETTING_LIBRARY_TIDY_ENABLED\' OR setting = \'SETTING_IMPORT_ENABLED\';');
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep87Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 87;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$settings = $this->table('setting');
|
||||
$settings
|
||||
->insert([
|
||||
'setting' => 'PROXY_EXCEPTIONS',
|
||||
'title' => 'Proxy Exceptions',
|
||||
'helptext' => 'Hosts and Keywords that should not be loaded via the Proxy Specified. These should be comma separated.',
|
||||
'value' => '1',
|
||||
'fieldType' => 'text',
|
||||
'options' => '',
|
||||
'cat' => 'network',
|
||||
'userChange' => '1',
|
||||
'type' => 'string',
|
||||
'validation' => '',
|
||||
'ordering' => '32',
|
||||
'default' => '',
|
||||
'userSee' => '1',
|
||||
])
|
||||
->save();
|
||||
|
||||
// If we haven't run step85 during this migration, then we will want to update our storageAvailable columns
|
||||
// Change to big ints.
|
||||
$display = $this->table('display');
|
||||
$display
|
||||
->changeColumn('storageAvailableSpace', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_BIG, 'null' => true])
|
||||
->changeColumn('storageTotalSpace', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_BIG, 'null' => true])
|
||||
->save();
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep88Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 88;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$auditLog = $this->table('auditlog', ['id' => 'logId']);
|
||||
$auditLog->addColumn('logDate', 'integer')
|
||||
->addColumn('userId', 'integer')
|
||||
->addColumn('message', 'string', ['limit' => 255])
|
||||
->addColumn('entity', 'string', ['limit' => 50])
|
||||
->addColumn('entityId', 'integer')
|
||||
->addColumn('objectAfter', 'text')
|
||||
->save();
|
||||
|
||||
$this->execute('INSERT INTO `pages` (`name`, `pagegroupID`) SELECT \'auditlog\', pagegroupID FROM `pagegroup` WHERE pagegroup.pagegroup = \'Reports\';');
|
||||
|
||||
$group = $this->table('group');
|
||||
if (!$group->hasColumn('libraryQuota')) {
|
||||
$group->addColumn('libraryQuota', 'integer', ['null' => true])
|
||||
->save();
|
||||
}
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep92Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 92;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$settings = $this->table('setting');
|
||||
$settings
|
||||
->insert([
|
||||
'setting' => 'CDN_URL',
|
||||
'title' => 'CDN Address',
|
||||
'helptext' => 'Content Delivery Network Address for serving file requests to Players',
|
||||
'value' => '',
|
||||
'fieldType' => 'text',
|
||||
'options' => '',
|
||||
'cat' => 'network',
|
||||
'userChange' => '0',
|
||||
'type' => 'string',
|
||||
'validation' => '',
|
||||
'ordering' => '33',
|
||||
'default' => '',
|
||||
'userSee' => '0',
|
||||
])
|
||||
->save();
|
||||
|
||||
$this->execute('ALTER TABLE `datasetcolumn` CHANGE `ListContent` `ListContent` VARCHAR( 1000 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL;');
|
||||
|
||||
$this->execute('ALTER TABLE `stat` ADD INDEX Type (`displayID`, `end`, `Type`);');
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
370
db/migrations/20180131113957_old_upgrade_step120_migration.php
Normal file
370
db/migrations/20180131113957_old_upgrade_step120_migration.php
Normal file
@@ -0,0 +1,370 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep120Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 120;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$log = $this->table('log');
|
||||
$log->removeColumn('scheduleId')
|
||||
->removeColumn('layoutId')
|
||||
->removeColumn('mediaId')
|
||||
->removeColumn('requestUri')
|
||||
->removeColumn('remoteAddr')
|
||||
->removeColumn('userAgent')
|
||||
->changeColumn('type', 'string', ['limit' => 254])
|
||||
->addColumn('channel', 'string', ['limit' => 5, 'after' => 'logDate'])
|
||||
->addColumn('runNo', 'string', ['limit' => 10])
|
||||
->save();
|
||||
|
||||
$module = $this->table('module');
|
||||
$module->addColumn('viewPath', 'string', ['limit' => 254, 'default' => '../modules'])
|
||||
->addColumn('class', 'string', ['limit' => 254])
|
||||
->save();
|
||||
|
||||
$permission = $this->table('permission', ['id' => 'permissionId']);
|
||||
$permission->addColumn('entityId', 'integer')
|
||||
->addColumn('groupId', 'integer')
|
||||
->addColumn('objectId', 'integer')
|
||||
->addColumn('view', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->addColumn('edit', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->addColumn('delete', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->save();
|
||||
|
||||
$permissionEntity = $this->table('permissionentity', ['id' => 'entityId']);
|
||||
$permissionEntity->addColumn('entity', 'string', ['limit' => 50])
|
||||
->addIndex('entity', ['unique' => true])
|
||||
->insert([
|
||||
['entity' => 'Xibo\\Entity\\Campaign'],
|
||||
['entity' => 'Xibo\\Entity\\DataSet'],
|
||||
['entity' => 'Xibo\\Entity\\DisplayGroup'],
|
||||
['entity' => 'Xibo\\Entity\\Media'],
|
||||
['entity' => 'Xibo\\Entity\\Page'],
|
||||
['entity' => 'Xibo\\Entity\\Playlist'],
|
||||
['entity' => 'Xibo\\Entity\\Region'],
|
||||
['entity' => 'Xibo\\Entity\\Widget'],
|
||||
])
|
||||
->save();
|
||||
|
||||
$this->execute('INSERT INTO `permission` (`groupId`, `entityId`, `objectId`, `view`, `edit`, `delete`) SELECT groupId, 1, pageId, 1, 0, 0 FROM `lkpagegroup`;');
|
||||
$this->execute('INSERT INTO `permission` (`groupId`, `entityId`, `objectId`, `view`, `edit`, `delete`) SELECT groupId, 5, campaignId, view, edit, del FROM `lkcampaigngroup`;');
|
||||
$this->execute('INSERT INTO `permission` (`groupId`, `entityId`, `objectId`, `view`, `edit`, `delete`) SELECT groupId, 4, mediaId, view, edit, del FROM `lkmediagroup`;');
|
||||
$this->execute('INSERT INTO `permission` (`groupId`, `entityId`, `objectId`, `view`, `edit`, `delete`) SELECT groupId, 9, dataSetId, view, edit, del FROM `lkdatasetgroup`;');
|
||||
$this->execute('INSERT INTO `permission` (`groupId`, `entityId`, `objectId`, `view`, `edit`, `delete`) SELECT groupId, 3, displayGroupId, view, edit, del FROM `lkdisplaygroupgroup`;');
|
||||
|
||||
$this->table('lkpagegroup')->drop()->save();
|
||||
$this->table('lkmenuitemgroup')->drop()->save();
|
||||
$this->table('lkcampaigngroup')->drop()->save();
|
||||
$this->table('lkmediagroup')->drop()->save();
|
||||
$this->table('lkdatasetgroup')->drop()->save();
|
||||
$this->table('lkdisplaygroupgroup')->drop()->save();
|
||||
|
||||
$pages = $this->table('pages');
|
||||
$pages
|
||||
->removeIndexByName('pages_ibfk_1')
|
||||
->dropForeignKey('pageGroupId')
|
||||
->removeColumn('pageGroupId')
|
||||
->addColumn('title', 'string', ['limit' => 50])
|
||||
->addColumn('asHome', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY, 'default' => 0])
|
||||
->insert([
|
||||
['name' => 'region', 'title' => ''],
|
||||
['name' => 'playlist', 'title' => ''],
|
||||
['name' => 'maintenance', 'title' => ''],
|
||||
])
|
||||
->save();
|
||||
|
||||
$this->execute('INSERT INTO `permission` (`groupId`, `entityId`, `objectId`, `view`, `edit`, `delete`) SELECT `groupId`, 1, (SELECT pageId FROM `pages` WHERE `name` = \'region\'), `view`, `edit`, `delete` FROM `permission` WHERE `objectId` = (SELECT pageId FROM `pages` WHERE `name` = \'layout\') AND `entityId` = 1;');
|
||||
$this->execute('INSERT INTO `permission` (`groupId`, `entityId`, `objectId`, `view`, `edit`, `delete`) SELECT `groupId`, 1, (SELECT pageId FROM `pages` WHERE `name` = \'playlist\'), `view`, `edit`, `delete` FROM `permission` WHERE `objectId` = (SELECT pageId FROM `pages` WHERE `name` = \'layout\') AND `entityId` = 1;');
|
||||
$this->execute('UPDATE `pages` SET title = CONCAT(UCASE(LEFT(name, 1)), SUBSTRING(name, 2)), asHome = 1;');
|
||||
$this->execute('UPDATE `pages` SET `name` = \'audit\' WHERE `name` = \'auditlog\';');
|
||||
$this->execute('UPDATE `pages` SET asHome = 0 WHERE `name` IN (\'update\',\'admin\',\'manual\',\'help\',\'clock\',\'preview\',\'region\',\'playlist\',\'maintenance\');');
|
||||
$this->execute('UPDATE `pages` SET `name` = \'library\', `title` = \'Library\' WHERE `pages`.`name` = \'content\';');
|
||||
$this->execute('UPDATE `pages` SET `name` = \'applications\', `title` = \'Applications\' WHERE `pages`.`name` = \'oauth\';');
|
||||
$this->execute('UPDATE `pages` SET `title` = \'Media Dashboard\' WHERE `pages`.`name` = \'mediamanager\';');
|
||||
$this->execute('UPDATE `pages` SET `title` = \'Status Dashboard\' WHERE `pages`.`name` = \'statusdashboard\';');
|
||||
$this->execute('UPDATE `pages` SET `title` = \'Display Profiles\' WHERE `pages`.`name` = \'displayprofile\';');
|
||||
$this->execute('UPDATE `pages` SET `title` = \'Display Groups\' WHERE `pages`.`name` = \'displaygroup\';');
|
||||
$this->execute('UPDATE `pages` SET `title` = \'Home\' WHERE `pages`.`name` = \'index\';');
|
||||
$this->execute('UPDATE `pages` SET `title` = \'Audit Trail\' WHERE `pages`.`name` = \'auditlog\';');
|
||||
|
||||
$this->table('menuitem')->drop()->save();
|
||||
$this->table('menu')->drop()->save();
|
||||
$this->table('pagegroup')->drop()->save();
|
||||
|
||||
$layout = $this->table('layout');
|
||||
$layout->addColumn('width', 'decimal')
|
||||
->addColumn('height', 'decimal')
|
||||
->addColumn('backgroundColor', 'string', ['limit' => 25, 'null' => true])
|
||||
->addColumn('backgroundzIndex', 'integer', ['default' => 1])
|
||||
->addColumn('schemaVersion', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->changeColumn('xml', 'text', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::TEXT_LONG, 'null' => true])
|
||||
->addColumn('statusMessage', 'text', ['null' => true])
|
||||
->save();
|
||||
|
||||
$this->execute('UPDATE `user` SET homepage = IFNULL((SELECT pageId FROM `pages` WHERE pages.name = `user`.homepage LIMIT 1), 1);');
|
||||
$this->execute('ALTER TABLE `user` CHANGE `homepage` `homePageId` INT NOT NULL DEFAULT \'1\' COMMENT \'The users homepage\';');
|
||||
|
||||
$this->execute('DELETE FROM module WHERE module = \'counter\';');
|
||||
|
||||
$linkRegionPlaylist = $this->table('lkregionplaylist', ['id' => false, 'primary_key' => 'regionId', 'playlistId', 'displayOrder']);
|
||||
$linkRegionPlaylist->addColumn('regionId', 'integer')
|
||||
->addColumn('playlistId', 'integer')
|
||||
->addColumn('displayOrder', 'integer')
|
||||
->save();
|
||||
|
||||
$linkWidgetMedia = $this->table('lkwidgetmedia', ['id' => false, 'primary_key' => ['widgetId', 'mediaId']]);
|
||||
$linkWidgetMedia->addColumn('widgetId', 'integer')
|
||||
->addColumn('mediaId', 'integer')
|
||||
->save();
|
||||
|
||||
$playlist = $this->table('playlist', ['id' => 'playlistId']);
|
||||
$playlist->addColumn('name', 'string', ['limit' => 254])
|
||||
->addColumn('ownerId', 'integer')
|
||||
->save();
|
||||
|
||||
$region = $this->table('region', ['id' => 'regionId']);
|
||||
$region
|
||||
->addColumn('layoutId', 'integer')
|
||||
->addColumn('ownerId', 'integer')
|
||||
->addColumn('name', 'string', ['limit' => 254, 'null' => true])
|
||||
->addColumn('width', 'decimal')
|
||||
->addColumn('height', 'decimal')
|
||||
->addColumn('top', 'decimal')
|
||||
->addColumn('left', 'decimal')
|
||||
->addColumn('zIndex', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_SMALL])
|
||||
->addColumn('duration', 'integer', ['default' => 0])
|
||||
->save();
|
||||
|
||||
$regionOption = $this->table('regionoption', ['id' => false, 'primary_key' => ['regionId', 'option']]);
|
||||
$regionOption->addColumn('regionId', 'integer')
|
||||
->addColumn('option', 'string', ['limit' => 50])
|
||||
->addColumn('value', 'text', ['null' => true])
|
||||
->save();
|
||||
|
||||
$widget = $this->table('widget', ['id' => 'widgetId']);
|
||||
$widget
|
||||
->addColumn('playlistId', 'integer')
|
||||
->addColumn('ownerId', 'integer')
|
||||
->addColumn('type', 'string', ['limit' => 50])
|
||||
->addColumn('duration', 'integer')
|
||||
->addColumn('displayOrder', 'integer')
|
||||
->addColumn('calculatedDuration', 'integer')
|
||||
->addColumn('useDuration', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY, 'default' => 1])
|
||||
->addForeignKey('playlistId', 'playlist', 'playlistId')
|
||||
->addForeignKey('ownerId', 'user', 'userId')
|
||||
->save();
|
||||
|
||||
$widgetOption = $this->table('widgetoption', ['id' => false, 'primary_key' => ['widgetId', 'type', 'option']]);
|
||||
$widgetOption->addColumn('widgetId', 'integer')
|
||||
->addColumn('type', 'string', ['limit' => 50])
|
||||
->addColumn('option', 'string', ['limit' => 254])
|
||||
->addColumn('value', 'text', ['null' => true])
|
||||
->addForeignKey('widgetId', 'widget', 'widgetId')
|
||||
->save();
|
||||
|
||||
$this->table('oauth_log')->drop()->save();
|
||||
$this->table('oauth_server_nonce')->drop()->save();
|
||||
$this->table('oauth_server_token')->drop()->save();
|
||||
$this->table('oauth_server_registry')->drop()->save();
|
||||
|
||||
// New oAuth tables
|
||||
$oauthClients = $this->table('oauth_clients', ['id' => false, 'primary_key' => ['id']]);
|
||||
$oauthClients
|
||||
->addColumn('id', 'string', ['limit' => 254])
|
||||
->addColumn('secret', 'string', ['limit' => 254])
|
||||
->addColumn('name', 'string', ['limit' => 254])
|
||||
->addColumn('userId', 'integer')
|
||||
->addColumn('authCode', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->addColumn('clientCredentials', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->save();
|
||||
|
||||
$oauthSessions = $this->table('oauth_sessions');
|
||||
$oauthSessions
|
||||
->addColumn('owner_type', 'string', ['limit' => 254])
|
||||
->addColumn('owner_id', 'string', ['limit' => 254])
|
||||
->addColumn('client_id', 'string', ['limit' => 254])
|
||||
->addColumn('client_redirect_uri', 'string', ['limit' => 500, 'null' => true])
|
||||
->addForeignKey('client_id', 'oauth_clients', 'id', ['delete' => 'CASCADE'])
|
||||
->save();
|
||||
|
||||
$oauthScopes = $this->table('oauth_scopes', ['id' => false, 'primary_key' => ['id']]);
|
||||
$oauthScopes
|
||||
->addColumn('id', 'string', ['limit' => 254])
|
||||
->addColumn('description', 'string', ['limit' => 1000])
|
||||
->save();
|
||||
|
||||
$oauthAccessTokens = $this->table('oauth_access_tokens', ['id' => false, 'primary_key' => ['access_token']]);
|
||||
$oauthAccessTokens
|
||||
->addColumn('access_token', 'string', ['limit' => 254])
|
||||
->addColumn('session_id', 'integer')
|
||||
->addColumn('expire_time', 'integer')
|
||||
->addForeignKey('session_id', 'oauth_sessions', 'id', ['delete' => 'CASCADE'])
|
||||
->save();
|
||||
|
||||
$oauthAccessTokenScopes = $this->table('oauth_access_token_scopes');
|
||||
$oauthAccessTokenScopes
|
||||
->addColumn('access_token', 'string', ['limit' => 254])
|
||||
->addColumn('scope', 'string', ['limit' => 254])
|
||||
->addForeignKey('access_token', 'oauth_access_tokens', 'access_token', ['delete' => 'CASCADE'])
|
||||
->addForeignKey('scope', 'oauth_scopes', 'id', ['delete' => 'CASCADE'])
|
||||
->save();
|
||||
|
||||
$oauthAuthCodes = $this->table('oauth_auth_codes', ['id' => false, 'primary_key' => ['auth_code']]);
|
||||
$oauthAuthCodes
|
||||
->addColumn('auth_code', 'string', ['limit' => 254])
|
||||
->addColumn('session_id', 'integer')
|
||||
->addColumn('expire_time', 'integer')
|
||||
->addColumn('client_redirect_uri', 'string', ['limit' => 500])
|
||||
->addForeignKey('session_id', 'oauth_sessions', 'id', ['delete' => 'CASCADE'])
|
||||
->save();
|
||||
|
||||
$oauthAuthCodeScopes = $this->table('oauth_auth_code_scopes');
|
||||
$oauthAuthCodeScopes
|
||||
->addColumn('auth_code', 'string', ['limit' => 254])
|
||||
->addColumn('scope', 'string', ['limit' => 254])
|
||||
->addForeignKey('auth_code', 'oauth_auth_codes', 'auth_code', ['delete' => 'CASCADE'])
|
||||
->addForeignKey('scope', 'oauth_scopes', 'id', ['delete' => 'CASCADE'])
|
||||
->save();
|
||||
|
||||
$oauthClientRedirects = $this->table('oauth_client_redirect_uris');
|
||||
$oauthClientRedirects
|
||||
->addColumn('client_id', 'string', ['limit' => 254])
|
||||
->addColumn('redirect_uri', 'string', ['limit' => 500])
|
||||
->save();
|
||||
|
||||
$oauthRefreshToeksn = $this->table('oauth_refresh_tokens', ['id' => false, 'primary_key' => ['refresh_token']]);
|
||||
$oauthRefreshToeksn
|
||||
->addColumn('refresh_token', 'string', ['limit' => 254])
|
||||
->addColumn('expire_time', 'integer')
|
||||
->addColumn('access_token', 'string', ['limit' => 254])
|
||||
->addForeignKey('access_token', 'oauth_access_tokens', 'access_token', ['delete' => 'CASCADE'])
|
||||
->save();
|
||||
|
||||
$oauthSessionsScopes = $this->table('oauth_session_scopes');
|
||||
$oauthSessionsScopes
|
||||
->addColumn('session_id', 'integer')
|
||||
->addColumn('scope', 'string', ['limit' => 254])
|
||||
->addForeignKey('session_id', 'oauth_sessions', 'id', ['delete' => 'CASCADE'])
|
||||
->addForeignKey('scope', 'oauth_scopes', 'id', ['delete' => 'CASCADE'])
|
||||
->save();
|
||||
|
||||
$this->table('file')->drop()->save();
|
||||
|
||||
$this->execute('TRUNCATE TABLE `xmdsnonce`;');
|
||||
$this->execute('RENAME TABLE `xmdsnonce` TO `requiredfile`;');
|
||||
|
||||
$requiredFile = $this->table('requiredfile');
|
||||
$requiredFile->addColumn('requestKey', 'string', ['limit' => 10])
|
||||
->addColumn('bytesRequested', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_BIG])
|
||||
->addColumn('complete' , 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->save();
|
||||
|
||||
$this->execute('ALTER TABLE `requiredfile` CHANGE `nonceId` `rfId` BIGINT( 20 ) NOT NULL AUTO_INCREMENT;');
|
||||
$this->execute('ALTER TABLE `requiredfile` CHANGE `regionId` `regionId` INT NULL;');
|
||||
$this->execute('ALTER TABLE `requiredfile` DROP `fileId`;');
|
||||
|
||||
$display = $this->table('display');
|
||||
$display
|
||||
->removeColumn('MediaInventoryXml')
|
||||
->save();
|
||||
|
||||
$this->execute('DELETE FROM `setting` WHERE setting = \'USE_INTL_DATEFORMAT\';');
|
||||
$this->execute('UPDATE `setting` SET `options` = \'Emergency|Alert|Critical|Error|Warning|Notice|Info|Debug\', value = \'Error\' WHERE setting = \'audit\';');
|
||||
$this->execute('UPDATE `setting` SET `options` = \'private|group|public\' WHERE `setting`.`setting` IN (\'MEDIA_DEFAULT\', \'LAYOUT_DEFAULT\');');
|
||||
$this->execute('INSERT INTO `setting` (`settingid`, `setting`, `value`, `fieldType`, `helptext`, `options`, `cat`, `userChange`, `title`, `validation`, `ordering`, `default`, `userSee`, `type`) VALUES (NULL, \'INSTANCE_SUSPENDED\', \'0\', \'checkbox\', \'Is this instance suspended?\', NULL, \'general\', \'0\', \'Instance Suspended\', \'\', \'120\', \'0\', \'0\', \'checkbox\'),(NULL, \'INHERIT_PARENT_PERMISSIONS\', \'1\', \'checkbox\', \'Inherit permissions from Parent when adding a new item?\', NULL, \'permissions\', \'1\', \'Inherit permissions\', \'\', \'50\', \'1\', \'1\', \'checkbox\');');
|
||||
$this->execute('INSERT INTO `datatype` (`DataTypeID`, `DataType`) VALUES (\'5\', \'Library Image\');');
|
||||
$this->execute('UPDATE `datatype` SET `DataType` = \'External Image\' WHERE `datatype`.`DataTypeID` =4 AND `datatype`.`DataType` = \'Image\' LIMIT 1 ;');
|
||||
|
||||
$this->table('lkdatasetlayout')->drop()->save();
|
||||
|
||||
$this->execute('CREATE TABLE `temp_lkmediadisplaygroup` AS SELECT `mediaid` ,`displaygroupid` FROM `lkmediadisplaygroup` WHERE 1 GROUP BY `mediaid` ,`displaygroupid`;');
|
||||
$this->execute('DROP TABLE `lkmediadisplaygroup`;');
|
||||
$this->execute('RENAME TABLE `temp_lkmediadisplaygroup` TO `lkmediadisplaygroup`;');
|
||||
|
||||
$this->execute('ALTER TABLE `lkmediadisplaygroup` ADD UNIQUE (`mediaid` ,`displaygroupid`);');
|
||||
$this->execute('ALTER TABLE `lkcampaignlayout` ADD UNIQUE (`CampaignID` ,`LayoutID` ,`DisplayOrder`);');
|
||||
|
||||
$linkScheduleDisplayGroup = $this->table('lkscheduledisplaygroup', ['id' => false, 'primary_key' => ['eventId', 'displayGroupId']]);
|
||||
$linkScheduleDisplayGroup
|
||||
->addColumn('eventId', 'integer')
|
||||
->addColumn('displayGroupId', 'integer')
|
||||
->save();
|
||||
|
||||
$this->execute('ALTER TABLE `schedule_detail` DROP FOREIGN KEY `schedule_detail_ibfk_8` ;');
|
||||
$this->execute('ALTER TABLE `schedule_detail` DROP `DisplayGroupID`;');
|
||||
|
||||
// Get all events and their Associated display group id's
|
||||
foreach ($this->fetchAll('SELECT eventId, displayGroupIds FROM `schedule`') as $event) {
|
||||
// Ping open the displayGroupIds
|
||||
$displayGroupIds = explode(',', $event['displayGroupIds']);
|
||||
|
||||
// Construct some SQL to add the link
|
||||
$sql = 'INSERT INTO `lkscheduledisplaygroup` (eventId, displayGroupId) VALUES ';
|
||||
|
||||
foreach ($displayGroupIds as $id) {
|
||||
$sql .= '(' . $event['eventId'] . ',' . $id . '),';
|
||||
}
|
||||
|
||||
$sql = rtrim($sql, ',');
|
||||
|
||||
$this->execute($sql);
|
||||
}
|
||||
|
||||
$this->execute('ALTER TABLE `schedule` DROP `DisplayGroupIDs`;');
|
||||
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\Image\' WHERE module = \'Image\';');
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\Video\' WHERE module = \'Video\';');
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\Flash\' WHERE module = \'Flash\';');
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\PowerPoint\' WHERE module = \'PowerPoint\';');
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\WebPage\' WHERE module = \'Webpage\';');
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\Ticker\' WHERE module = \'Ticker\';');
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\Text\' WHERE module = \'Text\';');
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\Embedded\' WHERE module = \'Embedded\';');
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\DataSetView\' WHERE module = \'datasetview\';');
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\ShellCommand\' WHERE module = \'shellcommand\';');
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\LocalVideo\' WHERE module = \'localvideo\';');
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\GenericFile\' WHERE module = \'genericfile\';');
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\Clock\' WHERE module = \'Clock\';');
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\Font\' WHERE module = \'Font\';');
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\Twitter\' WHERE module = \'Twitter\';');
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\ForecastIo\' WHERE module = \'forecastio\';');
|
||||
$this->execute('UPDATE `module` SET `class` = \'\\\\Xibo\\\\Widget\\\\Finance\' WHERE module = \'Finance\';');
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
144
db/migrations/20180131114002_old_upgrade_step121_migration.php
Normal file
144
db/migrations/20180131114002_old_upgrade_step121_migration.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2024 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep121Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 121;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$display = $this->table('display');
|
||||
$display
|
||||
->addColumn('xmrChannel', 'string', ['limit' => 254, 'null' => true])
|
||||
->addColumn('xmrPubKey', 'text', ['null' => true])
|
||||
->addColumn('lastCommandSuccess', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY, 'default' => 2])
|
||||
->save();
|
||||
|
||||
$settings = $this->table('setting');
|
||||
$settings
|
||||
->insert([
|
||||
[
|
||||
'setting' => 'XMR_ADDRESS',
|
||||
'title' => 'XMR Private Address',
|
||||
'helptext' => 'Please enter the private address for XMR.',
|
||||
'value' => 'http:://localhost:8081',
|
||||
'fieldType' => 'checkbox',
|
||||
'options' => '',
|
||||
'cat' => 'displays',
|
||||
'userChange' => '1',
|
||||
'type' => 'string',
|
||||
'validation' => '',
|
||||
'ordering' => '5',
|
||||
'default' => 'http:://localhost:8081',
|
||||
'userSee' => '1',
|
||||
],
|
||||
[
|
||||
'setting' => 'XMR_PUB_ADDRESS',
|
||||
'title' => 'XMR Public Address',
|
||||
'helptext' => 'Please enter the public address for XMR.',
|
||||
'value' => 'tcp:://localhost:5556',
|
||||
'fieldType' => 'dropdown',
|
||||
'options' => 'Checked|Unchecked',
|
||||
'cat' => 'displays',
|
||||
'userChange' => '1',
|
||||
'type' => 'string',
|
||||
'validation' => '',
|
||||
'ordering' => '6',
|
||||
'default' => 'tcp:://localhost:5556',
|
||||
'userSee' => '1',
|
||||
]
|
||||
])
|
||||
->save();
|
||||
|
||||
$linkLayoutDisplayGroup = $this->table('lklayoutdisplaygroup', ['comment' => 'Layout associations directly to Display Groups']);
|
||||
$linkLayoutDisplayGroup->addColumn('layoutId', 'integer')
|
||||
->addColumn('displayGroupId', 'integer')
|
||||
->addIndex(['layoutId', 'displayGroupId'], ['unique' => true])
|
||||
->save();
|
||||
|
||||
$pages = $this->table('pages');
|
||||
$pages->insert([
|
||||
'name' => 'command',
|
||||
'title' => 'Commands',
|
||||
'asHome' => 1
|
||||
])->save();
|
||||
|
||||
$command = $this->table('command', ['id' => 'commandId']);
|
||||
$command->addColumn('command', 'string', ['limit' => 254])
|
||||
->addColumn('code', 'string', ['limit' => 50])
|
||||
->addColumn('description', 'string', ['limit' => 1000, 'null' => true])
|
||||
->addColumn('userId', 'integer')
|
||||
->save();
|
||||
|
||||
$linkCommandDisplayProfile = $this->table('lkcommanddisplayprofile', ['id' => false, 'primary_key' => ['commandId', 'displayProfileId']]);
|
||||
$linkCommandDisplayProfile->addColumn('commandId', 'integer')
|
||||
->addColumn('displayProfileId', 'integer')
|
||||
->addColumn('commandString', 'string', ['limit' => 1000])
|
||||
->addColumn('validationString', 'string', ['limit' => 1000])
|
||||
->save();
|
||||
|
||||
$schedule = $this->table('schedule');
|
||||
$schedule->changeColumn('campaignId', 'integer', ['null' => true])
|
||||
->addColumn('eventTypeId', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY, 'after' => 'eventId', 'default' => 1])
|
||||
->addColumn('commandId', 'integer', ['after' => 'campaignId'])
|
||||
->changeColumn('toDt', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_BIG, 'null' => true])
|
||||
->save();
|
||||
|
||||
$this->execute('UPDATE `schedule` SET `eventTypeId` = 1;');
|
||||
|
||||
$scheduleDetail = $this->table('schedule_detail');
|
||||
$scheduleDetail->changeColumn('toDt', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_BIG, 'null' => true])
|
||||
->save();
|
||||
|
||||
$media = $this->table('media');
|
||||
$media->addColumn('released', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY, 'default' => 1])
|
||||
->addColumn('apiRef', 'string', ['limit' => 254, 'null' => true])
|
||||
->save();
|
||||
|
||||
$user = $this->table('user');
|
||||
$user->addColumn('firstName', 'string', ['limit' => 254, 'null' => true])
|
||||
->addColumn('lastName', 'string', ['limit' => 254, 'null' => true])
|
||||
->addColumn('phone', 'string', ['limit' => 254, 'null' => true])
|
||||
->addColumn('ref1', 'string', ['limit' => 254, 'null' => true])
|
||||
->addColumn('ref2', 'string', ['limit' => 254, 'null' => true])
|
||||
->addColumn('ref3', 'string', ['limit' => 254, 'null' => true])
|
||||
->addColumn('ref4', 'string', ['limit' => 254, 'null' => true])
|
||||
->addColumn('ref5', 'string', ['limit' => 254, 'null' => true])
|
||||
->save();
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep122Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 122;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$sql = '
|
||||
SELECT TABLE_NAME
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_SCHEMA = \'' . $_SERVER['MYSQL_DATABASE'] . '\'
|
||||
AND ENGINE = \'MyISAM\'
|
||||
';
|
||||
|
||||
foreach ($this->fetchAll($sql) as $table) {
|
||||
$this->execute('ALTER TABLE `' . $table['TABLE_NAME'] . '` ENGINE=INNODB', []);
|
||||
}
|
||||
|
||||
$auditLog = $this->table('auditlog');
|
||||
$auditLog->changeColumn('userId', 'integer', ['null' => true])
|
||||
->save();
|
||||
|
||||
$dataSet = $this->table('dataset');
|
||||
$dataSet->addColumn('code', 'string', ['limit' => 50, 'null' => true])
|
||||
->addColumn('isLookup', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY, 'default' => 0])
|
||||
->save();
|
||||
|
||||
$module = $this->table('module');
|
||||
$module->addColumn('defaultDuration', 'integer')
|
||||
->save();
|
||||
|
||||
$this->execute('UPDATE `module` SET defaultDuration = 10;');
|
||||
$this->execute('UPDATE `module` SET defaultDuration = (SELECT MAX(value) FROM `setting` WHERE setting = \'jpg_length\') WHERE `module` = \'image\';');
|
||||
$this->execute('UPDATE `module` SET defaultDuration = (SELECT MAX(value) FROM `setting` WHERE setting = \'swf_length\') WHERE `module` = \'flash\';');
|
||||
$this->execute('UPDATE `module` SET defaultDuration = (SELECT MAX(value) FROM `setting` WHERE setting = \'ppt_length\') WHERE `module` = \'powerpoint\';');
|
||||
$this->execute('UPDATE `module` SET defaultDuration = 0 WHERE `module` = \'video\';');
|
||||
$this->execute('DELETE FROM `setting` WHERE setting IN (\'ppt_length\', \'jpg_length\', \'swf_length\');');
|
||||
$this->execute('UPDATE `widget` SET `calculatedDuration` = `duration`;');
|
||||
|
||||
$userOption = $this->table('useroption', ['id' => false, 'primary_key' => ['userId', 'option']]);
|
||||
$userOption->addColumn('userId', 'integer')
|
||||
->addColumn('option', 'string', ['limit' => 50])
|
||||
->addColumn('value', 'text')
|
||||
->save();
|
||||
|
||||
$displayGroup = $this->table('displaygroup');
|
||||
$displayGroup->addColumn('isDynamic', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->addColumn('dynamicCriteria', 'string', ['null' => true, 'limit' => 2000])
|
||||
->addColumn('userId', 'integer')
|
||||
->save();
|
||||
|
||||
$this->execute('UPDATE `displaygroup` SET userId = (SELECT userId FROM `user` WHERE usertypeid = 1 LIMIT 1) WHERE userId = 0;');
|
||||
|
||||
$session = $this->table('session');
|
||||
$session->removeColumn('lastPage')
|
||||
->removeColumn('securityToken')
|
||||
->save();
|
||||
|
||||
$linkDisplayGroup = $this->table('lkdgdg', ['id' => false, ['primary_key' => ['parentId', 'childId', 'depth']]]);
|
||||
$linkDisplayGroup
|
||||
->addColumn('parentId', 'integer')
|
||||
->addColumn('childId', 'integer')
|
||||
->addColumn('depth', 'integer')
|
||||
->addIndex(['childId', 'parentId', 'depth'], ['unique' => true])
|
||||
->save();
|
||||
|
||||
$this->execute('INSERT INTO `lkdgdg` (parentId, childId, depth) SELECT displayGroupId, displayGroupId, 0 FROM `displaygroup` WHERE `displayGroupID` NOT IN (SELECT `parentId` FROM `lkdgdg` WHERE depth = 0);');
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep123Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 123;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$schedule = $this->table('schedule');
|
||||
$schedule->addColumn('dayPartId', 'integer')
|
||||
->changeColumn('fromDt', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_BIG, 'null' => true])
|
||||
->save();
|
||||
|
||||
// The following was added in step 92, we need to check to see if we already have this
|
||||
if (!$this->fetchRow('SELECT * FROM setting WHERE setting = \'CDN_URL\'')) {
|
||||
$settings = $this->table('setting');
|
||||
$settings
|
||||
->insert([
|
||||
'setting' => 'CDN_URL',
|
||||
'title' => 'CDN Address',
|
||||
'helptext' => 'Content Delivery Network Address for serving file requests to Players',
|
||||
'value' => '',
|
||||
'fieldType' => 'text',
|
||||
'options' => '',
|
||||
'cat' => 'network',
|
||||
'userChange' => '0',
|
||||
'type' => 'string',
|
||||
'validation' => '',
|
||||
'ordering' => '33',
|
||||
'default' => '',
|
||||
'userSee' => '0',
|
||||
])
|
||||
->save();
|
||||
}
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
137
db/migrations/20180131114017_old_upgrade_step124_migration.php
Normal file
137
db/migrations/20180131114017_old_upgrade_step124_migration.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep124Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 124;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$group = $this->table('group');
|
||||
$group->addColumn('isSystemNotification', 'integer', ['default' => 0])
|
||||
->insert([
|
||||
'group' => 'System Notifications',
|
||||
'isUserSpecific' => 0,
|
||||
'isSystemNotification' => 1
|
||||
])
|
||||
->save();
|
||||
|
||||
$notification = $this->table('notification', ['id' => 'notificationId']);
|
||||
$notification
|
||||
->addColumn('subject', 'string', ['limit' => 255])
|
||||
->addColumn('body', 'text', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::TEXT_LONG])
|
||||
->addColumn('createDt', 'integer')
|
||||
->addColumn('releaseDt', 'integer')
|
||||
->addColumn('isEmail', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->addColumn('isInterrupt', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->addColumn('isSystem', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->addColumn('userId', 'integer')
|
||||
->save();
|
||||
|
||||
$linkNotificationDg = $this->table('lknotificationdg', ['id' => 'lkNotificationDgId']);
|
||||
$linkNotificationDg
|
||||
->addColumn('notificationId', 'integer')
|
||||
->addColumn('displayGroupId', 'integer')
|
||||
->addIndex(['notificationId', 'displayGroupId'], ['unique' => true])
|
||||
->save();
|
||||
|
||||
$linkNotificationGroup = $this->table('lknotificationgroup', ['id' => 'lkNotificationGroupId']);
|
||||
$linkNotificationGroup
|
||||
->addColumn('notificationId', 'integer')
|
||||
->addColumn('groupId', 'integer')
|
||||
->addIndex(['notificationId', 'groupId'], ['unique' => true])
|
||||
->save();
|
||||
|
||||
$linkNotificationUser = $this->table('lknotificationuser', ['id' => 'lkNotificationUserId']);
|
||||
$linkNotificationUser
|
||||
->addColumn('notificationId', 'integer')
|
||||
->addColumn('userId', 'integer')
|
||||
->addColumn('read', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->addColumn('readDt', 'integer')
|
||||
->addColumn('emailDt', 'integer')
|
||||
->addIndex(['notificationId', 'userId'], ['unique' => true])
|
||||
->save();
|
||||
|
||||
$pages = $this->table('pages');
|
||||
$pages->insert([
|
||||
[
|
||||
'name' => 'notification',
|
||||
'title' => 'Notifications',
|
||||
'asHome' => 0
|
||||
],
|
||||
[
|
||||
'name' => 'drawer',
|
||||
'title' => 'Notification Drawer',
|
||||
'asHome' => 0
|
||||
]
|
||||
])->save();
|
||||
|
||||
$permissionEntity = $this->table('permissionentity');
|
||||
$permissionEntity->insert([
|
||||
'entity' => '\\Xibo\\Entity\\Notification'
|
||||
])->save();
|
||||
|
||||
$this->execute('UPDATE `group` SET isSystemNotification = 1 WHERE isUserSpecific = 1 AND `groupId` IN (SELECT `groupId` FROM `lkusergroup` INNER JOIN `user` ON `user`.userId = `lkusergroup`.userId WHERE `user`.userTypeId = 1);');
|
||||
|
||||
// If we've run step 92 as part of this upgrade, then don't do the below
|
||||
$this->execute('ALTER TABLE `datasetcolumn` CHANGE `ListContent` `ListContent` VARCHAR( 1000 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL;');
|
||||
|
||||
if (!$this->checkIndexExists('stat', ['displayId', 'end', 'type'], false)) {
|
||||
$this->execute('ALTER TABLE `stat` ADD INDEX Type (`displayID`, `end`, `Type`);');
|
||||
}
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an index exists
|
||||
* @param string $table
|
||||
* @param string[] $columns
|
||||
* @param bool $isUnique
|
||||
* @return bool
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function checkIndexExists($table, $columns, $isUnique)
|
||||
{
|
||||
if (!is_array($columns) || count($columns) <= 0)
|
||||
throw new InvalidArgumentException('Incorrect call to checkIndexExists', 'columns');
|
||||
|
||||
// Use the information schema to see if the index exists or not.
|
||||
// all users have permission to the information schema
|
||||
$sql = '
|
||||
SELECT *
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE table_schema=DATABASE()
|
||||
AND table_name = \'' . $table . '\'
|
||||
AND non_unique = \'' . (($isUnique) ? 0 : 1) . '\'
|
||||
AND (
|
||||
';
|
||||
|
||||
$i = 0;
|
||||
foreach ($columns as $column) {
|
||||
$i++;
|
||||
|
||||
$sql .= (($i == 1) ? '' : ' OR') . ' (seq_in_index = \'' . $i . '\' AND column_name = \'' . $column . '\') ';
|
||||
}
|
||||
|
||||
$sql .= ' )';
|
||||
|
||||
$indexes = $this->fetchAll($sql);
|
||||
|
||||
return (count($indexes) === count($columns));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep125Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 125;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$schedule = $this->table('schedule');
|
||||
$schedule->changeColumn('is_priority', 'integer')
|
||||
->save();
|
||||
|
||||
$this->execute('
|
||||
INSERT INTO module (Module, Name, Enabled, RegionSpecific, Description, ImageUri, SchemaVersion, ValidExtensions, PreviewEnabled, assignable, render_as, settings, viewPath, class, defaultDuration) VALUES
|
||||
(\'audio\', \'Audio\', 1, 0, \'Audio - support varies depending on the client hardware\', \'forms/video.gif\', 1, \'mp3,wav\', 1, 1, null, null, \'../modules\', \'Xibo\\\\Widget\\\\Audio\', 0),
|
||||
(\'pdf\', \'PDF\', 1, 0, \'PDF document viewer\', \'forms/pdf.gif\', 1, \'pdf\', 1, 1, \'html\', null, \'../modules\', \'Xibo\\\\Widget\\\\Pdf\', 60);
|
||||
');
|
||||
|
||||
$linkWidgetAudio = $this->table('lkwidgetaudio', ['id' => false, 'primary_key' => ['widgetId', 'mediaId']]);
|
||||
$linkWidgetAudio->addColumn('widgetId', 'integer')
|
||||
->addColumn('mediaId', 'integer')
|
||||
->addColumn('volume', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->addColumn('loop', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->save();
|
||||
|
||||
$oauthClientScopes = $this->table('oauth_client_scopes');
|
||||
$oauthClientScopes
|
||||
->addColumn('clientId', 'string', ['limit' => 254])
|
||||
->addColumn('scopeId', 'string', ['limit' => 254])
|
||||
->addIndex(['clientId', 'scopeId'], ['unique' => true])
|
||||
->save();
|
||||
|
||||
// Bulk insert doesn't appear to handle non auto-index primary keys?!
|
||||
$this->execute('
|
||||
INSERT INTO `oauth_scopes` (id, description) VALUES
|
||||
(\'all\', \'All access\'),
|
||||
(\'mcaas\', \'Media Conversion as a Service\')
|
||||
');
|
||||
|
||||
$oauthRouteScopes = $this->table('oauth_scope_routes');
|
||||
$oauthRouteScopes
|
||||
->addColumn('scopeId', 'string', ['limit' => 254])
|
||||
->addColumn('route', 'string', ['limit' => 1000])
|
||||
->addColumn('method', 'string', ['limit' => 8])
|
||||
->insert([
|
||||
['scopeId' => 'mcaas', 'route' => '/', 'method' => 'GET'],
|
||||
['scopeId' => 'mcaas', 'route' => '/library/download/:id(/:type)', 'method' => 'GET'],
|
||||
['scopeId' => 'mcaas', 'route' => '/library/mcaas/:id', 'method' => 'POST'],
|
||||
])
|
||||
->save();
|
||||
|
||||
$module = $this->table('module');
|
||||
$module->addColumn('installName', 'string', ['limit' => 254, 'null' => true])
|
||||
->save();
|
||||
|
||||
$this->execute('ALTER TABLE display CHANGE isAuditing auditingUntil int NOT NULL DEFAULT \'0\' COMMENT \'Is this display auditing\';');
|
||||
|
||||
$this->execute('INSERT INTO setting (setting, value, fieldType, helptext, options, cat, userChange, title, validation, ordering, `default`, userSee, type) VALUES (\'ELEVATE_LOG_UNTIL\', \'1463396415\', \'datetime\', \'Elevate the log level until this date.\', null, \'troubleshooting\', 1, \'Elevate Log Until\', \' \', 25, \'\', 1, \'datetime\');');
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep126Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 126;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$stat = $this->table('stat');
|
||||
$stat->addColumn('widgetId', 'integer', ['null' => true])
|
||||
->save();
|
||||
|
||||
$displayEvent = $this->table('displayevent', ['id' => 'displayEventId']);
|
||||
$displayEvent
|
||||
->addColumn('eventDate', 'integer')
|
||||
->addColumn('displayId', 'integer')
|
||||
->addColumn('start', 'integer')
|
||||
->addColumn('end', 'integer', ['null' => true])
|
||||
->addIndex('eventDate')
|
||||
->addIndex('end')
|
||||
->save();
|
||||
|
||||
$this->execute('INSERT INTO displayevent (eventDate, displayId, start, end) SELECT UNIX_TIMESTAMP(statDate), displayID, UNIX_TIMESTAMP(start), UNIX_TIMESTAMP(end) FROM stat WHERE Type = \'displaydown\';');
|
||||
|
||||
$this->execute('DELETE FROM stat WHERE Type = \'displaydown\';');
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
db/migrations/20180131114030_old_upgrade_step127_migration.php
Normal file
126
db/migrations/20180131114030_old_upgrade_step127_migration.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep127Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 127;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$schedule = $this->table('schedule');
|
||||
$schedule->addColumn('recurrenceRepeatsOn', 'string', ['null' => true])
|
||||
->save();
|
||||
|
||||
$this->execute('INSERT INTO `setting` (`setting`, `value`, `fieldType`, `helptext`, `options`, `cat`, `userChange`, `title`, `validation`, `ordering`, `default`, `userSee`, `type`) VALUES (\'RESTING_LOG_LEVEL\', \'Error\', \'dropdown\', \'Set the level of the resting log level. The CMS will revert to this log level after an elevated period ends. In production systems \"error\" is recommended.\', \'Emergency|Alert|Critical|Error\', \'troubleshooting\', 1, \'Resting Log Level\', \'\', 19, \'error\', 1, \'word\');');
|
||||
|
||||
$dataSet = $this->table('dataset');
|
||||
$dataSet->changeColumn('code', 'string', ['limit' => 50, 'null' => true])
|
||||
->save();
|
||||
|
||||
$this->execute('INSERT INTO `pages` (`name`, `Title`, `asHome`) VALUES (\'daypart\', \'Dayparting\', 0);');
|
||||
|
||||
$dayPart = $this->table('daypart', ['id' => 'dayPartId']);
|
||||
$dayPart
|
||||
->addColumn('name', 'string', ['limit' => 50])
|
||||
->addColumn('description', 'string', ['limit' => 50, 'null' => true])
|
||||
->addColumn('isRetired', 'integer', ['default' => 0, 'limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->addColumn('userId', 'integer')
|
||||
->addColumn('startTime', 'string', ['limit' => 8, 'default' => '00:00:00'])
|
||||
->addColumn('endTime', 'string', ['limit' => 8, 'default' => '00:00:00'])
|
||||
->addColumn('exceptions', 'text')
|
||||
->save();
|
||||
|
||||
$this->execute('INSERT INTO `permissionentity` (`entityId`, `entity`) VALUES (NULL, \'Xibo\\Entity\\DayPart\');');
|
||||
|
||||
$user = $this->table('user');
|
||||
$user->changeColumn('userPassword', 'string', ['limit' => 255]);
|
||||
|
||||
$this->execute('INSERT INTO pages (name, title, asHome) VALUES (\'task\', \'Task\', 1);');
|
||||
|
||||
$this->execute('INSERT INTO setting (setting, value, fieldType, helptext, options, cat, userChange, title, validation, ordering, `default`, userSee, type) VALUES (\'TASK_CONFIG_LOCKED_CHECKB\', \'Unchecked\', \'dropdown\', \'Is the task config locked? Useful for Service providers.\', \'Checked|Unchecked\', \'defaults\', 0, \'Lock Task Config\', \'\', 30, \'Unchecked\', 0, \'word\');');
|
||||
|
||||
$task = $this->table('task', ['id' => 'taskId']);
|
||||
$task
|
||||
->addColumn('name', 'string', ['limit' => 254])
|
||||
->addColumn('class', 'string', ['limit' => 254])
|
||||
->addColumn('status', 'integer', ['default' => 2, 'limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->addColumn('pid', 'integer')
|
||||
->addColumn('options', 'text')
|
||||
->addColumn('schedule', 'string', ['limit' => 254])
|
||||
->addColumn('lastRunDt', 'integer')
|
||||
->addColumn('lastRunMessage', 'string', ['null' => true])
|
||||
->addColumn('lastRunStatus', 'integer', ['default' => 0, 'limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->addColumn('lastRunDuration', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_SMALL])
|
||||
->addColumn('lastRunExitCode', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_SMALL])
|
||||
->addColumn('isActive', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->addColumn('runNow', 'integer', ['default' => 0, 'limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->addColumn('configFile', 'string', ['limit' => 254])
|
||||
->insert([
|
||||
[
|
||||
'name' => 'Daily Maintenance',
|
||||
'class' => '\Xibo\XTR\MaintenanceDailyTask',
|
||||
'options' => '[]',
|
||||
'schedule' => '0 0 * * * *',
|
||||
'isActive' => '1',
|
||||
'configFile' => '/tasks/maintenance-daily.task'
|
||||
],
|
||||
[
|
||||
'name' => 'Regular Maintenance',
|
||||
'class' => '\Xibo\XTR\MaintenanceRegularTask',
|
||||
'options' => '[]',
|
||||
'schedule' => '*/5 * * * * *',
|
||||
'isActive' => '1',
|
||||
'configFile' => '/tasks/maintenance-regular.task'
|
||||
],
|
||||
[
|
||||
'name' => 'Email Notifications',
|
||||
'class' => '\Xibo\XTR\EmailNotificationsTask',
|
||||
'options' => '[]',
|
||||
'schedule' => '*/5 * * * * *',
|
||||
'isActive' => '1',
|
||||
'configFile' => '/tasks/email-notifications.task'
|
||||
],
|
||||
[
|
||||
'name' => 'Stats Archive',
|
||||
'class' => '\Xibo\XTR\StatsArchiveTask',
|
||||
'options' => '{"periodSizeInDays":"7","maxPeriods":"4"}',
|
||||
'schedule' => '0 0 * * Mon',
|
||||
'isActive' => '1',
|
||||
'configFile' => '/tasks/stats-archiver.task'
|
||||
],
|
||||
[
|
||||
'name' => 'Remove old Notifications',
|
||||
'class' => '\Xibo\XTR\NotificationTidyTask',
|
||||
'options' => '{"maxAgeDays":"7","systemOnly":"1","readOnly":"0"}',
|
||||
'schedule' => '15 0 * * *',
|
||||
'isActive' => '1',
|
||||
'configFile' => '/tasks/notification-tidy.task'
|
||||
]
|
||||
])
|
||||
->save();
|
||||
|
||||
$this->execute('INSERT INTO `setting` (setting, value, fieldType, helptext, options, cat, userChange, title, validation, ordering, `default`, userSee, type) VALUES(\'WHITELIST_LOAD_BALANCERS\', \'\', \'text\', \'If the CMS is behind a load balancer, what are the load balancer IP addresses, comma delimited.\', \'\', \'network\', 1, \'Whitelist Load Balancers\', \'\', 100, \'\', 1, \'string\');');
|
||||
|
||||
$this->execute('INSERT INTO `setting` (setting, value, fieldType, helptext, options, cat, userChange, title, validation, ordering, `default`, userSee, type) VALUES(\'DEFAULT_LAYOUT\', \'1\', \'text\', \'The default layout to assign for new displays and displays which have their current default deleted.\', \'1\', \'displays\', 1, \'Default Layout\', \'\', 4, \'\', 1, \'int\');');
|
||||
|
||||
$display = $this->table('display');
|
||||
$display->addColumn('deviceName', 'string', ['null' => true])
|
||||
->save();
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
128
db/migrations/20180131114050_old_upgrade_step128_migration.php
Normal file
128
db/migrations/20180131114050_old_upgrade_step128_migration.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep128Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 128;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$this->execute('UPDATE `resolution` SET resolution = \'4k cinema\' WHERE resolution = \'4k\';');
|
||||
|
||||
$this->execute('INSERT INTO `resolution` (`resolution`, `width`, `height`, `intended_width`, `intended_height`, `version`, `enabled`) VALUES(\'4k UHD Landscape\', 450, 800, 3840, 2160, 2, 1),(\'4k UHD Portrait\', 800, 450, 2160, 3840, 2, 1);');
|
||||
|
||||
$this->execute('UPDATE schedule SET fromDt = 0, toDt = 2556057600 WHERE dayPartId = 1');
|
||||
|
||||
$this->table('schedule_detail')->drop()->save();
|
||||
|
||||
$schedule = $this->table('schedule');
|
||||
$schedule->addColumn('lastRecurrenceWatermark', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_BIG, 'null' => true])
|
||||
->save();
|
||||
|
||||
$this->table('requiredfile')->drop()->save();
|
||||
|
||||
$log = $this->table('log');
|
||||
$log
|
||||
->changeColumn('channel', 'string', ['limit' => 20])
|
||||
->save();
|
||||
|
||||
$this->execute('UPDATE `setting` SET `helpText` = \'The Time to Live (maxage) of the STS header expressed in seconds.\' WHERE `setting` = \'STS_TTL\';');
|
||||
|
||||
if (!$this->checkIndexExists('lkdisplaydg', ['displayGroupId', 'displayId'], 1)) {
|
||||
$index = 'CREATE UNIQUE INDEX lkdisplaydg_displayGroupId_displayId_uindex ON `lkdisplaydg` (displayGroupId, displayId);';
|
||||
|
||||
// Try to create the index, if we fail, assume duplicates
|
||||
try {
|
||||
$this->execute($index);
|
||||
} catch (\PDOException $e) {
|
||||
// Create a verify table
|
||||
$this->execute('CREATE TABLE lkdisplaydg_verify AS SELECT * FROM lkdisplaydg WHERE 1 GROUP BY displaygroupId, displayId;');
|
||||
|
||||
// Delete from original table
|
||||
$this->execute('DELETE FROM lkdisplaydg;');
|
||||
|
||||
// Insert the de-duped records
|
||||
$this->execute('INSERT INTO lkdisplaydg SELECT * FROM lkdisplaydg_verify;');
|
||||
|
||||
// Drop the verify table
|
||||
$this->execute('DROP TABLE lkdisplaydg_verify;');
|
||||
|
||||
// Create the index fresh, now that duplicates removed
|
||||
$this->execute($index);
|
||||
}
|
||||
}
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an index exists
|
||||
* @param string $table
|
||||
* @param string[] $columns
|
||||
* @param bool $isUnique
|
||||
* @return bool
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function checkIndexExists($table, $columns, $isUnique)
|
||||
{
|
||||
if (!is_array($columns) || count($columns) <= 0)
|
||||
throw new InvalidArgumentException('Incorrect call to checkIndexExists', 'columns');
|
||||
|
||||
// Use the information schema to see if the index exists or not.
|
||||
// all users have permission to the information schema
|
||||
$sql = '
|
||||
SELECT *
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE table_schema=DATABASE()
|
||||
AND table_name = \'' . $table . '\'
|
||||
AND non_unique = \'' . (($isUnique) ? 0 : 1) . '\'
|
||||
AND (
|
||||
';
|
||||
|
||||
$i = 0;
|
||||
foreach ($columns as $column) {
|
||||
$i++;
|
||||
|
||||
$sql .= (($i == 1) ? '' : ' OR') . ' (seq_in_index = \'' . $i . '\' AND column_name = \'' . $column . '\') ';
|
||||
}
|
||||
|
||||
$sql .= ' )';
|
||||
|
||||
$indexes = $this->fetchAll($sql);
|
||||
|
||||
return (count($indexes) === count($columns));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep129Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 129;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$requiredFile = $this->table('requiredfile', ['id' => 'rfId']);
|
||||
$requiredFile
|
||||
->addColumn('displayId', 'integer')
|
||||
->addColumn('type', 'string', ['limit' => 1])
|
||||
->addColumn('class', 'string', ['limit' => 1])
|
||||
->addColumn('itemId', 'integer', ['null' => true])
|
||||
->addColumn('bytesRequested', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_BIG])
|
||||
->addColumn('complete', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY, 'default' => 0])
|
||||
->addColumn('path', 'string', ['null' => true, 'limit' => 255])
|
||||
->addColumn('size', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_BIG, 'default' => 0])
|
||||
->addIndex(['displayId', 'type'])
|
||||
->save();
|
||||
|
||||
$resolution = $this->table('resolution');
|
||||
$resolution
|
||||
->addColumn('userId', 'integer')
|
||||
->save();
|
||||
|
||||
$this->execute('UPDATE `resolution` SET userId = 0;');
|
||||
|
||||
$this->execute('UPDATE `setting` SET `options` = \'private|group|group write|public|public write\' WHERE setting IN (\'MEDIA_DEFAULT\', \'LAYOUT_DEFAULT\');');
|
||||
|
||||
$linkCampaignTag = $this->table('lktagcampaign', ['id' => 'lkTagCampaignId']);
|
||||
$linkCampaignTag
|
||||
->addColumn('tagId', 'integer')
|
||||
->addColumn('campaignId', 'integer')
|
||||
->addIndex(['tagId', 'campaignId'], ['unique' => true])
|
||||
->save();
|
||||
|
||||
$display = $this->table('display');
|
||||
$display
|
||||
->addColumn('timeZone', 'string', ['limit' => 254, 'null' => true])
|
||||
->save();
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep130Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 130;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$schedule = $this->table('schedule');
|
||||
$schedule
|
||||
->addColumn('syncTimezone', 'integer', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY])
|
||||
->save();
|
||||
|
||||
$this->execute('UPDATE `permissionentity` SET entity = \'Xibo\\Entity\\Notification\' WHERE entity = \'XiboEntityNotification\';');
|
||||
|
||||
if (!$this->fetchRow('
|
||||
SELECT * FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS WHERE constraint_schema=DATABASE()
|
||||
AND `table_name` = \'oauth_clients%\' AND referenced_table_name = \'user\';')) {
|
||||
|
||||
$this->execute('UPDATE `oauth_clients` SET userId = (SELECT userId FROM `user` WHERE userTypeId = 1 LIMIT 1)
|
||||
WHERE userId NOT IN (SELECT userId FROM `user`);');
|
||||
|
||||
$this->execute('ALTER TABLE `oauth_clients` ADD CONSTRAINT oauth_clients_user_UserID_fk FOREIGN KEY (userId) REFERENCES `user` (UserID);');
|
||||
}
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep131Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 131;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$this->execute('INSERT INTO `setting` (`setting`, `value`, `fieldType`, `helptext`, `options`, `cat`, `userChange`, `title`, `validation`, `ordering`, `default`, `userSee`, `type`) VALUES (\'DISPLAY_PROFILE_STATS_DEFAULT\', \'0\', \'checkbox\', NULL, NULL, \'displays\', 1, \'Default setting for Statistics Enabled?\', \'\', 70, \'0\', 1, \'checkbox\'),(\'DISPLAY_PROFILE_CURRENT_LAYOUT_STATUS_ENABLED\', \'1\', \'checkbox\', NULL, NULL, \'displays\', 1, \'Enable the option to report the current layout status?\', \'\', 80, \'0\', 1, \'checkbox\'),(\'DISPLAY_PROFILE_SCREENSHOT_INTERVAL_ENABLED\', \'1\', \'checkbox\', NULL, NULL, \'displays\', 1, \'Enable the option to set the screenshot interval?\', \'\', 90, \'0\', 1, \'checkbox\'),(\'DISPLAY_PROFILE_SCREENSHOT_SIZE_DEFAULT\', \'200\', \'number\', \'The default size in pixels for the Display Screenshots\', NULL, \'displays\', 1, \'Display Screenshot Default Size\', \'\', 100, \'200\', 1, \'int\'),(\'LATEST_NEWS_URL\', \'https://xibosignage.com/feed\', \'text\', \'RSS/Atom Feed to be displayed on the Status Dashboard\', \'\', \'general\', 0, \'Latest News URL\', \'\', 111, \'\', 0, \'string\');');
|
||||
|
||||
$display = $this->table('display');
|
||||
$display->removeColumn('currentLayoutId')->save();
|
||||
|
||||
$permissionEntity = $this->table('permissionentity');
|
||||
$permissionEntity->insert([
|
||||
'entity' => 'Xibo\\Entity\\Display'
|
||||
])->save();
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep132Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 132;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$this->execute('UPDATE `schedule` SET toDt = 2147483647 WHERE toDt = 2556057600;');
|
||||
|
||||
$this->execute('UPDATE `permission` SET
|
||||
entityId = (
|
||||
SELECT entityId
|
||||
FROM permissionentity
|
||||
WHERE entity = \'Xibo\\Entity\\DisplayGroup\'),
|
||||
objectId = (
|
||||
SELECT lkdisplaydg.DisplayGroupID
|
||||
FROM `lkdisplaydg`
|
||||
INNER JOIN `displaygroup` ON `displaygroup`.DisplayGroupID = `lkdisplaydg`.DisplayGroupID
|
||||
AND `displaygroup`.IsDisplaySpecific = 1
|
||||
WHERE permission.objectId = `lkdisplaydg`.DisplayID)
|
||||
WHERE entityId IN (SELECT entityId FROM permissionentity WHERE entity = \'Xibo\\Entity\\Display\');');
|
||||
|
||||
$this->execute('DELETE FROM `permissionentity` WHERE `entity` = \'Xibo\\Entity\\Display\';');
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class OldUpgradeStep133Migration extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$STEP = 133;
|
||||
|
||||
// Are we an upgrade from an older version?
|
||||
if ($this->hasTable('version')) {
|
||||
// We do have a version table, so we're an upgrade from anything 1.7.0 onward.
|
||||
$row = $this->fetchRow('SELECT * FROM `version`');
|
||||
$dbVersion = $row['DBVersion'];
|
||||
|
||||
// Are we on the relevent step for this upgrade?
|
||||
if ($dbVersion < $STEP) {
|
||||
// Perform the upgrade
|
||||
$this->execute('INSERT INTO `setting` (`setting`, `value`, `fieldType`, `helptext`, `options`, `cat`, `userChange`, `title`, `validation`, `ordering`, `default`, `userSee`, `type`) VALUES (\'DISPLAY_LOCK_NAME_TO_DEVICENAME\', \'0\', \'checkbox\', NULL, NULL, \'displays\', 1, \'Lock the Display Name to the device name provided by the Player?\', \'\', 80, \'0\', 1, \'checkbox\');');
|
||||
|
||||
$task = $this->table('task');
|
||||
$task
|
||||
->addColumn('lastRunStartDt', 'integer', ['null' => true])
|
||||
->save();
|
||||
|
||||
// Bump our version
|
||||
$this->execute('UPDATE `version` SET DBVersion = ' . $STEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user