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