snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
128
e2e-tests/.ci/.e2erc
Normal file
@@ -0,0 +1,128 @@
|
||||
# shellcheck disable=SC2148,SC2155
|
||||
|
||||
# Function definitions
|
||||
mme2e_log () { echo "[$(date +%Y-%m-%dT%H:%M:%S%Z)]" "$@"; }
|
||||
mme2e_get_current_shopt_arg () {
|
||||
# This function lets you get the current value of shell options, e.g. pipefail or allexport, in the form
|
||||
# of arguments to pass to the 'set' shell function, in order to restore its value later
|
||||
SHOPT_ARG=${1:?}
|
||||
case $(set -o | sed -n -E "s/^${SHOPT_ARG}[[:space:]]+(o..?)$/\1/p") in
|
||||
on) echo -n "-o ${SHOPT_ARG}";;
|
||||
off) echo -n "+o ${SHOPT_ARG}";;
|
||||
*) return 1;;
|
||||
esac
|
||||
}
|
||||
mme2e_load_env_file () {
|
||||
# This loads the ./env file. Variables are automatically exported
|
||||
[ -f ./env ] || return 0
|
||||
MME2E_PREVIOUS_ALLEXPORT=$(mme2e_get_current_shopt_arg allexport)
|
||||
set -o allexport
|
||||
mme2e_log "Loading env file"
|
||||
. ./env
|
||||
# shellcheck disable=SC2086
|
||||
set ${MME2E_PREVIOUS_ALLEXPORT}
|
||||
}
|
||||
mme2e_generate_envfile_from_var_names () {
|
||||
# Read var names from stdin, one per line
|
||||
while read -r VARIABLE; do
|
||||
[ -z "${!VARIABLE:-}" ] || echo "${VARIABLE}=${!VARIABLE}";
|
||||
done
|
||||
}
|
||||
mme2e_wait_command_success () {
|
||||
COMMAND=${1?}
|
||||
RETRY_MESSAGE=${2?}
|
||||
RETRIES_LEFT=${3:-1}
|
||||
RETRIES_INTERVAL=${4:-10}
|
||||
MME2E_PREVIOUS_PIPEFAIL=$(mme2e_get_current_shopt_arg pipefail)
|
||||
set -o pipefail
|
||||
until bash -c "${COMMAND}"; do
|
||||
RETRIES_LEFT=$((RETRIES_LEFT - 1))
|
||||
[ "$RETRIES_LEFT" -le "0" ] && break
|
||||
mme2e_log "${RETRY_MESSAGE} ($RETRIES_LEFT retries left, sleeping $RETRIES_INTERVAL seconds)"
|
||||
sleep "$RETRIES_INTERVAL"
|
||||
done
|
||||
# shellcheck disable=SC2086
|
||||
set ${MME2E_PREVIOUS_PIPEFAIL}
|
||||
if [ "$RETRIES_LEFT" = "0" ]; then
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
mme2e_wait_service_healthy () {
|
||||
DC_COMMAND="${DC_COMMAND:-$MME2E_DC_SERVER}"
|
||||
SERVICE_NAME=${1?}
|
||||
RETRIES_LEFT=${2:-1}
|
||||
RETRIES_INTERVAL=${3:-10}
|
||||
mme2e_wait_command_success "${DC_COMMAND} ps ${SERVICE_NAME} | grep -qE '\(healthy\)'" "Waiting for ${SERVICE_NAME} container to be healthy" "$RETRIES_LEFT" "$RETRIES_INTERVAL"
|
||||
}
|
||||
mme2e_wait_image () {
|
||||
IMAGE_NAME=${1?}
|
||||
RETRIES_LEFT=${2:-1}
|
||||
RETRIES_INTERVAL=${3:-10}
|
||||
mme2e_wait_command_success "docker pull --platform linux/amd64 $IMAGE_NAME" "Waiting for docker image ${IMAGE_NAME} to be available" "$RETRIES_LEFT" "$RETRIES_INTERVAL"
|
||||
}
|
||||
mme2e_is_token_in_list() {
|
||||
local TOKEN=$1
|
||||
local LIST=$2
|
||||
grep -qE "(^| )$TOKEN( |$)" <<<"$LIST"
|
||||
}
|
||||
|
||||
# Call prerequisite utility functions
|
||||
mme2e_load_env_file
|
||||
|
||||
# Utility variables
|
||||
# NB: these assume you `source` them from the directory this file is in
|
||||
# Set up base docker compose file and export
|
||||
export MME2E_DC_SERVER="docker compose -p mmserver -f ./server.yml"
|
||||
export MME2E_DC_DASHBOARD="docker compose -p mmdashboard -f ./dashboard/docker/docker-compose.yml -f ./dashboard.override.yml"
|
||||
export MME2E_UID=$(id -u)
|
||||
export MME2E_OSTYPE=$(docker version -f '{{ .Client.Os }}')
|
||||
export MME2E_ARCHTYPE=$(docker version -f '{{ .Client.Arch }}')
|
||||
export NODE_VERSION_REQUIRED=$(cat ../../.nvmrc)
|
||||
|
||||
# Utility alias, for interactive shell usage. Can be reversed with 'unalias docker-compose-mmserver' in your shell
|
||||
# NB: this only works in interactive shells
|
||||
alias docker-compose-mmserver='${MME2E_DC_SERVER}'
|
||||
alias docker-compose-mmdashboard='${MME2E_DC_DASHBOARD}'
|
||||
|
||||
# Default values for optional variables
|
||||
export SERVER_IMAGE_DEFAULT="mattermostdevelopment/mattermost-enterprise-edition:$(git rev-parse --short=7 HEAD)"
|
||||
export BROWSER_DEFAULT="chrome"
|
||||
export SERVER_DEFAULT="onprem"
|
||||
export TEST_DEFAULT="cypress"
|
||||
export ENABLED_DOCKER_SERVICES_DEFAULT="postgres inbucket"
|
||||
export BRANCH_DEFAULT=$(git branch --show-current || echo -n "unknown")
|
||||
export BUILD_ID_DEFAULT=$(date +%s)
|
||||
export CI_BASE_URL_DEFAULT="localhost"
|
||||
# Test tool default overrides
|
||||
case "${TEST:-$TEST_DEFAULT}" in
|
||||
cypress )
|
||||
export TEST_FILTER_DEFAULT='--stage=@prod --group=@smoke' ;;
|
||||
playwright )
|
||||
export TEST_FILTER_DEFAULT='--grep @smoke' ;;
|
||||
* )
|
||||
export TEST_FILTER_DEFAULT='' ;;
|
||||
esac
|
||||
# OS specific defaults overrides
|
||||
case $MME2E_OSTYPE in
|
||||
linux )
|
||||
export IS_LINUX="-linux"
|
||||
;;
|
||||
darwin )
|
||||
export BROWSER_DEFAULT="chrome"
|
||||
export IS_LINUX=""
|
||||
;;
|
||||
* )
|
||||
esac
|
||||
|
||||
# Populate the optional variables that are used in the docker-compose file
|
||||
export SERVER_IMAGE=${SERVER_IMAGE:-$SERVER_IMAGE_DEFAULT}
|
||||
export BROWSER=${BROWSER:-$BROWSER_DEFAULT}
|
||||
export SERVER=${SERVER:-$SERVER_DEFAULT}
|
||||
export TEST=${TEST:-$TEST_DEFAULT}
|
||||
export ENABLED_DOCKER_SERVICES=${ENABLED_DOCKER_SERVICES:-$ENABLED_DOCKER_SERVICES_DEFAULT}
|
||||
export TEST_FILTER=${TEST_FILTER:-$TEST_FILTER_DEFAULT}
|
||||
export BRANCH_UNSANITIZED=${BRANCH:-$BRANCH_DEFAULT}
|
||||
export BRANCH=${BRANCH_UNSANITIZED::50} # The automation dashboard only accepts branch names up to 50 characters
|
||||
export BUILD_ID_UNSANITIZED=${BUILD_ID:-$BUILD_ID_DEFAULT}
|
||||
export BUILD_ID=${BUILD_ID_UNSANITIZED::64} # The automation dashboard only accepts build IDs up to 64 characters
|
||||
export CI_BASE_URL=${CI_BASE_URL:-$CI_BASE_URL_DEFAULT}
|
||||
4
e2e-tests/.ci/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
dashboard
|
||||
server.yml
|
||||
env
|
||||
.env.*
|
||||
4
e2e-tests/.ci/.shellcheckrc
Normal file
@@ -0,0 +1,4 @@
|
||||
# SC1091: Not following: <file>
|
||||
# https://www.shellcheck.net/wiki/SC1091
|
||||
disable=SC1091
|
||||
# Used to ignore for sourcing ".e2erc*"
|
||||
21
e2e-tests/.ci/dashboard.entrypoint.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
set -e -u -o pipefail
|
||||
|
||||
npm install
|
||||
|
||||
# Run migrations. This is also a way to wait for the database to be up
|
||||
MIGRATION_ATTEMPTS_LEFT=10
|
||||
MIGRATION_ATTEMPTS_INTERVAL=10
|
||||
until npm run migrate:latest; do
|
||||
MIGRATION_ATTEMPTS_LEFT=$((MIGRATION_ATTEMPTS_LEFT - 1))
|
||||
[ "$MIGRATION_ATTEMPTS_LEFT" -gt 0 ] || break
|
||||
echo "Migration script failed, sleeping $MIGRATION_ATTEMPTS_INTERVAL"
|
||||
sleep $MIGRATION_ATTEMPTS_INTERVAL
|
||||
done
|
||||
[ "$MIGRATION_ATTEMPTS_LEFT" -gt 0 ] || {
|
||||
echo "Migration script failed, exhausted attempts. Giving up."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Launch the dashboard
|
||||
exec npm run dev
|
||||
26
e2e-tests/.ci/dashboard.generate_test_cycle.sh
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
set -e -u -o pipefail
|
||||
cd "$(dirname "$0")"
|
||||
. .e2erc
|
||||
|
||||
TEST_ENV_FILE=".env.$TEST"
|
||||
|
||||
mme2e_log "Loading variables from $TEST_ENV_FILE"
|
||||
if ! [ -f "$TEST_ENV_FILE" ]; then
|
||||
mme2e_log "Error: $TEST_ENV_FILE is required to exist, for the test cycle to be generated. Aborting." >&2
|
||||
exit 1
|
||||
fi
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "$TEST_ENV_FILE"
|
||||
|
||||
if [ -z "${AUTOMATION_DASHBOARD_URL:-}" ]; then
|
||||
mme2e_log "AUTOMATION_DASHBOARD_URL is unset. Skipping test cycle generation."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mme2e_log "Generating the test cycle on the Automation Dashboard"
|
||||
cd "../$TEST"
|
||||
npm i
|
||||
# shellcheck disable=SC2086
|
||||
exec node --trace-warnings generate_test_cycle.js $TEST_FILTER
|
||||
25
e2e-tests/.ci/dashboard.override.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
services:
|
||||
dashboard:
|
||||
image: mattermostdevelopment/mirrored-node:18.17
|
||||
environment:
|
||||
PG_URI: postgres://mmuser:mostest_password@db:5432/automation_dashboard_db
|
||||
JWT_SECRET: s8gGBA3ujKRohSw1L8HLOY7Jjnu2ZYv8 # Generated with e.g. `dd if=/dev/urandom count=24 bs=1 2>/dev/null | base64 -w0`
|
||||
JWT_USER: cypress-test
|
||||
JWT_ROLE: integration
|
||||
JWT_ALG: HS256
|
||||
JWT_EXPIRES_IN: 365d
|
||||
ALLOWED_USER: cypress-test
|
||||
ALLOWED_ROLE: integration
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-s", "-o/dev/null", "127.0.0.1:4000"]
|
||||
interval: 10s
|
||||
timeout: 15s
|
||||
retries: 12
|
||||
volumes:
|
||||
- "../:/app"
|
||||
- "../../dashboard.entrypoint.sh:/usr/local/bin/dashboard.entrypoint.sh:ro"
|
||||
working_dir: /app
|
||||
entrypoint: /usr/local/bin/dashboard.entrypoint.sh
|
||||
ports:
|
||||
- 4000:4000
|
||||
50
e2e-tests/.ci/dashboard.start.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
set -e -u -o pipefail
|
||||
cd "$(dirname "$0")"
|
||||
. .e2erc
|
||||
|
||||
MME2E_DASHBOARD_REF_DEFAULT="origin/main"
|
||||
MME2E_DASHBOARD_REF=${MME2E_DASHBOARD_REF:-$MME2E_DASHBOARD_REF_DEFAULT}
|
||||
|
||||
mme2e_log "Cloning the automation-dashboard project"
|
||||
if [ ! -d dashboard ]; then
|
||||
git clone https://github.com/saturninoabril/automation-dashboard.git dashboard
|
||||
# Must reinitialize some variables that depend on the dashboard repo being checked out
|
||||
. .e2erc
|
||||
fi
|
||||
git -C dashboard fetch
|
||||
git -C dashboard checkout "$MME2E_DASHBOARD_REF"
|
||||
|
||||
mme2e_log "Starting the dashboard"
|
||||
${MME2E_DC_DASHBOARD} up -d db dashboard
|
||||
|
||||
if ! DC_COMMAND="${MME2E_DC_DASHBOARD}" mme2e_wait_service_healthy dashboard 60 10; then
|
||||
mme2e_log "Dashboard container not healthy, retry attempts exhausted. Giving up." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mme2e_log "Generating the dashboard's local URL"
|
||||
case $MME2E_OSTYPE in
|
||||
darwin)
|
||||
# shellcheck disable=SC2046
|
||||
AUTOMATION_DASHBOARD_IP=$(ifconfig $(route get 1.1.1.1 | grep interface | awk '{print $2}') | grep -w inet | awk '{print $2}')
|
||||
;;
|
||||
*)
|
||||
# shellcheck disable=SC2046
|
||||
MME2E_DC_DASHBOARD_NETWORK=$(docker inspect $(${MME2E_DC_DASHBOARD} ps -q dashboard) | jq -r '.[0].NetworkSettings.Networks | (keys|.[0])')
|
||||
AUTOMATION_DASHBOARD_IP=$(docker network inspect "$MME2E_DC_DASHBOARD_NETWORK" | jq -r '.[0].IPAM.Config[0].Gateway')
|
||||
;;
|
||||
esac
|
||||
# shellcheck disable=SC2034
|
||||
AUTOMATION_DASHBOARD_URL="http://${AUTOMATION_DASHBOARD_IP}:4000/api"
|
||||
|
||||
mme2e_log "Generating a signed JWT token for accessing the dashboard"
|
||||
# Not running 'npm install'. We assume it has been run by the dashboard container's entrypoint
|
||||
# shellcheck disable=SC2034
|
||||
AUTOMATION_DASHBOARD_TOKEN=$(${MME2E_DC_DASHBOARD} exec -T dashboard node script/sign.js | awk '{ print $2; }') # The token secret is specified in the dashboard.override.yml file
|
||||
|
||||
mme2e_log "Generating the .env.dashboard file, to point Cypress to the dashboard URL"
|
||||
mme2e_generate_envfile_from_var_names >.env.dashboard <<EOF
|
||||
AUTOMATION_DASHBOARD_URL
|
||||
AUTOMATION_DASHBOARD_TOKEN
|
||||
EOF
|
||||
12
e2e-tests/.ci/dashboard.stop.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
set -e -u -o pipefail
|
||||
cd "$(dirname "$0")"
|
||||
. .e2erc
|
||||
|
||||
if [ -d dashboard ]; then
|
||||
mme2e_log "Stopping the dashboard containers"
|
||||
${MME2E_DC_DASHBOARD} down
|
||||
else
|
||||
# Base docker-compose file is found in the dashboard repo, we can't run docker commands without it
|
||||
mme2e_log "Not stopping the dashboard containers: dashboard repo not checked out locally, skipping"
|
||||
fi
|
||||
27
e2e-tests/.ci/report.collect.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# SC2034: <variable> appears unused.
|
||||
# https://www.shellcheck.net/wiki/SC2034
|
||||
# shellcheck disable=SC2034
|
||||
# shellcheck disable=SC2086,SC2223
|
||||
|
||||
set -e -u -o pipefail
|
||||
cd "$(dirname "$0")"
|
||||
. .e2erc
|
||||
|
||||
# The collected data will be written to the "e2e-tests/$TEST/results/" directory
|
||||
cd "../$TEST/"
|
||||
if [ ! -d "results/" ]; then
|
||||
mme2e_log "Error: 'results/' directory does not exist. Aborting report data collection." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If the Automation Dashboard is used, try to collect run data from it
|
||||
if [ -n "${AUTOMATION_DASHBOARD_URL:-}" ]; then
|
||||
mme2e_log "Automation Dashboard usage detected: retrieving cycle results"
|
||||
# Assume that we're in the cypress dir and that the 'results/' directory exists
|
||||
: ${BUILD_ID:?}
|
||||
AD_CYCLE_FILE="results/ad_cycle.json"
|
||||
AD_SPECS_FILE="results/ad_specs.json"
|
||||
curl -o "$AD_CYCLE_FILE" -fsSL "${AUTOMATION_DASHBOARD_URL}/cycle?build=${BUILD_ID}"
|
||||
curl -o "$AD_SPECS_FILE" -fsSL "${AUTOMATION_DASHBOARD_URL}/executions/specs?cycle_id=$(jq -r .id "$AD_CYCLE_FILE")"
|
||||
fi
|
||||
115
e2e-tests/.ci/report.publish.sh
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/bin/bash
|
||||
# SC2034: <variable> appears unused.
|
||||
# https://www.shellcheck.net/wiki/SC2034
|
||||
# shellcheck disable=SC2034
|
||||
# shellcheck disable=SC2086,SC2223
|
||||
|
||||
set -e -u -o pipefail
|
||||
cd "$(dirname "$0")"
|
||||
. .e2erc
|
||||
|
||||
# Default or assert variables required by the save_report.js script, and document optional variables
|
||||
: ${FULL_REPORT:=false} # Valid values: true, false
|
||||
: ${TYPE:=NONE} # Valid values: PR, RELEASE, MASTER, MASTER_UNSTABLE, CLOUD, CLOUD_UNSTABLE, NONE (which is the same as omitting it); also known as REPORT_TYPE
|
||||
: ${WEBHOOK_URL:-} # Optional. Mattermost webhook to post the report back to
|
||||
: ${RELEASE_DATE:-} # Optional. If set, its value will be included in the report as the release date of the tested artifact
|
||||
if [ "$TYPE" = "PR" ]; then
|
||||
# Try to determine PR number: first from PR_NUMBER, then from BRANCH (server-pr-XXXX format)
|
||||
if [ -n "${PR_NUMBER:-}" ]; then
|
||||
export PULL_REQUEST="https://github.com/mattermost/mattermost/pull/${PR_NUMBER}"
|
||||
elif grep -qE '^server-pr-[0-9]+$' <<<"${BRANCH:-}"; then
|
||||
PR_NUMBER="${BRANCH##*-}"
|
||||
export PULL_REQUEST="https://github.com/mattermost/mattermost/pull/${PR_NUMBER}"
|
||||
else
|
||||
mme2e_log "Warning: TYPE=PR but cannot determine PR number from PR_NUMBER or BRANCH. Falling back to TYPE=NONE."
|
||||
TYPE=NONE
|
||||
fi
|
||||
fi
|
||||
|
||||
# Env vars used during the test. Their values will be included in the report
|
||||
: ${TEST:?} # See E2E tests' readme
|
||||
: ${BRANCH:?} # May be either a ref, a commit hash, or 'server-pr-PR_NUMBER' (if TYPE=PR)
|
||||
: ${BUILD_ID:?}
|
||||
: ${SERVER:?} # May be either 'onprem' or 'cloud'
|
||||
: ${MM_ENV:-}
|
||||
|
||||
# Populate intermediate variables
|
||||
export BUILD_TAG="${SERVER_IMAGE##*/}"
|
||||
export MM_DOCKER_IMAGE="${BUILD_TAG%%:*}" # NB: the 'mattermostdevelopment/' prefix is assumed
|
||||
export MM_DOCKER_TAG="${BUILD_TAG##*:}"
|
||||
export SERVER_TYPE="${SERVER}"
|
||||
export AUTOMATION_DASHBOARD_FRONTEND_URL="${AUTOMATION_DASHBOARD_URL:+${AUTOMATION_DASHBOARD_URL%%/api}}"
|
||||
|
||||
if [ -n "${TM4J_API_KEY:-}" ]; then
|
||||
# Set additional variables required for Zephyr reporting
|
||||
if grep -q 'UNSTABLE' <<<"$TYPE"; then
|
||||
export TEST_SUITE=prod
|
||||
else
|
||||
export TEST_SUITE=unstable
|
||||
fi
|
||||
if [ "$TYPE" = "RELEASE" ]; then
|
||||
export SERVER_ARTIFACT_TYPE=release
|
||||
else
|
||||
export SERVER_ARTIFACT_TYPE=master
|
||||
export TEST_IS_DAILY=yes
|
||||
fi
|
||||
export TM4J_ENABLE=true
|
||||
export JIRA_PROJECT_KEY=MM
|
||||
export TM4J_ENVIRONMENT_NAME="${TEST@u}/${BROWSER@u}/${SERVER_ARTIFACT_TYPE}-${SERVER_TYPE}-ent${TEST_IS_DAILY:+-${TEST_SUITE}}"
|
||||
# Assert that the test type is among the ones supported by Zephyr, and select the corresponding folderId
|
||||
case "${SERVER}-${TYPE}" in
|
||||
onprem-RELEASE)
|
||||
export TM4J_FOLDER_ID="2014475"
|
||||
;;
|
||||
onprem-MASTER)
|
||||
export TM4J_FOLDER_ID="2014476"
|
||||
;;
|
||||
onprem-MASTER_UNSTABLE)
|
||||
export TM4J_FOLDER_ID="2014478"
|
||||
;;
|
||||
cloud-RELEASE)
|
||||
export TM4J_FOLDER_ID="2014474"
|
||||
;;
|
||||
cloud-CLOUD)
|
||||
export TM4J_FOLDER_ID="2014479"
|
||||
;;
|
||||
cloud-CLOUD_UNSTABLE)
|
||||
export TM4J_FOLDER_ID="2014481"
|
||||
;;
|
||||
*)
|
||||
mme2e_log "Error: unsupported Zephyr environment for the requested report (SERVER=${SERVER}, TYPE=${TYPE}). Aborting." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
: ${TEST_CYCLE_LINK_PREFIX:?}
|
||||
: ${TM4J_CYCLE_KEY:-} # Optional. Populated automatically by the reporting script
|
||||
: ${TM4J_CYCLE_NAME:-} # Optional. Populated automatically by the reporting script
|
||||
mme2e_log "TMJ4 integration enabled. Environment: $TM4J_ENVIRONMENT_NAME"
|
||||
fi
|
||||
|
||||
if [ -n "${DIAGNOSTIC_WEBHOOK_URL:-}" ]; then
|
||||
: ${DIAGNOSTIC_USER_ID:?}
|
||||
: ${DIAGNOSTIC_TEAM_ID:?}
|
||||
mme2e_log "Diagnostic report upload enabled."
|
||||
fi
|
||||
|
||||
# Double check that the "results/" subdirectory to collect report informations from exists
|
||||
cd "../${TEST}/"
|
||||
if [ ! -d "results/" ]; then
|
||||
mme2e_log "Error: 'results/' directory does not exist. Aborting report data collection." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$TEST" in
|
||||
cypress)
|
||||
npm i
|
||||
node save_report.js
|
||||
;;
|
||||
playwright)
|
||||
if [ -n "$WEBHOOK_URL" ]; then
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i
|
||||
# Utilize environment data and report files to generate the webhook body
|
||||
node report.webhookgen.js | curl -X POST -fsSL -H 'Content-Type: application/json' -d @- "$WEBHOOK_URL"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
41
e2e-tests/.ci/server.cloud_init.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
set -e -u -o pipefail
|
||||
cd "$(dirname "$0")"
|
||||
. .e2erc
|
||||
[ -f .env.cloud ] && . .env.cloud
|
||||
|
||||
if [ "$SERVER" != "cloud" ]; then
|
||||
mme2e_log "Skipping cloud instance initialization: operation supported only for cloud server, but running with SERVER='$SERVER'"
|
||||
exit 0
|
||||
elif [ -n "${MM_CUSTOMER_ID:-}" ]; then
|
||||
mme2e_log "Skipping cloud user creation: customer with ID '$MM_CUSTOMER_ID' is already configured. Please run 'make cloud-teardown' before creating a new user."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mme2e_log "Initializing cloud tests"
|
||||
|
||||
# Assert that required variables are set
|
||||
MME2E_ENVCHECK_MSG="variable required for initializing cloud tests, but is empty or unset."
|
||||
: "${CWS_URL:?$MME2E_ENVCHECK_MSG}"
|
||||
: "${MM_LICENSE:?$MME2E_ENVCHECK_MSG}"
|
||||
|
||||
response=$(curl -fsSL -X POST -H @- "${CWS_URL}/api/v1/tests/create-customer?sku=cloud-enterprise&is_paid=true" <<<"${CWS_EXTRA_HTTP_HEADERS:-}")
|
||||
MM_CUSTOMER_ID=$(echo "$response" | jq -r .customer_id)
|
||||
MM_CLOUD_API_KEY=$(echo "$response" | jq -r .api_key)
|
||||
MM_CLOUD_INSTALLATION_ID=$(echo "$response" | jq -r .installation_id)
|
||||
|
||||
export MM_CUSTOMER_ID=$MM_CUSTOMER_ID
|
||||
export MM_CLOUD_API_KEY=$MM_CLOUD_API_KEY
|
||||
export MM_CLOUD_INSTALLATION_ID=$MM_CLOUD_INSTALLATION_ID
|
||||
export MM_CLOUDSETTINGS_CWSURL=$CWS_URL
|
||||
export MM_CLOUDSETTINGS_CWSAPIURL=$CWS_URL
|
||||
|
||||
mme2e_generate_envfile_from_var_names >.env.cloud <<EOF
|
||||
MM_CLOUDSETTINGS_CWSURL
|
||||
MM_CLOUDSETTINGS_CWSAPIURL
|
||||
MM_CLOUD_API_KEY
|
||||
MM_CUSTOMER_ID
|
||||
MM_CLOUD_INSTALLATION_ID
|
||||
EOF
|
||||
|
||||
mme2e_log "Test cloud customer created, MM_CUSTOMER_ID: $MM_CUSTOMER_ID."
|
||||
26
e2e-tests/.ci/server.cloud_teardown.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
set -e -u -o pipefail
|
||||
cd "$(dirname "$0")"
|
||||
. .e2erc
|
||||
[ -f .env.cloud ] && . .env.cloud
|
||||
|
||||
if [ -z "${MM_CUSTOMER_ID:-}" ]; then
|
||||
mme2e_log "Skipping cloud instance teardown: MM_CUSTOMER_ID variable is empty, no cloud user to cleanup."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mme2e_log "Tearing down cloud tests"
|
||||
|
||||
mme2e_log "Loading .env.cloud"
|
||||
. .env.cloud
|
||||
|
||||
# Assert that required variables are set
|
||||
MME2E_ENVCHECK_MSG="variable required for tearing down cloud tests, but is empty or unset."
|
||||
: "${CWS_URL:?$MME2E_ENVCHECK_MSG}"
|
||||
: "${MM_CUSTOMER_ID:?$MME2E_ENVCHECK_MSG}"
|
||||
|
||||
mme2e_log "Deleting customer $MM_CUSTOMER_ID."
|
||||
curl -fsSL -X DELETE -H @- "${CWS_URL}/api/v1/tests/customers/$MM_CUSTOMER_ID/payment-customer" <<<"${CWS_EXTRA_HTTP_HEADERS:-}"
|
||||
rm -fv .env.cloud
|
||||
|
||||
mme2e_log "Test cloud customer deleted, MM_CUSTOMER_ID: $MM_CUSTOMER_ID."
|
||||
477
e2e-tests/.ci/server.generate.sh
Executable file
@@ -0,0 +1,477 @@
|
||||
#!/bin/bash
|
||||
# SC2034: <variable> appears unused.
|
||||
# https://www.shellcheck.net/wiki/SC2034
|
||||
# shellcheck disable=SC2034
|
||||
# Note: Variables are dynamically used depending on usage input (ENABLED_DOCKER_SERVICES)
|
||||
|
||||
set -e -u -o pipefail
|
||||
cd "$(dirname "$0")"
|
||||
. .e2erc
|
||||
|
||||
enable_docker_service() {
|
||||
local SERVICE_TO_ENABLE="$1"
|
||||
if ! mme2e_is_token_in_list "$SERVICE_TO_ENABLE" "$ENABLED_DOCKER_SERVICES"; then
|
||||
ENABLED_DOCKER_SERVICES="$ENABLED_DOCKER_SERVICES $SERVICE_TO_ENABLE"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_docker_services_validity() {
|
||||
local SERVICES_TO_CHECK="$*"
|
||||
local SERVICES_VALID="postgres minio inbucket openldap elasticsearch opensearch redis keycloak cypress webhook-interactions playwright"
|
||||
local SERVICES_REQUIRED="postgres inbucket"
|
||||
for SERVICE_NAME in $SERVICES_TO_CHECK; do
|
||||
if ! mme2e_is_token_in_list "$SERVICE_NAME" "$SERVICES_VALID"; then
|
||||
mme2e_log "Error, requested invalid service: $SERVICE_NAME" >&2
|
||||
mme2e_log "Valid services are: $SERVICES_VALID" >&2
|
||||
mme2e_log "Aborting" >&2
|
||||
exit 1
|
||||
fi
|
||||
SERVICES_REQUIRED="${SERVICES_REQUIRED/$SERVICE_NAME/}"
|
||||
done
|
||||
if [ -n "${SERVICES_REQUIRED/ /}" ]; then
|
||||
mme2e_log "Missing required services: $SERVICES_REQUIRED" >&2
|
||||
mme2e_log "Aborting" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
generate_docker_compose_file() {
|
||||
# Generating the server docker-compose file
|
||||
local DC_FILE="server.yml"
|
||||
mme2e_log "Generating docker-compose file in: $DC_FILE"
|
||||
|
||||
# Generate the docker compose override file
|
||||
cat <<EOL >"$DC_FILE"
|
||||
# Image hashes in this file are for amd64 systems
|
||||
# NB: May include paths relative to the "server/build" directory, which contains the original compose file that this yaml is overriding
|
||||
|
||||
services:
|
||||
server:
|
||||
image: \${SERVER_IMAGE}
|
||||
platform: linux/amd64
|
||||
restart: always
|
||||
env_file:
|
||||
- "./.env.server"
|
||||
environment:
|
||||
MM_SERVICESETTINGS_ALLOWCORSFROM: "*"
|
||||
MM_SERVICESETTINGS_ENABLELOCALMODE: "true"
|
||||
MM_SERVICESETTINGS_ENABLESECURITYFIXALERT: "false"
|
||||
MM_CONNECTEDWORKSPACESSETTINGS_ENABLEREMOTECLUSTERSERVICE: "true"
|
||||
MM_CONNECTEDWORKSPACESSETTINGS_ENABLESHAREDWORKSPACES: "true"
|
||||
MM_SQLSETTINGS_DATASOURCE: "postgres://mmuser:mostest_password@localhost:5432/mattermost_test?sslmode=disable&connect_timeout=10&binary_parameters=yes"
|
||||
MM_SQLSETTINGS_DRIVERNAME: "postgres"
|
||||
MM_EMAILSETTINGS_SMTPSERVER: "localhost"
|
||||
MM_CLUSTERSETTINGS_READONLYCONFIG: "false"
|
||||
MM_SERVICEENVIRONMENT: "test"
|
||||
MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES: "true"
|
||||
MM_FEATUREFLAGS_PERMISSIONPOLICIES: "true"
|
||||
MM_FEATUREFLAGS_TEAMMEMBERSHIPACCESSCONTROL: "true"
|
||||
MM_FEATUREFLAGS_CLASSIFICATIONMARKINGS: "true"
|
||||
MM_FEATUREFLAGS_INTEGRATEDBOARDS: "true"
|
||||
MM_FEATUREFLAGS_PROPERTYFIELDRANK: "true"
|
||||
MM_FEATUREFLAGS_RESOURCEATTRIBUTESINPOLICIES: "true"
|
||||
MM_FEATUREFLAGS_ATTRIBUTEVALUEMASKING: "true"
|
||||
MM_FEATUREFLAGS_WYSIWYGEDITOR: "true"
|
||||
MM_FEATUREFLAGS_RECURRINGSCHEDULEDPOSTS: "true"
|
||||
MM_FEATUREFLAGS_ENABLECONCURRENTREACT: "true"
|
||||
MM_LOGSETTINGS_ENABLEDIAGNOSTICS: "false"
|
||||
MM_LOGSETTINGS_CONSOLELEVEL: "DEBUG"
|
||||
network_mode: host
|
||||
ports:
|
||||
- "8065:8065"
|
||||
depends_on:
|
||||
$(for service in $ENABLED_DOCKER_SERVICES; do
|
||||
# The server container will start only if all other dependent services are healthy
|
||||
# Skip creating the dependency for containers that don't need a healthcheck
|
||||
if grep -qE "^(cypress|webhook-interactions|playwright)" <<<"$service"; then
|
||||
continue
|
||||
fi
|
||||
echo " $service:"
|
||||
echo " condition: service_healthy"
|
||||
done)
|
||||
|
||||
$(if mme2e_is_token_in_list "postgres" "$ENABLED_DOCKER_SERVICES"; then
|
||||
echo '
|
||||
postgres:
|
||||
image: mattermostdevelopment/mirrored-postgres:15
|
||||
restart: "no"
|
||||
network_mode: host
|
||||
networks: !reset []
|
||||
extends:
|
||||
file: ../../server/build/docker-compose.common.yml
|
||||
service: postgres'
|
||||
fi)
|
||||
|
||||
$(if mme2e_is_token_in_list "inbucket" "$ENABLED_DOCKER_SERVICES"; then
|
||||
echo '
|
||||
inbucket:
|
||||
network_mode: host
|
||||
networks: !reset []
|
||||
restart: "no"
|
||||
extends:
|
||||
file: ../../server/build/docker-compose.common.yml
|
||||
service: inbucket
|
||||
healthcheck:
|
||||
test: [ "CMD", "nc", "-z", "-w1", "127.0.0.1", "10025" ]
|
||||
interval: 10s
|
||||
timeout: 15s
|
||||
retries: 12'
|
||||
fi)
|
||||
|
||||
$(if mme2e_is_token_in_list "minio" "$ENABLED_DOCKER_SERVICES"; then
|
||||
echo '
|
||||
minio:
|
||||
network_mode: host
|
||||
networks: !reset []
|
||||
restart: "no"
|
||||
extends:
|
||||
file: ../../server/build/docker-compose.common.yml
|
||||
service: minio
|
||||
healthcheck:
|
||||
test: [ "CMD", "mc", "ready", "local" ]
|
||||
interval: 10s
|
||||
timeout: 15s
|
||||
retries: 12'
|
||||
fi)
|
||||
|
||||
$(if mme2e_is_token_in_list "openldap" "$ENABLED_DOCKER_SERVICES"; then
|
||||
echo '
|
||||
openldap:
|
||||
network_mode: host
|
||||
networks: !reset []
|
||||
restart: "no"
|
||||
extends:
|
||||
file: ../../server/build/docker-compose.common.yml
|
||||
service: openldap
|
||||
healthcheck:
|
||||
test: [ "CMD", "bash", "-o", "pipefail", "-c", "ss -ltn \"sport = :636\" | grep -qE \"^LISTEN\"" ]
|
||||
interval: 10s
|
||||
timeout: 15s
|
||||
retries: 12'
|
||||
fi)
|
||||
|
||||
$(if mme2e_is_token_in_list "elasticsearch" "$ENABLED_DOCKER_SERVICES"; then
|
||||
echo '
|
||||
elasticsearch:
|
||||
restart: "no"
|
||||
network_mode: host
|
||||
networks: !reset []
|
||||
environment:
|
||||
xpack.security.enabled: "false"
|
||||
action.destructive_requires_name: "false"
|
||||
extends:
|
||||
file: ../../server/build/docker-compose.common.yml
|
||||
service: elasticsearch
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-fsS", "localhost:9200/_cluster/health?wait_for_status=green&timeout=5s" ]
|
||||
interval: 10s
|
||||
timeout: 15s
|
||||
retries: 12'
|
||||
if [ "$MME2E_ARCHTYPE" = "arm64" ]; then
|
||||
echo '
|
||||
image: mattermostdevelopment/mattermost-elasticsearch:8.9.0
|
||||
platform: linux/arm64/v8'
|
||||
fi
|
||||
fi)
|
||||
|
||||
$(if mme2e_is_token_in_list "opensearch" "$ENABLED_DOCKER_SERVICES"; then
|
||||
echo '
|
||||
opensearch:
|
||||
restart: "no"
|
||||
network_mode: host
|
||||
networks: !reset []
|
||||
environment:
|
||||
http.port: 9200
|
||||
extends:
|
||||
file: ../../server/build/docker-compose.common.yml
|
||||
service: opensearch
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-fsS", "localhost:9200/_cluster/health?wait_for_status=green&timeout=5s" ]
|
||||
interval: 10s
|
||||
timeout: 15s
|
||||
retries: 12'
|
||||
fi)
|
||||
|
||||
$(if mme2e_is_token_in_list "redis" "$ENABLED_DOCKER_SERVICES"; then
|
||||
echo '
|
||||
redis:
|
||||
restart: "no"
|
||||
network_mode: host
|
||||
networks: !reset []
|
||||
extends:
|
||||
file: ../../server/build/docker-compose.common.yml
|
||||
service: redis
|
||||
healthcheck:
|
||||
test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ]
|
||||
interval: 10s
|
||||
timeout: 15s
|
||||
retries: 12'
|
||||
fi)
|
||||
|
||||
$(if mme2e_is_token_in_list "keycloak" "$ENABLED_DOCKER_SERVICES"; then
|
||||
echo '
|
||||
keycloak:
|
||||
restart: "no"
|
||||
network_mode: host
|
||||
networks: !reset []
|
||||
environment:
|
||||
JAVA_OPTS: "-Xms64m -Xmx2G -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true"
|
||||
volumes:
|
||||
- "../../server/build/docker/keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json"
|
||||
- "../../server/build/docker/keycloak/kc-healthcheck.sh:/usr/local/bin/kc-healthcheck.sh"
|
||||
extends:
|
||||
file: ../../server/build/docker-compose.common.yml
|
||||
service: keycloak
|
||||
healthcheck:
|
||||
# We cannot use a simple curl --silent localhost:9990/health | grep -q \"status\":\"UP\" because theres no curl in the image: https://www.keycloak.org/server/health#_using_the_health_checks
|
||||
test: [ "CMD", "/usr/local/bin/kc-healthcheck.sh" ]
|
||||
interval: 10s
|
||||
timeout: 15s
|
||||
retries: 12'
|
||||
fi)
|
||||
|
||||
$(if mme2e_is_token_in_list "cypress" "$ENABLED_DOCKER_SERVICES"; then
|
||||
echo '
|
||||
cypress:
|
||||
image: "cypress/browsers:node-24.14.0-chrome-145.0.7632.116-1-ff-148.0-edge-145.0.3800.70-1"
|
||||
platform: linux/amd64
|
||||
entrypoint: ["/bin/bash", "-c"]
|
||||
command: ["until [ -f /var/run/mm_terminate ]; do sleep 5; done"]
|
||||
env_file:
|
||||
- "../../e2e-tests/.ci/.env.cypress"
|
||||
environment:
|
||||
CYPRESS_baseUrl: "http://localhost:8065"
|
||||
CYPRESS_dbConnection: "postgres://mmuser:mostest_password@localhost:5432/mattermost_test?sslmode=disable&connect_timeout=10"
|
||||
CYPRESS_smtpUrl: "http://localhost:9001"
|
||||
CYPRESS_webhookBaseUrl: "http://localhost:3000"
|
||||
CYPRESS_chromeWebSecurity: "false"
|
||||
CYPRESS_firstTest: "true"
|
||||
CYPRESS_resetBeforeTest: "true"
|
||||
CYPRESS_allowedUntrustedInternalConnections: "localhost webhook-interactions"
|
||||
TM4J_ENABLE: "false"
|
||||
# disable shared memory X11 affecting Cypress v4 and Chrome
|
||||
# https://github.com/cypress-io/cypress-docker-images/issues/270
|
||||
QT_X11_NO_MITSHM: "1"
|
||||
_X11_NO_MITSHM: "1"
|
||||
_MITSHM: "0"
|
||||
# avoid too many progress messages
|
||||
# https://github.com/cypress-io/cypress/issues/1243
|
||||
CI: "1"
|
||||
# Use /tmp for Cypress cache and config so any UID can write without permission issues
|
||||
CYPRESS_CACHE_FOLDER: /tmp/cypress-cache
|
||||
XDG_CONFIG_HOME: /tmp/xdg-config
|
||||
# Ensure we are independent from the global node environment
|
||||
PATH: /e2e-tests/cypress/node_modules/.bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 8096
|
||||
hard: 1048576
|
||||
working_dir: /e2e-tests/cypress
|
||||
network_mode: host
|
||||
volumes:
|
||||
- "../../e2e-tests/cypress/:/e2e-tests/cypress"
|
||||
- "../../webapp/platform/eslint-plugin/:/webapp/platform/eslint-plugin"'
|
||||
fi)
|
||||
|
||||
$(if mme2e_is_token_in_list "webhook-interactions" "$ENABLED_DOCKER_SERVICES"; then
|
||||
# shellcheck disable=SC2016
|
||||
echo '
|
||||
webhook-interactions:
|
||||
image: node:${NODE_VERSION_REQUIRED}
|
||||
command: sh -c "npm init -y > /dev/null && npm install express@5.1.0 axios@1.11.0 client-oauth2@github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49 && exec node webhook_serve.js"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-s", "-o/dev/null", "127.0.0.1:3000"]
|
||||
interval: 10s
|
||||
timeout: 15s
|
||||
retries: 12
|
||||
working_dir: /webhook
|
||||
network_mode: host
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- "../../e2e-tests/cypress/webhook_serve.js:/webhook/webhook_serve.js:ro"
|
||||
- "../../e2e-tests/cypress/utils/:/webhook/utils:ro"
|
||||
- "../../e2e-tests/cypress/tests/plugins/post_message_as.js:/webhook/tests/plugins/post_message_as.js:ro"'
|
||||
fi)
|
||||
|
||||
$(if mme2e_is_token_in_list "playwright" "$ENABLED_DOCKER_SERVICES"; then
|
||||
# shellcheck disable=SC2016
|
||||
echo '
|
||||
playwright:
|
||||
image: mcr.microsoft.com/playwright:v1.62.0-noble
|
||||
entrypoint: ["/bin/bash", "-c"]
|
||||
command:
|
||||
- |
|
||||
# Install Node.js based on .nvmrc
|
||||
NODE_VERSION=$$(cat /mattermost/.nvmrc)
|
||||
echo "Installing Node.js $${NODE_VERSION}..."
|
||||
curl -fsSL https://deb.nodesource.com/setup_$${NODE_VERSION%%.*}.x | bash -
|
||||
apt-get install -y nodejs
|
||||
echo "Node.js version: $$(node --version)"
|
||||
# Wait for termination signal
|
||||
until [ -f /var/run/mm_terminate ]; do sleep 5; done
|
||||
env_file:
|
||||
- "./.env.playwright"
|
||||
environment:
|
||||
CI: "true"
|
||||
PLAYWRIGHT_SKIP_BROWSER_GC: 1
|
||||
PW_BASE_URL: http://localhost:8065
|
||||
PW_ADMIN_USERNAME: sysadmin
|
||||
PW_ADMIN_PASSWORD: Sys@dmin-sample1
|
||||
PW_ADMIN_EMAIL: sysadmin@sample.mattermost.com
|
||||
PW_ENSURE_PLUGINS_INSTALLED: ""
|
||||
MM_TEST_DB_URL: "postgres://mmuser:mostest_password@localhost:5432/mattermost_test?sslmode=disable&connect_timeout=10&binary_parameters=yes"
|
||||
PW_HA_CLUSTER_ENABLED: "false"
|
||||
PW_RESET_BEFORE_TEST: "false"
|
||||
PW_HEADLESS: "true"
|
||||
PW_SLOWMO: 0
|
||||
PW_WORKERS: 1
|
||||
PW_SNAPSHOT_ENABLE: "false"
|
||||
PW_PERCY_ENABLE: "false"
|
||||
PW_WEBHOOK_BASE_URL: http://localhost:3000
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 8096
|
||||
hard: 1048576
|
||||
working_dir: /mattermost
|
||||
network_mode: host
|
||||
volumes:
|
||||
- "../../:/mattermost"'
|
||||
fi)
|
||||
EOL
|
||||
|
||||
mme2e_log "docker-compose file generated."
|
||||
}
|
||||
|
||||
generate_env_files() {
|
||||
# Generate .env.server
|
||||
mme2e_log "Generating .env.server"
|
||||
truncate -s 0 .env.server
|
||||
|
||||
# Setting SERVER-specific variables
|
||||
case "$SERVER" in
|
||||
cloud)
|
||||
echo "MM_NOTIFY_ADMIN_COOL_OFF_DAYS=0.00000001" >>.env.server
|
||||
echo 'MM_FEATUREFLAGS_ANNUALSUBSCRIPTION="true"' >>.env.server
|
||||
# Load .env.cloud into .env.server
|
||||
# We assume the file exist at this point. The actual check for that should be done before calling this function
|
||||
cat >>.env.server <.env.cloud
|
||||
;;
|
||||
esac
|
||||
|
||||
# Setting MM_ENV-injected variables
|
||||
# shellcheck disable=SC2086
|
||||
envarr=$(echo ${MM_ENV:-} | tr "," "\n")
|
||||
for env in $envarr; do
|
||||
echo "$env" >>.env.server
|
||||
done
|
||||
|
||||
# Generating service-specific env vars
|
||||
for SERVICE in $ENABLED_DOCKER_SERVICES; do
|
||||
case $SERVICE in
|
||||
opensearch)
|
||||
echo "MM_ELASTICSEARCHSETTINGS_BACKEND=opensearch" >>.env.server
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Generating TEST-specific env files
|
||||
# Some are defaulted in .e2erc due to being needed to other scripts as well
|
||||
export REPO=mattermost # Static, but declared here for making generate_test_cycle.js easier to run
|
||||
export HEADLESS=true # Static, but declared here for making generate_test_cycle.js easier to run
|
||||
case "$TEST" in
|
||||
cypress)
|
||||
mme2e_log "Cypress: Generating .env.cypress"
|
||||
truncate -s 0 .env.cypress
|
||||
|
||||
mme2e_generate_envfile_from_var_names >.env.cypress <<-EOF
|
||||
BRANCH
|
||||
BUILD_ID
|
||||
CI_BASE_URL
|
||||
BROWSER
|
||||
HEADLESS
|
||||
REPO
|
||||
CYPRESS_pushNotificationServer
|
||||
EOF
|
||||
# Adding service-specific cypress variables
|
||||
for SERVICE in $ENABLED_DOCKER_SERVICES; do
|
||||
case $SERVICE in
|
||||
openldap)
|
||||
echo "CYPRESS_ldapServer=localhost" >>.env.cypress
|
||||
echo "CYPRESS_runLDAPSync=true" >>.env.cypress
|
||||
;;
|
||||
minio)
|
||||
echo "CYPRESS_minioS3Endpoint=localhost:9000" >>.env.cypress
|
||||
;;
|
||||
keycloak)
|
||||
echo "CYPRESS_keycloakBaseUrl=http://localhost:8484" >>.env.cypress
|
||||
;;
|
||||
elasticsearch | opensearch)
|
||||
echo "CYPRESS_elasticsearchConnectionURL=http://localhost:9200" >>.env.cypress
|
||||
;;
|
||||
esac
|
||||
done
|
||||
# Adding SERVER-specific cypress variables
|
||||
case "$SERVER" in
|
||||
cloud)
|
||||
echo "CYPRESS_serverEdition=Cloud" >>.env.cypress
|
||||
echo "CYPRESS_cwsURL=${CWS_URL}" >>.env.cypress
|
||||
echo "CYPRESS_cwsAPIURL=${CWS_URL}" >>.env.cypress
|
||||
;;
|
||||
*)
|
||||
echo "CYPRESS_serverEdition=E20" >>.env.cypress
|
||||
;;
|
||||
esac
|
||||
# Add Automation Dashboard related variables to cypress container
|
||||
if [ -n "${AUTOMATION_DASHBOARD_URL:-}" ]; then
|
||||
mme2e_log "Automation dashboard URL is set: loading related variables into the Cypress container"
|
||||
mme2e_generate_envfile_from_var_names >>.env.cypress <<-EOF
|
||||
AUTOMATION_DASHBOARD_URL
|
||||
AUTOMATION_DASHBOARD_TOKEN
|
||||
EOF
|
||||
elif DC_COMMAND="$MME2E_DC_DASHBOARD" mme2e_wait_service_healthy dashboard 1; then
|
||||
mme2e_log "Detected a running automation dashboard: loading its access variables into the Cypress container"
|
||||
cat >>.env.cypress <.env.dashboard
|
||||
fi
|
||||
;;
|
||||
playwright)
|
||||
mme2e_log "Playwright: Generating .env.playwright"
|
||||
mme2e_generate_envfile_from_var_names >.env.playwright <<-EOF
|
||||
BRANCH
|
||||
BUILD_ID
|
||||
EOF
|
||||
;;
|
||||
none)
|
||||
mme2e_log "Requested TEST=$TEST. Skipping generation of test-specific env files."
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Perform SERVER-specific checks/customizations
|
||||
case "$SERVER" in
|
||||
cloud)
|
||||
if ! [ -f .env.cloud ]; then
|
||||
mme2e_log "Error: when using SERVER=$SERVER, the .env.cloud file is expected to exist, before generating the docker-compose file. Aborting." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# Perform TEST-specific checks/customizations
|
||||
case $TEST in
|
||||
cypress)
|
||||
enable_docker_service cypress
|
||||
enable_docker_service webhook-interactions
|
||||
;;
|
||||
playwright)
|
||||
enable_docker_service playwright
|
||||
enable_docker_service webhook-interactions
|
||||
;;
|
||||
esac
|
||||
|
||||
mme2e_log "Generating docker-compose file using the following parameters..."
|
||||
mme2e_log "TEST: ${TEST}"
|
||||
mme2e_log "SERVER: ${SERVER}"
|
||||
mme2e_log "ENABLED_DOCKER_SERVICES: ${ENABLED_DOCKER_SERVICES}"
|
||||
assert_docker_services_validity "$ENABLED_DOCKER_SERVICES"
|
||||
generate_docker_compose_file
|
||||
generate_env_files
|
||||
74
e2e-tests/.ci/server.prepare.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
set -e -u -o pipefail
|
||||
cd "$(dirname "$0")"
|
||||
. .e2erc
|
||||
|
||||
mme2e_log "Configuring starting server parameters that may be changed at runtime"
|
||||
for SETTING in \
|
||||
TeamSettings.EnableOpenServer=true \
|
||||
PluginSettings.Enable=true \
|
||||
PluginSettings.EnableUploads=true \
|
||||
PluginSettings.AutomaticPrepackagedPlugins=true; do
|
||||
mme2e_log "Configuring parameter: $SETTING"
|
||||
# shellcheck disable=SC2046
|
||||
${MME2E_DC_SERVER} exec -T -- server mmctl --local config set $(tr '=' ' ' <<<$SETTING)
|
||||
done
|
||||
if [ -n "${MM_LICENSE:-}" ]; then
|
||||
# We prefer uploading the license here, instead of setting the env var for the server
|
||||
# This is to retain the flexibility of being able to remove it programmatically, if the tests require it
|
||||
mme2e_log "Uploading license to server"
|
||||
${MME2E_DC_SERVER} exec -T -- server mmctl --local license upload-string "$MM_LICENSE"
|
||||
fi
|
||||
|
||||
# if [ "$TEST" = "cypress" ]; then
|
||||
# mme2e_log "Prepare Cypress: install dependencies"
|
||||
# # Create user in Cypress container and work around the node image's assumption that the app files are owned by user 1000
|
||||
# mme2e_log "Creating user for Cypress container"
|
||||
# ${MME2E_DC_SERVER} exec -T -u 0 -- cypress bash -c "id $MME2E_UID || useradd -u $MME2E_UID -m nodeci"
|
||||
# ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress npm i
|
||||
# ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress cypress install
|
||||
# mme2e_log "Prepare Cypress: populating fixtures"
|
||||
# ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress tee tests/fixtures/keycloak.crt >/dev/null <../../server/build/docker/keycloak/keycloak.crt
|
||||
# # Download plugins using wget in the Cypress container
|
||||
# for PLUGIN_URL in \
|
||||
# "https://github.com/mattermost/mattermost-plugin-gitlab/releases/download/v1.3.0/com.github.manland.mattermost-plugin-gitlab-1.3.0.tar.gz" \
|
||||
# "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.9.0/com.mattermost.demo-plugin-0.9.0.tar.gz" \
|
||||
# "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.8.0/com.mattermost.demo-plugin-0.8.0.tar.gz"; do
|
||||
# PLUGIN_NAME="${PLUGIN_URL##*/}"
|
||||
# PLUGIN_PATH="tests/fixtures/$PLUGIN_NAME"
|
||||
|
||||
# # Check if file exists
|
||||
# if ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress test -f "$PLUGIN_PATH"; then
|
||||
# mme2e_log "Skipping installation of plugin $PLUGIN_NAME: file exists"
|
||||
# continue
|
||||
# fi
|
||||
|
||||
# # Use wget in the Cypress container instead of curl in server (for distroless compatibility)
|
||||
# mme2e_log "Downloading $PLUGIN_NAME to fixtures directory"
|
||||
# ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress wget -q -O "/cypress/$PLUGIN_PATH" "$PLUGIN_URL"
|
||||
# done
|
||||
# fi
|
||||
|
||||
# Run service-specific initialization steps
|
||||
for SERVICE in $ENABLED_DOCKER_SERVICES; do
|
||||
case "$SERVICE" in
|
||||
openldap)
|
||||
LDIF_FILE=../../server/tests/test-data.ldif
|
||||
LDIF_CANARY=$(sed -n -E 's/^dn:[[:space:]]*(.*)$/\1/p' ${LDIF_FILE} | tail -n1)
|
||||
if ${MME2E_DC_SERVER} exec -T -- openldap bash -c "ldapsearch -x -D \"cn=admin,dc=mm,dc=test,dc=com\" -w mostest_password -b \"$LDIF_CANARY\" >/dev/null"; then
|
||||
mme2e_log "Skipping configuration for the $SERVICE container: already initialized"
|
||||
continue
|
||||
fi
|
||||
mme2e_log "Configuring the $SERVICE container"
|
||||
${MME2E_DC_SERVER} exec -T openldap bash -c 'ldapadd -Y EXTERNAL -H ldapi:/// -w mostest_password || true' <../../server/tests/custom-schema-objectID.ldif
|
||||
${MME2E_DC_SERVER} exec -T -- openldap bash -c 'ldapadd -Y EXTERNAL -H ldapi:/// -w mostest_password || true' <../../server/tests/custom-schema-cpa.ldif
|
||||
${MME2E_DC_SERVER} exec -T -- openldap bash -c 'ldapadd -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest_password' <../../server/tests/test-data.ldif
|
||||
;;
|
||||
minio)
|
||||
mme2e_log "Configuring the $SERVICE container"
|
||||
${MME2E_DC_SERVER} exec -T -- minio sh -c 'mkdir -p /data/mattermost-test'
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
mme2e_log "Mattermost is running and ready for E2E testing"
|
||||
75
e2e-tests/.ci/server.run_cypress.sh
Executable file
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
# shellcheck disable=SC2038
|
||||
set -e -u -o pipefail
|
||||
cd "$(dirname "$0")"
|
||||
. .e2erc
|
||||
|
||||
# Print run information
|
||||
mme2e_log "Printing Cypress container informations"
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress node -p 'module.paths'
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash <<"EOF"
|
||||
cat <<INNEREOF
|
||||
node version: $(node -v)
|
||||
npm version: $(npm -v)
|
||||
debian version: $(cat /etc/debian_version)
|
||||
user: $(whoami)
|
||||
chrome: $(google-chrome --version || true)
|
||||
firefox: $(firefox --version || true)
|
||||
INNEREOF
|
||||
EOF
|
||||
|
||||
mme2e_log "Prepare Cypress: ensure container user entry exists for UID $MME2E_UID"
|
||||
${MME2E_DC_SERVER} exec -T -- cypress bash <<EOF
|
||||
getent passwd $MME2E_UID >/dev/null 2>&1 || \
|
||||
useradd --uid $MME2E_UID --gid 0 --home /root --no-create-home --shell /bin/bash mme2euser 2>/dev/null || true
|
||||
EOF
|
||||
|
||||
mme2e_log "Prepare Cypress: install dependencies"
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash <<EOF
|
||||
npm install --cache /tmp/empty-cache
|
||||
EOF
|
||||
# cypress install verifies the Electron binary, which needs a display and dbus session.
|
||||
# Use xvfb-run and dbus-run-session for the headless virtual display and bus.
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash <<'EOF'
|
||||
set -euo pipefail
|
||||
Xvfb :99 -screen 0 1280x1024x24 -nolisten tcp &
|
||||
XVFB_PID=$!
|
||||
trap 'kill "$XVFB_PID" 2>/dev/null || true' EXIT
|
||||
sleep 1
|
||||
DISPLAY=:99 dbus-run-session -- cypress install
|
||||
EOF
|
||||
|
||||
# Initialize cypress report directory
|
||||
mme2e_log "Prepare Cypress: clean and initialize report and logs directory"
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash <<EOF
|
||||
rm -rf logs results
|
||||
mkdir -p logs
|
||||
mkdir -p results/junit
|
||||
touch results/junit/empty.xml
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>' > results/junit/empty.xml
|
||||
EOF
|
||||
|
||||
# Run cypress test
|
||||
# No need to collect its exit status: if it's nonzero, this script will terminate since we use '-e'
|
||||
LOGFILE_SUFFIX="${CI_BASE_URL//\//_}" # Remove slashes from CI_BASE_URL to produce a usable filename
|
||||
# shellcheck disable=SC2016
|
||||
if ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash -c '[ -n "${AUTOMATION_DASHBOARD_URL}" ]'; then
|
||||
mme2e_log "AUTOMATION_DASHBOARD_URL is set. Using run_test_cycle.js for the cypress run"
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress node run_test_cycle.js | tee "../cypress/logs/${LOGFILE_SUFFIX}_cypress.log"
|
||||
else
|
||||
mme2e_log "AUTOMATION_DASHBOARD_URL is unset. Using run_tests.js for the cypress run"
|
||||
# shellcheck disable=SC2086
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress node run_tests.js $TEST_FILTER | tee "../cypress/logs/${LOGFILE_SUFFIX}_cypress.log"
|
||||
fi
|
||||
|
||||
# Collect run results
|
||||
cat >../cypress/results/summary.json <<EOF
|
||||
{
|
||||
"passed": $(find ../cypress/results/mochawesome-report/json/tests/ -name '*.json' | xargs -l jq -r '.stats.passes' | jq -s add),
|
||||
"failed": $(find ../cypress/results/mochawesome-report/json/tests/ -name '*.json' | xargs -l jq -r '.stats.failures' | jq -s add),
|
||||
"failed_expected": 0
|
||||
}
|
||||
EOF
|
||||
|
||||
# Collect server logs
|
||||
${MME2E_DC_SERVER} logs --no-log-prefix -- server >"../cypress/logs/${LOGFILE_SUFFIX}_mattermost.log" 2>&1
|
||||
154
e2e-tests/.ci/server.run_playwright.sh
Executable file
@@ -0,0 +1,154 @@
|
||||
#!/bin/bash
|
||||
set -e -u -o pipefail
|
||||
cd "$(dirname "$0")"
|
||||
. .e2erc
|
||||
|
||||
mme2e_log "Prepare Playwright: clean and initialize report and logs directory"
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash <<EOF
|
||||
cd e2e-tests/playwright
|
||||
rm -rf logs results storage_state
|
||||
mkdir -p logs results
|
||||
touch logs/mattermost.log
|
||||
EOF
|
||||
|
||||
mme2e_log "Prepare Playwright: install dependencies"
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash <<EOF
|
||||
cd webapp/
|
||||
npm install --cache /tmp/empty-cache
|
||||
cd ../e2e-tests/playwright
|
||||
npm install --cache /tmp/empty-cache
|
||||
EOF
|
||||
|
||||
mme2e_log "Prepare Playwright: environment info"
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash <<"EOF"
|
||||
cat <<INNEREOF
|
||||
debian = $(cat /etc/debian_version)
|
||||
uname = $(uname -m)
|
||||
node = $(node -v)
|
||||
npm = $(npm -v)
|
||||
playwright = $(cd e2e-tests/playwright && npx playwright --version)
|
||||
browsers
|
||||
$(du -hs ~/ms-playwright/* | awk '{print " = " $2 " (" $1 ")"}')
|
||||
INNEREOF
|
||||
EOF
|
||||
|
||||
# Enable next line to debug Playwright
|
||||
# export DEBUG=pw:protocol,pw:browser,pw:api
|
||||
|
||||
mme2e_log "Start LibreTranslate mock server for autotranslation tests"
|
||||
${MME2E_DC_SERVER} exec -u "$MME2E_UID" -d -- playwright bash -c "cd e2e-tests/playwright && npm run start:libretranslate-mock" || true
|
||||
|
||||
mme2e_log "Wait for LibreTranslate mock server to be ready"
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash -c "for i in {1..30}; do curl -s http://localhost:3010/ && exit 0; sleep 1; done; echo 'Mock server failed to start'; exit 1" || true
|
||||
|
||||
# Pass TEST_FILTER and PW_SHARD with `docker compose exec -e VAR`
|
||||
# (no =value) so Compose copies them from this shell's environment.
|
||||
|
||||
mme2e_log "Playwright: running chrome project (sharded)"
|
||||
${MME2E_DC_SERVER} exec -i -T -u "$MME2E_UID" \
|
||||
-e TEST_FILTER \
|
||||
-e PW_SHARD \
|
||||
-- playwright bash -lc "cd e2e-tests/playwright && npm run test:ci -- \${TEST_FILTER:+\$TEST_FILTER} \${PW_SHARD:+\$PW_SHARD}" | tee ../playwright/logs/playwright-first.log || true
|
||||
|
||||
# IMPORTANT: Playwright's blob reporter WIPES its outputDir at the start
|
||||
# of every invocation. If we leave first-pass blobs inside
|
||||
# `results/blob-report/`, a follow-on `npm run test:ci` deletes them.
|
||||
STASH_DIR="e2e-tests/playwright/.blob-stash"
|
||||
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash -lc "
|
||||
rm -rf ${STASH_DIR}
|
||||
mkdir -p ${STASH_DIR}
|
||||
if compgen -G 'e2e-tests/playwright/results/blob-report/*.zip' >/dev/null 2>&1; then
|
||||
for f in e2e-tests/playwright/results/blob-report/*.zip; do
|
||||
mv \"\$f\" \"${STASH_DIR}/first-\$(basename \"\$f\")\"
|
||||
done
|
||||
fi
|
||||
" || true
|
||||
|
||||
RESULTS_FILE="../playwright/results/reporter/results.json"
|
||||
FAILED_SPECS=""
|
||||
if [ -f "$RESULTS_FILE" ]; then
|
||||
FAILED_SPECS=$(jq -r '
|
||||
[.suites[] | . as $top |
|
||||
(recurse(.suites[]?) | .specs[]? | .tests[]? |
|
||||
select((.results | length) > 0) |
|
||||
select((.results | last).status == "failed" or (.results | last).status == "timedOut") |
|
||||
(.location.file // $top.file))
|
||||
] | map(select(. != null)) | unique | join(",")
|
||||
' "$RESULTS_FILE" 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [ -n "$FAILED_SPECS" ]; then
|
||||
mme2e_log "Retrying failed specs on the same runner: $FAILED_SPECS"
|
||||
# Split the comma-separated list into an array and shell-quote every entry
|
||||
# with printf '%q' so that filenames containing shell metacharacters
|
||||
# (;, &&, $(), etc.) are treated as literals by the inner bash -lc shell
|
||||
# and cannot be used for command injection.
|
||||
IFS=',' read -ra _spec_array <<< "$FAILED_SPECS"
|
||||
SPEC_ARGS=$(printf '%q ' "${_spec_array[@]}")
|
||||
|
||||
${MME2E_DC_SERVER} exec -i -T -u "$MME2E_UID" \
|
||||
-e TEST_FILTER \
|
||||
-e PW_SHARD \
|
||||
-- playwright bash -lc "cd e2e-tests/playwright && npm run test:ci -- $SPEC_ARGS \${TEST_FILTER:+\$TEST_FILTER} \${PW_SHARD:+\$PW_SHARD}" | tee ../playwright/logs/playwright-retry.log || true
|
||||
|
||||
# Stash retry blobs alongside the first-pass blobs.
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash -lc "
|
||||
if compgen -G 'e2e-tests/playwright/results/blob-report/*.zip' >/dev/null 2>&1; then
|
||||
for f in e2e-tests/playwright/results/blob-report/*.zip; do
|
||||
mv \"\$f\" \"${STASH_DIR}/retry-\$(basename \"\$f\")\"
|
||||
done
|
||||
fi
|
||||
" || true
|
||||
fi
|
||||
|
||||
# Move all stashed blobs back into blob-report/ for final merge-reports
|
||||
# and for upload-artifact. This step runs whether or not retries ran:
|
||||
# if no retries, we just put the first-pass blobs back.
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash -lc "
|
||||
mkdir -p e2e-tests/playwright/results/blob-report
|
||||
if compgen -G '${STASH_DIR}/*.zip' >/dev/null 2>&1; then
|
||||
mv ${STASH_DIR}/*.zip e2e-tests/playwright/results/blob-report/
|
||||
fi
|
||||
rmdir ${STASH_DIR} 2>/dev/null || true
|
||||
" || true
|
||||
|
||||
# Merge the combined blob set into a single results.json so the per-shard
|
||||
# `summary.json` below reflects first-pass + retry outcomes. The cross-
|
||||
# shard merge in the CI template's `calculate-results` job will re-merge
|
||||
# all shards' blobs from the same `blob-report/` contents.
|
||||
if [ -n "$FAILED_SPECS" ]; then
|
||||
mme2e_log "Merging first-pass + retry blob reports"
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash -lc "
|
||||
cd e2e-tests/playwright
|
||||
rm -rf results/reporter
|
||||
mkdir -p results/reporter
|
||||
npx playwright merge-reports --config merge.config.mjs results/blob-report
|
||||
" || true
|
||||
else
|
||||
mme2e_log "No failed specs in shard; skipping inline retry"
|
||||
fi
|
||||
|
||||
# Keep a combined tail log for backwards-compat with anything grepping
|
||||
# playwright.log. The authoritative results are the merged blob reports.
|
||||
cat ../playwright/logs/playwright-first.log \
|
||||
../playwright/logs/playwright-retry.log \
|
||||
>../playwright/logs/playwright.log 2>/dev/null || true
|
||||
|
||||
# Collect run results from the merged results.json. This summary is used
|
||||
# only by local dev / the cypress-oriented template; the playwright CI
|
||||
# template computes authoritative totals from the merged blob reports it
|
||||
# downloads across all shards.
|
||||
# Documentation: https://playwright.dev/docs/api/class-testcase#test-case-expected-status
|
||||
if [ -f ../playwright/results/reporter/results.json ]; then
|
||||
jq -f /dev/stdin ../playwright/results/reporter/results.json >../playwright/results/summary.json <<EOF
|
||||
{
|
||||
passed: .stats.expected,
|
||||
failed: .stats.unexpected,
|
||||
failed_expected: (.stats.skipped + .stats.flaky)
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Collect server logs
|
||||
${MME2E_DC_SERVER} logs --no-log-prefix -- server >../playwright/logs/mattermost.log 2>&1
|
||||
113
e2e-tests/.ci/server.run_specs.sh
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/bin/bash
|
||||
# shellcheck disable=SC2038
|
||||
# Run specific spec files
|
||||
# Usage: SPEC_FILES="path/to/spec1.ts,path/to/spec2.ts" make start-server run-specs
|
||||
|
||||
set -e -u -o pipefail
|
||||
cd "$(dirname "$0")"
|
||||
. .e2erc
|
||||
|
||||
if [ -z "${SPEC_FILES:-}" ]; then
|
||||
mme2e_log "Error: SPEC_FILES environment variable is required"
|
||||
mme2e_log "Usage: SPEC_FILES=\"path/to/spec.ts\" make start-server run-specs"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mme2e_log "Running spec files: $SPEC_FILES"
|
||||
|
||||
case $TEST in
|
||||
cypress)
|
||||
mme2e_log "Running Cypress with specified specs"
|
||||
# Initialize cypress report directory
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash <<EOF
|
||||
rm -rf logs results
|
||||
mkdir -p logs
|
||||
mkdir -p results/junit
|
||||
mkdir -p results/mochawesome-report/json/tests
|
||||
touch results/junit/empty.xml
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>' > results/junit/empty.xml
|
||||
EOF
|
||||
|
||||
# Install dependencies
|
||||
mme2e_log "Prepare Cypress: install dependencies"
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash <<EOF
|
||||
cd webapp/
|
||||
npm install --cache /tmp/empty-cache
|
||||
cd ../e2e-tests/cypress
|
||||
npm install --cache /tmp/empty-cache
|
||||
EOF
|
||||
|
||||
# Run cypress with specific spec files and mochawesome reporter
|
||||
LOGFILE_SUFFIX="${CI_BASE_URL//\//_}_specs"
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress npx cypress run \
|
||||
--spec "$SPEC_FILES" \
|
||||
--reporter cypress-multi-reporters \
|
||||
--reporter-options configFile=reporter-config.json \
|
||||
| tee "../cypress/logs/${LOGFILE_SUFFIX}_cypress.log" || true
|
||||
|
||||
# Collect run results
|
||||
if [ -d ../cypress/results/mochawesome-report/json/tests/ ]; then
|
||||
cat >../cypress/results/summary.json <<EOF
|
||||
{
|
||||
"passed": $(find ../cypress/results/mochawesome-report/json/tests/ -name '*.json' | xargs -l jq -r '.stats.passes' | jq -s add),
|
||||
"failed": $(find ../cypress/results/mochawesome-report/json/tests/ -name '*.json' | xargs -l jq -r '.stats.failures' | jq -s add),
|
||||
"failed_expected": 0
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Collect server logs
|
||||
${MME2E_DC_SERVER} logs --no-log-prefix -- server >"../cypress/logs/${LOGFILE_SUFFIX}_mattermost.log" 2>&1
|
||||
;;
|
||||
playwright)
|
||||
mme2e_log "Running Playwright with specified specs"
|
||||
# Convert comma-separated to space-separated for playwright
|
||||
SPEC_ARGS=$(echo "$SPEC_FILES" | tr ',' ' ')
|
||||
|
||||
# Initialize playwright report and logs directory
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash <<EOF
|
||||
cd e2e-tests/playwright
|
||||
rm -rf logs results storage_state
|
||||
mkdir -p logs results
|
||||
touch logs/mattermost.log
|
||||
EOF
|
||||
|
||||
# Install dependencies
|
||||
mme2e_log "Prepare Playwright: install dependencies"
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- playwright bash <<EOF
|
||||
cd webapp/
|
||||
npm install --cache /tmp/empty-cache
|
||||
cd ../e2e-tests/playwright
|
||||
npm install --cache /tmp/empty-cache
|
||||
EOF
|
||||
|
||||
# Run playwright with specific spec files
|
||||
LOGFILE_SUFFIX="${CI_BASE_URL//\//_}_specs"
|
||||
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" \
|
||||
-e TEST_FILTER \
|
||||
-e PW_SHARD \
|
||||
-- playwright bash -lc "cd e2e-tests/playwright && npm run test:ci -- $SPEC_ARGS \${TEST_FILTER:+\$TEST_FILTER} \${PW_SHARD:+\$PW_SHARD}" | tee "../playwright/logs/${LOGFILE_SUFFIX}_playwright.log" || true
|
||||
|
||||
# Collect run results (if results.json exists)
|
||||
if [ -f ../playwright/results/reporter/results.json ]; then
|
||||
jq -f /dev/stdin ../playwright/results/reporter/results.json >../playwright/results/summary.json <<EOF
|
||||
{
|
||||
passed: .stats.expected,
|
||||
failed: .stats.unexpected,
|
||||
failed_expected: (.stats.skipped + .stats.flaky)
|
||||
}
|
||||
EOF
|
||||
mme2e_log "Results file found and summary generated"
|
||||
fi
|
||||
|
||||
# Collect server logs
|
||||
${MME2E_DC_SERVER} logs --no-log-prefix -- server >"../playwright/logs/${LOGFILE_SUFFIX}_mattermost.log" 2>&1
|
||||
;;
|
||||
*)
|
||||
mme2e_log "Error, unsupported value for TEST: $TEST" >&2
|
||||
mme2e_log "Aborting" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mme2e_log "Spec run complete"
|
||||
22
e2e-tests/.ci/server.run_test.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
set -e -u -o pipefail
|
||||
cd "$(dirname "$0")"
|
||||
. .e2erc
|
||||
|
||||
case $TEST in
|
||||
cypress)
|
||||
./server.run_cypress.sh
|
||||
;;
|
||||
playwright)
|
||||
./server.run_playwright.sh
|
||||
;;
|
||||
none)
|
||||
mme2e_log "Running with TEST=$TEST. No tests to run."
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
mme2e_log "Error, unsupported value for TEST: $TEST" >&2
|
||||
mme2e_log "Aborting" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
123
e2e-tests/.ci/server.start.sh
Executable file
@@ -0,0 +1,123 @@
|
||||
#!/bin/bash
|
||||
set -e -u -o pipefail
|
||||
cd "$(dirname "$0")"
|
||||
. .e2erc
|
||||
|
||||
# Wait for the required server image
|
||||
mme2e_log "Waiting for server image to be available"
|
||||
mme2e_wait_image "$SERVER_IMAGE" 4 30
|
||||
|
||||
# Launch mattermost-server, and wait for it to be healthy
|
||||
mme2e_log "Starting E2E containers"
|
||||
${MME2E_DC_SERVER} create
|
||||
|
||||
# `docker compose up -d` returns non-zero the moment any depended container
|
||||
# exits during startup, which masks openldap's own `restart: always` policy.
|
||||
# On a small fraction of ubuntu-24.04 runners the osixia/openldap:1.4.0 image
|
||||
# exits 1 on first boot (suspected init-script race under runner load). Retry
|
||||
# the `up` a bounded number of times, force-recreating openldap between tries
|
||||
# so its first-boot bootstrap re-runs cleanly, and dump rich diagnostics on
|
||||
# every failure so future CI failures contain the actual data we need to
|
||||
# permanently root-cause this. The diagnostics directory is also uploaded as
|
||||
# a workflow artifact (see e2e-tests-*-template.yml `ci/upload-docker-diagnostics`).
|
||||
DIAG_DIR="${PWD}/../docker-diagnostics"
|
||||
mkdir -p "$DIAG_DIR"
|
||||
|
||||
dump_openldap_diagnostics() {
|
||||
local label="$1"
|
||||
local out="$DIAG_DIR/${label}"
|
||||
mkdir -p "$out"
|
||||
mme2e_log "[diagnostics:${label}] capturing openldap state to $out"
|
||||
|
||||
# Container-level state (exit code, OOMKilled, error string, restart count)
|
||||
docker inspect mmserver-openldap-1 >"$out/openldap.inspect.json" 2>&1 || true
|
||||
${MME2E_DC_SERVER} ps -a >"$out/compose.ps.txt" 2>&1 || true
|
||||
${MME2E_DC_SERVER} logs --no-log-prefix -- openldap >"$out/openldap.log" 2>&1 || true
|
||||
|
||||
# Merged compose config — confirms which security_opt / cap_add / image is actually applied
|
||||
${MME2E_DC_SERVER} config >"$out/compose.config.yml" 2>&1 || true
|
||||
|
||||
# Host-level state useful for OOM / AppArmor diagnosis
|
||||
uname -a >"$out/host.uname.txt" 2>&1 || true
|
||||
free -m >"$out/host.free.txt" 2>&1 || true
|
||||
df -h >"$out/host.df.txt" 2>&1 || true
|
||||
docker version >"$out/docker.version.txt" 2>&1 || true
|
||||
docker info >"$out/docker.info.txt" 2>&1 || true
|
||||
docker compose version >"$out/compose.version.txt" 2>&1 || true
|
||||
cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns >"$out/host.apparmor_userns.txt" 2>&1 || true
|
||||
# AppArmor denials and OOM kills land in dmesg — grep them out (needs sudo on GH runners).
|
||||
sudo dmesg | tail -200 >"$out/host.dmesg.tail.txt" 2>&1 || true
|
||||
sudo dmesg | grep -iE 'apparmor|denied|oom|killed|openldap|slapd' >"$out/host.dmesg.relevant.txt" 2>&1 || true
|
||||
|
||||
# Echo the most useful slice straight to the workflow log so it shows up
|
||||
# in the GH Actions UI without needing to download the artifact.
|
||||
mme2e_log "----- openldap inspect (exit/oom/error) -----"
|
||||
docker inspect mmserver-openldap-1 \
|
||||
--format 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}} Restarts={{.RestartCount}} Status={{.State.Status}}' \
|
||||
2>&1 || true
|
||||
mme2e_log "----- openldap log (last 100) -----"
|
||||
${MME2E_DC_SERVER} logs --no-log-prefix --tail=100 -- openldap 2>&1 || true
|
||||
mme2e_log "----- relevant dmesg -----"
|
||||
sudo dmesg | grep -iE 'apparmor|denied|oom|killed|openldap|slapd' | tail -40 2>&1 || true
|
||||
mme2e_log "----- end diagnostics:${label} -----"
|
||||
}
|
||||
|
||||
UP_ATTEMPTS=3
|
||||
for attempt in $(seq 1 $UP_ATTEMPTS); do
|
||||
if ${MME2E_DC_SERVER} up -d --remove-orphans; then
|
||||
break
|
||||
fi
|
||||
dump_openldap_diagnostics "up-attempt-${attempt}"
|
||||
if [ "$attempt" -eq "$UP_ATTEMPTS" ]; then
|
||||
mme2e_log "compose up failed after ${UP_ATTEMPTS} attempts; aborting"
|
||||
exit 1
|
||||
fi
|
||||
# Force-recreate openldap so its first-boot init re-runs from a clean state
|
||||
${MME2E_DC_SERVER} rm -fsv openldap || true
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# Postgres check
|
||||
if ! mme2e_wait_command_success "${MME2E_DC_SERVER} exec -T -- postgres pg_isready -h localhost" "Waiting for postgres to accept connections" "30" "5"; then
|
||||
mme2e_log "Postgres not accepting connections"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! mme2e_wait_service_healthy server 60 10; then
|
||||
mme2e_log "Mattermost container not healthy, retry attempts exhausted. Giving up." >&2
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC2043
|
||||
for MIGRATION in migration_advanced_permissions_phase_2; do
|
||||
# Query explanation: if it doesn't find the migration in the table, there are 0 results and the command fails with a divide-by-zero error. Otherwise the command succeeds
|
||||
MIGRATION_CHECK_COMMAND="${MME2E_DC_SERVER} exec -T postgres sh -c 'PGPASSWORD=mostest_password psql -U mmuser mattermost_test -c \"select 1 / (select count(*) from Systems where name = '\''${MIGRATION}'\'' and value = '\''true'\'');\"'"
|
||||
if ! mme2e_wait_command_success "$MIGRATION_CHECK_COMMAND" "Waiting for migration to be completed: ${MIGRATION}" "10" "10"; then
|
||||
mme2e_log "Migration ${MIGRATION} not completed, retry attempts exhausted. Giving up." >&2
|
||||
|
||||
LOG_DIR="../${TEST}/logs"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# Save server logs to a file
|
||||
${MME2E_DC_SERVER} logs --no-log-prefix -- server >"$LOG_DIR/server.log"
|
||||
mme2e_log "Server logs saved to server.log"
|
||||
|
||||
# Save postgres logs to a file
|
||||
${MME2E_DC_SERVER} logs --no-log-prefix -- postgres >"$LOG_DIR/postgres.log"
|
||||
mme2e_log "Postgres logs saved to postgres.log"
|
||||
|
||||
exit 2
|
||||
fi
|
||||
mme2e_log "${MIGRATION}: completed."
|
||||
done
|
||||
mme2e_log "Mattermost container is running and healthy"
|
||||
|
||||
# Wait for webhook-interactions container if running cypress or playwright tests
|
||||
if [ "$TEST" = "cypress" ] || [ "$TEST" = "playwright" ]; then
|
||||
mme2e_log "Checking webhook-interactions container health"
|
||||
${MME2E_DC_SERVER} logs --no-log-prefix -- webhook-interactions 2>&1 | tail -5
|
||||
if ! mme2e_wait_service_healthy webhook-interactions 2 10; then
|
||||
mme2e_log "Webhook interactions container not healthy, retry attempts exhausted. Giving up." >&2
|
||||
exit 1
|
||||
fi
|
||||
mme2e_log "Webhook interactions container is running and healthy"
|
||||
fi
|
||||
7
e2e-tests/.ci/server.stop.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -e -u -o pipefail
|
||||
cd "$(dirname "$0")"
|
||||
. .e2erc
|
||||
|
||||
mme2e_log "Stopping E2E containers"
|
||||
${MME2E_DC_SERVER} down -v --remove-orphans
|
||||
14
e2e-tests/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# env, cert, key, license
|
||||
.env*
|
||||
*.crt
|
||||
*.key
|
||||
*.license
|
||||
|
||||
# Plugin
|
||||
*.tar.gz
|
||||
|
||||
# node
|
||||
*.lock
|
||||
|
||||
# build
|
||||
*.tsbuildinfo
|
||||
56
e2e-tests/Makefile
Normal file
@@ -0,0 +1,56 @@
|
||||
SHELL := /bin/bash
|
||||
|
||||
.PHONY: all run stop clean
|
||||
all: run
|
||||
run: generate-server start-server run-test
|
||||
stop: stop-server stop-dashboard clean
|
||||
report: collect-report-data publish-report
|
||||
clean:
|
||||
rm -fv .ci/server.yml
|
||||
rm -fv .ci/.env.{server,dashboard,cypress,playwright}
|
||||
|
||||
.PHONY: generate-server start-server run-test run-specs stop-server restart-server
|
||||
generate-server:
|
||||
bash ./.ci/server.generate.sh
|
||||
start-server: generate-server
|
||||
bash ./.ci/server.start.sh
|
||||
bash ./.ci/server.prepare.sh
|
||||
run-test:
|
||||
bash ./.ci/server.run_test.sh
|
||||
run-specs:
|
||||
bash ./.ci/server.run_specs.sh
|
||||
stop-server: generate-server
|
||||
bash ./.ci/server.stop.sh
|
||||
restart-server: stop-server start-server
|
||||
|
||||
.PHONY: start-dashboard generate-test-cycle stop-dashboard
|
||||
start-dashboard:
|
||||
bash ./.ci/dashboard.start.sh
|
||||
generate-test-cycle: requirecmd-node generate-server
|
||||
bash ./.ci/dashboard.generate_test_cycle.sh
|
||||
stop-dashboard:
|
||||
bash ./.ci/dashboard.stop.sh
|
||||
|
||||
.PHONY: cloud-init cloud-teardown
|
||||
cloud-init: requirecmd-jq requirecmd-curl
|
||||
bash ./.ci/server.cloud_init.sh
|
||||
cloud-teardown:
|
||||
bash ./.ci/server.cloud_teardown.sh
|
||||
|
||||
.PHONY: collect-report-data publish-report
|
||||
collect-report-data:
|
||||
bash ./.ci/report.collect.sh
|
||||
publish-report: requirecmd-node
|
||||
bash ./.ci/report.publish.sh
|
||||
|
||||
.PHONY: check-shell fmt-shell fmt-node fmt
|
||||
requirecmd-%:
|
||||
@which "$(*)" >/dev/null || { echo "Error, missing required CLI tool: $(*). Aborting." >&2; exit 1; }
|
||||
check-shell: requirecmd-shellcheck
|
||||
shellcheck ./.ci/*.sh ./.ci/.e2erc* # Install with https://webinstall.dev/shfmt/
|
||||
fmt-shell: requirecmd-shfmt
|
||||
shfmt -w -s -i 2 ./.ci/*.sh # Install with https://webinstall.dev/shellcheck/
|
||||
fmt-node: requirecmd-npx
|
||||
# Formats yaml files
|
||||
npx prettier ./.ci "!./.ci/dashboard" --write --cache
|
||||
fmt: fmt-node fmt-shell
|
||||
76
e2e-tests/README.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# E2E testing for the Mattermost web client
|
||||
|
||||
This directory contains the E2E testing code for the Mattermost web client.
|
||||
|
||||
### How to run locally
|
||||
|
||||
##### For test case development
|
||||
|
||||
Please refer to the [dedicated developer documentation](https://developers.mattermost.com/contribute/more-info/webapp/e2e-testing/) for instructions.
|
||||
|
||||
> **Playwright note:** the instructions below describe the Docker Compose flow Cypress uses (and that Playwright previously used too). Playwright's CI and recommended local setup have since moved to [Testcontainers](https://node.testcontainers.org/) — see `playwright/README.md`'s Server Setup section for details. This Compose flow remains the way to run Cypress, and to run a plain server instance (`TEST=none make`).
|
||||
|
||||
##### For pipeline debugging
|
||||
|
||||
The E2E testing pipeline's scripts depend on the following tools being installed on your system: `docker`, `docker-compose`, `make`, `git`, `jq`, `node`, and some common utilities (`coreutils`, `findutils`, `bash`, `awk`, `sed`, `grep`)
|
||||
|
||||
Instructions, tl;dr: create a local branch with your E2E test changes, then open a PR to the `mattermost-server` repo targeting the `master` branch (so that CI will produce the image that docker-compose needs), then run `make` in this directory.
|
||||
|
||||
Instructions, detailed:
|
||||
1. (optional, undefined variables are set to sane defaults) Create the `.ci/env` file, and populate it with the variables you need out of the following list:
|
||||
* `SERVER`: either `onprem` (default) or `cloud`.
|
||||
* `CWS_URL` (mandatory when `SERVER=cloud`, only used in such case): when spinning up a cloud-like test server that communicates with a test instance of a customer web server.
|
||||
* `TEST`: either `cypress` (default), `playwright`, or `none` (to avoid creating the cypress/playwright sidecar containers, e.g. if you only want to launch a server instance)
|
||||
* `ENABLED_DOCKER_SERVICES`: a space-separated list of services to start alongside the server. Default to `postgres inbucket`, for smoke test purposes and for lightweight and faster start-up time. Depending on the test requirement being worked on, you may want to override as needed, as such:
|
||||
- Cypress full tests require all services to be running: `postgres inbucket minio openldap elasticsearch keycloak`.
|
||||
- Cypress smoke tests require only the following: `postgres inbucket`.
|
||||
- Playwright no longer runs against this Compose flow in CI (see the note above) — this only matters if you're using it to spin up a plain server instance (`TEST=none make`) for Playwright's `external` mode.
|
||||
* The following variables, will be passed over to the server container: `MM_LICENSE` (no enterprise features will be available if this is unset; required when `SERVER=cloud`), and the exploded `MM_ENV` (a comma-separated list of env var specifications)
|
||||
* The following variables, which will be passed over to the cypress container: `BRANCH`, `BUILD_ID`, `CI_BASE_URL`, `BROWSER`, `AUTOMATION_DASHBOARD_URL` and `AUTOMATION_DASHBOARD_TOKEN`
|
||||
* The `SERVER_IMAGE` variable can also be set if you want to select a custom mattermost-server image. If not specified, the value of the `SERVER_IMAGE_DEFAULT` variable defined in file `.ci/.e2erc` is used.
|
||||
* The `TEST_FILTER` variable can also be set, to customize which tests you want Cypress/Playwright to run. If not specified, only the smoke tests will run
|
||||
- Its format depends on which tool is used: for Cypress, please check the `e2e-tests/cypress/run_tests.js` file for details. For Playwright, it can simply be populated with arguments you want to give to the `playwright test` command.
|
||||
* More variables may be required to configure reporting and cloud interactions. Check the content of the `.ci/report.*.sh` and `.ci/server.cloud_*.sh` scripts for reference.
|
||||
2. (optional) `make start-dashboard && make generate-test-cycle`: start the automation dashboard in the background, and initiate a test cycle on it, for the given `BUILD_ID`
|
||||
* NB: the `BUILD_ID` value should stay the same across the `make generate-test-cycle` command, and the subsequent `make` (see next step). If you need to initiate a new test cycle on the same dashboard, you'll need to change the `BUILD_ID` value and rerun both `make generate-test-cycle` and `make`.
|
||||
* Note that part of the dashboard functionality assumes the `BUILD_ID` to have a certain format (see [here](https://github.com/saturninoabril/automation-dashboard/blob/175891781bf1072c162c58c6ec0abfc5bcb3520e/lib/common_utils.ts#L3-L23) for details). This is not relevant for local running, but it's important to note in the testing pipelines.
|
||||
* This also automatically sets the `AUTOMATION_DASHBOARD_URL` and `AUTOMATION_DASHBOARD_TOKEN` variables for the cypress container
|
||||
* Note that if you run the dashboard locally, but also specify other `AUTOMATION_DASHBOARD_*` variables in your `.ci/env` file, the latter variables will take precedence.
|
||||
* The dashboard is used for orchestrating specs with parallel test runs and is typically used in CI.
|
||||
* Only Cypress is currently using the dashboard; Playwright is not.
|
||||
3. `make`: start and prepare the server, then run the Cypress smoke tests
|
||||
* You can track the progress of the run in the `http://localhost:4000/cycles` dashboard if you launched it locally
|
||||
* For `SERVER=cloud` runs, you'll need to first create a cloud customer against the specified `CWS_URL` service by running `make cloud-init`. The user isn't automatically removed, and may be reused across multiple runs until you run `make cloud-teardown` to delete it.
|
||||
* If you want to run the Playwright tests instead of the Cypress ones, you can run `TEST=playwright make` — though `playwright/README.md`'s Testcontainers option is now the recommended way to run Playwright locally/in CI
|
||||
* If you just want to run a local server instance, without any further testing, you can run `TEST=none make`
|
||||
* If you're using the automation dashboard, you have the option of sharding the E2E test run: you can launch the `make` command in parallel on different machines (NB: you must use the same `BUILD_ID` and `BRANCH` values that you used for `make generate-test-cycle`) to distribute running the test cases across them. When doing this, you should also set on each machine the `CI_BASE_URL` variable to a value that uniquely identifies the instance where `make` is running.
|
||||
* This script will also parse the local test results, and write a `e2e-tests/${TEST}/results/summary.json` file containing the following keys: `passed`, `failed` and `failed_expected` (the total number of testcases that were run is the sum of these three numbers)
|
||||
4. `make stop`: tears down the server (and the dashboard, if running)
|
||||
* This will stop and cleanup all of the E2E testing containers, including the database and its persistent volume.
|
||||
* This also implicitly runs `make clean`, which also removes any generated environment or docker-compose files.
|
||||
|
||||
Notes:
|
||||
- Setting a variable in `.ci/env` is functionally equivalent to exporting variables in your current shell's environment, before invoking the makefile.
|
||||
- The `.ci/.env.*` files are auto-generated by the pipeline scripts and aren't meant to be modified manually. The only file you should edit to control the containers' environment is `.ci/env`, as specified in the instructions above.
|
||||
- All of the variables in `.ci/env` must be set before the `make generate-server` command is run (or, if using the dashboard, before the `make generate-test-cycle` command). Modifying that file afterward has no effect because the containers' env files are generated in that step.
|
||||
- If you restart the dashboard at any point, you must also restart the server containers, so that it picks up the new IP of the dashboard from the newly generated `.env.dashboard` file
|
||||
- If new variables need to be passed to any of the containers, here are the general principles to follow when deciding where to populate it:
|
||||
* If their value is fixed (e.g. a static server configuration), these may be simply added to the `docker_compose_generator.sh` file, to the appropriate container.
|
||||
* If you need to introduce variables that you want to control from `.ci/env`: you need to update the scripts under the `.ci/` dir and configure them to write the new variables' values over to the appropriate `.env.*` file. In particular, avoid defining variables that depend on other variables within the docker-compose override files: this is to ensure uniformity in their availability and simplifies the question of what container has access to which variable considerably.
|
||||
* Exceptions are of course accepted wherever it makes sense (e.g. if you need to group variables based on some common functionality)
|
||||
- The `report` Make target is meant for internal usage. Usage and variables are documented in the respective scripts.
|
||||
- `make start-server` won't cleanup containers that don't change across runs. This means that you can use it to emulate a Mattermost server upgrade while retaining your database data by simply changing the `SERVER_IMAGE` variable on your machine, and then re-running `make start-server`. But this also means that if you want to run a clean local environment, you may have to manually run `make stop` to cleanup any running containers and their volumes, which include e.g. the database.
|
||||
|
||||
##### For code changes:
|
||||
* `make fmt-ci` to format and check yaml files and shell scripts.
|
||||
|
||||
##### For test stressing an E2E testcase
|
||||
|
||||
For Cypress:
|
||||
1. Enter the `cypress/` subdirectory
|
||||
2. Identify which test files you want to run, and how many times each. For instance: suppose you want to run `create_a_team_spec.js` and `demoted_user_spec.js` (which you can locate with the `find` command, under `cypress/tests/`), each run 3 times
|
||||
3. Run the chosen testcases the desired amount of times: `node run_tests.js --include-file=create_a_team_spec.js,demoted_user_spec.js --invert --stress-test-count=3`
|
||||
* Your system needs to be setup for Cypress usage, to be able to run this command. Refer to the [E2E testing developer documentation](https://developers.mattermost.com/contribute/more-info/webapp/e2e-testing/) for this.
|
||||
4. The `cypress/results/testPasses.json` file will count, for each of the testfiles, how many times it was run, and how many times each of the testcases contained in it passed. If the attempts and passes numbers do not match, that specific testcase may be flaky.
|
||||
|
||||
For Playwright: not currently supported — Playwright tests are run and re-run individually via `npm run test -- <spec>`, see `playwright/README.md`.
|
||||
13
e2e-tests/cypress/Dockerfile.webhook
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM node:14-alpine
|
||||
|
||||
RUN apk update && apk upgrade && \
|
||||
apk add --no-cache bash git openssh
|
||||
|
||||
WORKDIR /usr/src
|
||||
RUN npm install axios express client-oauth2@larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49
|
||||
COPY ./tests/plugins/post_message_as.js /usr/src/tests/plugins/post_message_as.js
|
||||
COPY ./utils/webhook_utils.js /usr/src/utils/webhook_utils.js
|
||||
COPY ./webhook_serve.js /usr/src
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "node", "webhook_serve.js" ]
|
||||
132
e2e-tests/cypress/README-Subpath.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Testing with subpath servers
|
||||
Some tests need multiple servers running in subpath mode. These tests have the cypress `Group: @subpath` metadata near the top of the test file. Instructions on running a server under subpath can be found here: [https://developers.mattermost.com/blog/subpath/](https://developers.mattermost.com/blog/subpath/)
|
||||
|
||||
In the `cypress.json` configuration file, the `baseURL` setting will need to be updated with the subpath URL of the first server, and the `secondServerURL` setting with the subpath URL of the second server.
|
||||
|
||||
### Running subpath tests on local machine
|
||||
Two mattermost servers running on the same machine must be served from different ports. To have the servers respond on the same URL and the same port under different subpaths, you will need to use a reverse proxy (nginx or apache) to proxy the same local url to both mattermost servers under different subpaths.
|
||||
|
||||
#### Example set up using NGINX:
|
||||
|
||||
You'll need to run two Mattermost servers.
|
||||
|
||||
1. Set the `SiteURL` and the listening port for the first server:
|
||||
|
||||
```
|
||||
"SiteURL": "http://localhost/company/mattermost1"
|
||||
"ListenAddress": ":8065",
|
||||
```
|
||||
|
||||
2. Set the `SiteURL` and the listening port for the second server:
|
||||
|
||||
```
|
||||
"SiteURL": "http://localhost/company/mattermost2"
|
||||
"ListenAddress": ":8066",
|
||||
```
|
||||
|
||||
The DB `DataSource` will need to be different for both servers.
|
||||
|
||||
3. Install NGINX - exact steps depend on your OS
|
||||
|
||||
4. Update your NGINX site configuration. The specific details for each setting can be found in the [Mattermost docs](https://docs.mattermost.com/install/config-proxy-nginx.html)
|
||||
|
||||
```
|
||||
upstream backend1 {
|
||||
server localhost:8065;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
upstream backend2 {
|
||||
server localhost:8066;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
|
||||
location ~ /company/mattermost1/api/v[0-9]+/(users/)?websocket$ {
|
||||
client_body_timeout 60;
|
||||
client_max_body_size 50M;
|
||||
lingering_timeout 5;
|
||||
proxy_buffer_size 16k;
|
||||
proxy_buffers 256 16k;
|
||||
proxy_connect_timeout 90;
|
||||
proxy_pass http://backend1;
|
||||
proxy_read_timeout 90s;
|
||||
proxy_send_timeout 300;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Frame-Options SAMEORIGIN;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
send_timeout 300;
|
||||
}
|
||||
|
||||
location /company/mattermost1 {
|
||||
client_max_body_size 50M;
|
||||
proxy_buffer_size 16k;
|
||||
proxy_buffers 256 16k;
|
||||
proxy_cache_lock on;
|
||||
proxy_cache_min_uses 2;
|
||||
proxy_cache_revalidate on;
|
||||
proxy_cache_use_stale timeout;
|
||||
proxy_http_version 1.1;
|
||||
proxy_pass http://backend1;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Frame-Options SAMEORIGIN;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location ~ /company/mattermost2/api/v[0-9]+/(users/)?websocket$ {
|
||||
client_body_timeout 60;
|
||||
client_max_body_size 50M;
|
||||
lingering_timeout 5;
|
||||
proxy_buffer_size 16k;
|
||||
proxy_buffers 256 16k;
|
||||
proxy_connect_timeout 90;
|
||||
proxy_pass http://backend2;
|
||||
proxy_read_timeout 90s;
|
||||
proxy_send_timeout 300;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Frame-Options SAMEORIGIN;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
send_timeout 300;
|
||||
}
|
||||
|
||||
location /company/mattermost2 {
|
||||
proxy_buffer_size 16k;
|
||||
proxy_buffers 256 16k;
|
||||
proxy_cache_lock on;
|
||||
proxy_cache_min_uses 2;
|
||||
proxy_cache_revalidate on;
|
||||
proxy_cache_use_stale timeout;
|
||||
proxy_http_version 1.1;
|
||||
proxy_pass http://backend2;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Frame-Options SAMEORIGIN;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
client_max_body_size 50M;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. Restart NGINX to reload the configuration. Exact steps depend on your OS/distribution. On most Linux distributions you can run `sudo systemctl restart nginx`
|
||||
|
||||
6. In the `cypress.json` file, set `baseURL` to `"http://localhost/company/mattermost1"` and `secondServerURL` to `"http://localhost/company/mattermost2"`
|
||||
|
||||
7. Start both Mattermost tests and run the e2e tests.
|
||||
127
e2e-tests/cypress/cypress.config.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as path from 'path';
|
||||
|
||||
import {defineConfig} from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
chromeWebSecurity: false,
|
||||
defaultCommandTimeout: 30000,
|
||||
downloadsFolder: 'tests/downloads',
|
||||
fixturesFolder: 'tests/fixtures',
|
||||
numTestsKeptInMemory: 0,
|
||||
retries: {
|
||||
|
||||
// retries for cypress:run
|
||||
runMode: 1,
|
||||
|
||||
// No retries for cypress:open
|
||||
openMode: 0,
|
||||
},
|
||||
screenshotsFolder: 'tests/screenshots',
|
||||
taskTimeout: 60000,
|
||||
video: true,
|
||||
videoCompression: true,
|
||||
videosFolder: 'tests/videos',
|
||||
viewportWidth: 1300,
|
||||
allowCypressEnv: false,
|
||||
expose: {
|
||||
adminEmail: process.env.CYPRESS_adminEmail || 'sysadmin@sample.mattermost.com',
|
||||
adminUsername: process.env.CYPRESS_adminUsername || 'sysadmin',
|
||||
adminPassword: process.env.CYPRESS_adminPassword || 'Sys@dmin-sample1',
|
||||
allowedUntrustedInternalConnections: 'localhost',
|
||||
cwsURL: 'http://localhost:8076',
|
||||
cwsAPIURL: 'http://localhost:8076',
|
||||
dbClient: 'postgres',
|
||||
dbConnection: process.env.CYPRESS_dbConnection || 'postgres://mmuser:mostest_password@localhost/mattermost_test?sslmode=disable&connect_timeout=10',
|
||||
elasticsearchConnectionURL: 'http://localhost:9200',
|
||||
firstTest: false,
|
||||
keycloakAppName: 'mattermost',
|
||||
keycloakBaseUrl: 'http://localhost:8484',
|
||||
keycloakUsername: 'mmuser',
|
||||
keycloakPassword: 'mostest_password',
|
||||
ldapServer: 'localhost',
|
||||
ldapPort: 389,
|
||||
minioAccessKey: 'minioaccesskey',
|
||||
minioSecretKey: 'miniosecretkey',
|
||||
minioS3Bucket: 'mattermost-test',
|
||||
minioS3Endpoint: 'localhost:9000',
|
||||
minioS3SSL: false,
|
||||
numberOfTrialUsers: 100,
|
||||
pushNotificationServer: 'https://push-test.mattermost.com',
|
||||
resetBeforeTest: false,
|
||||
runLDAPSync: true,
|
||||
secondServerURL: 'http://localhost/s/p',
|
||||
serverEdition: 'Team',
|
||||
serverClusterEnabled: false,
|
||||
serverClusterName: 'mm_dev_cluster',
|
||||
serverClusterHostCount: 3,
|
||||
smtpUrl: process.env.CYPRESS_smtpUrl || 'http://localhost:9001',
|
||||
webhookBaseUrl: 'http://localhost:3000',
|
||||
},
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// Custom webpack preprocessor for TypeScript 6 + @/ path aliases.
|
||||
// @cypress/webpack-preprocessor@7.1.0 already handles TS6's
|
||||
// downlevelIteration deprecation (cypress-io/cypress#33575).
|
||||
// We add: ts-loader for .ts files, resolve extensions, and @/ alias.
|
||||
// When Cypress >= 15.14 ships, the only custom piece needed is the @/ alias.
|
||||
const webpackPreprocessor = require('@cypress/webpack-preprocessor');
|
||||
|
||||
const webpackOptions = {
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.json'],
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'tests'),
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Restore Node.js polyfills (crypto, process, path, etc.)
|
||||
// that webpack 5 no longer provides by default.
|
||||
plugins: [
|
||||
new (require('node-polyfill-webpack-plugin'))(),
|
||||
new (require('webpack').ProvidePlugin)({
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
on('file:preprocessor', webpackPreprocessor({webpackOptions}));
|
||||
return require('./tests/plugins/index.js')(on, config);
|
||||
},
|
||||
baseUrl: process.env.MM_SERVICESETTINGS_SITEURL || 'http://localhost:8065',
|
||||
excludeSpecPattern: '**/node_modules/**/*',
|
||||
specPattern: 'tests/integration/**/*_spec.{js,ts}',
|
||||
supportFile: 'tests/support/index.js',
|
||||
testIsolation: false,
|
||||
},
|
||||
});
|
||||
84
e2e-tests/cypress/eslint.config.mjs
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import pluginCypress from 'eslint-plugin-cypress';
|
||||
import noOnlyTest from 'eslint-plugin-no-only-tests';
|
||||
import globals from 'globals';
|
||||
|
||||
import eslintPlugin from '@mattermost/eslint-plugin';
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['**/node_modules', '**/logs', '**/results'],
|
||||
},
|
||||
...eslintPlugin.configs.base,
|
||||
{
|
||||
files: ['**/*.ts', '**/*.js'],
|
||||
plugins: {
|
||||
cypress: pluginCypress,
|
||||
'no-only-tests': noOnlyTest,
|
||||
},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.chai,
|
||||
...globals.node,
|
||||
...globals.mocha,
|
||||
Cypress: 'readonly',
|
||||
cy: 'readonly',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'cypress/assertion-before-screenshot': 'warn',
|
||||
'cypress/no-assigning-return-values': 'error',
|
||||
'cypress/no-force': 'off',
|
||||
'cypress/no-async-tests': 'error',
|
||||
'cypress/no-pause': 'error',
|
||||
'cypress/no-unnecessary-waiting': 0,
|
||||
'cypress/unsafe-to-chain-command': 0,
|
||||
'func-names': 0,
|
||||
'global-require': 0,
|
||||
'@stylistic/lines-around-comment': 0,
|
||||
'max-nested-callbacks': 0,
|
||||
'no-await-in-loop': 0,
|
||||
'no-loop-func': 0,
|
||||
'no-unused-expressions': 0,
|
||||
'no-process-env': 0,
|
||||
'no-duplicate-imports': 0,
|
||||
'no-undefined': 0,
|
||||
'no-use-before-define': 0,
|
||||
'no-only-tests/no-only-tests': ['error', {focus: ['only']}],
|
||||
'no-console': 'error',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'headers/header-format': [
|
||||
'error',
|
||||
{
|
||||
source: 'string',
|
||||
style: 'line',
|
||||
content: 'Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.\nSee LICENSE.txt for license information.',
|
||||
trailingNewlines: 2,
|
||||
},
|
||||
],
|
||||
'import/no-duplicates': 2,
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
'newlines-between': 'always',
|
||||
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||
},
|
||||
],
|
||||
'import/no-unresolved': 'off',
|
||||
'max-lines': ['warn', {max: 800, skipBlankLines: true, skipComments: true}],
|
||||
'@stylistic/eol-last': ['error', 'always'],
|
||||
'@stylistic/no-trailing-spaces': 'error',
|
||||
},
|
||||
},
|
||||
];
|
||||
103
e2e-tests/cypress/generate_test_cycle.js
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
/*
|
||||
* This command, which is normally used in CI, generates test cycle in full or partial
|
||||
* depending on test metadata and environment capabilities into the Test Automation Dashboard.
|
||||
* Such generated test cycle is then used to run each spec file by "node run_test_cycle.js".
|
||||
*
|
||||
* Usage: [ENVIRONMENT] node generate_test_cycle.js [options]
|
||||
*
|
||||
* Options:
|
||||
* --stage=[stage]
|
||||
* Selects spec files with matching stage. It can be of multiple values separated by comma.
|
||||
* E.g. "--stage='@prod,@dev'" will select files with either @prod or @dev.
|
||||
* --group=[group]
|
||||
* Selects spec files with matching group. It can be of multiple values separated by comma.
|
||||
* E.g. "--group='@channel,@messaging'" will select files with either @channel or @messaging.
|
||||
* --invert
|
||||
* Selected files are those not matching any of the specified stage or group.
|
||||
* --include-group=[group]
|
||||
* Include spec files with matching group. It can be of multiple values separated by comma.
|
||||
* E.g. "--include-group='@enterprise'" will select files including @enterprise.
|
||||
* --exclude-group=[group]
|
||||
* Exclude spec files with matching group. It can be of multiple values separated by comma.
|
||||
* E.g. "--exclude-group='@enterprise'" will select files except @enterprise.
|
||||
* --include-file=[filename or directory]
|
||||
* Include spec files with matching directory or filename pattern. Uses `find` command under the hood. It can be of multiple values separated by comma.
|
||||
* E.g. "--include-file='channel'" will include files recursively under `channel` directory/s.
|
||||
* E.g. "--include-file='*channel*'" will include files and files under directory/s recursively that matches the name with `*channel*`.
|
||||
* --exclude-file=[filename or directory]
|
||||
* Exclude spec files with matching directory or filename pattern. Uses `find` command under the hood. It can be of multiple values separated by comma.
|
||||
* E.g. "--exclude-file='channel'" will exclude files recursively under `channel` directory/s.
|
||||
* E.g. "--exclude-file='*channel*'" will exclude files and files under directory/s recursively that matches the name with `*channel*`.
|
||||
|
||||
*
|
||||
* Environment:
|
||||
* AUTOMATION_DASHBOARD_URL : Dashboard URL
|
||||
* AUTOMATION_DASHBOARD_TOKEN : Dashboard token
|
||||
* REPO : Project repository, ex. mattermost-webapp
|
||||
* BRANCH : Branch identifier from CI
|
||||
* BUILD_ID : Build identifier from CI
|
||||
* BROWSER : Chrome by default. Set to run test on other browser such as chrome, edge, electron and firefox.
|
||||
* The environment should have the specified browser to successfully run.
|
||||
* HEADLESS : Headless by default (true) or false to run on headed mode.
|
||||
*
|
||||
* Example:
|
||||
* 1. "node generate_test_cycle.js"
|
||||
* - will create test cycle based on default test environment, except those matching skipped metadata
|
||||
* 2. "node generate_test_cycle.js --stage='@prod'"
|
||||
* - will create test cycle for production tests, except those matching skipped metadata
|
||||
* 3. "node generate_test_cycle.js --stage='@prod' --invert"
|
||||
* - will create test cycle for all non-production tests
|
||||
* 4. "BROWSER='chrome' HEADLESS='false' node generate_test_cycle.js --stage='@prod' --group='@channel,@messaging'"
|
||||
* - will create test cycle for spec files matching stage and group values in Chrome (headed)
|
||||
* 5. "node generate_test_cycle.js --stage='@prod' --exclude-group='@enterprise'"
|
||||
* - will create test cycle for all production tests except @enterprise group
|
||||
* - typical test run for Team Edition
|
||||
* 6. "node generate_test_cycle.js --stage='@prod' --sort-first='@elasticsearch' --sort-last='@mfa'"
|
||||
* - will create test cycle for all production tests with specs specifically ordered as first and last
|
||||
*/
|
||||
|
||||
const os = require('os');
|
||||
|
||||
const chalk = require('chalk').default;
|
||||
|
||||
const {createAndStartCycle} = require('./utils/dashboard');
|
||||
const {getSortedTestFiles} = require('./utils/file');
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const {
|
||||
BRANCH,
|
||||
BROWSER,
|
||||
BUILD_ID,
|
||||
HEADLESS,
|
||||
REPO,
|
||||
} = process.env;
|
||||
|
||||
async function main() {
|
||||
const browser = BROWSER || 'chrome';
|
||||
const headless = typeof HEADLESS === 'undefined' ? true : HEADLESS === 'true';
|
||||
const platform = os.platform();
|
||||
const {weightedTestFiles} = getSortedTestFiles(platform, browser, headless);
|
||||
|
||||
if (!weightedTestFiles.length) {
|
||||
console.log(chalk.red('Nothing to test!'));
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await createAndStartCycle({
|
||||
repo: REPO,
|
||||
branch: BRANCH,
|
||||
build: BUILD_ID,
|
||||
files: weightedTestFiles,
|
||||
});
|
||||
|
||||
console.log(chalk.green('Successfully generated a test cycle.'));
|
||||
console.log(data.cycle);
|
||||
}
|
||||
|
||||
main();
|
||||
93
e2e-tests/cypress/package.json
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"name": "cypress",
|
||||
"devDependencies": {
|
||||
"@cypress/request": "4.0.1",
|
||||
"@cypress/webpack-preprocessor": "7.1.2",
|
||||
"@mattermost/client": "11.9.0",
|
||||
"@mattermost/eslint-plugin": "file:../../webapp/platform/eslint-plugin",
|
||||
"@mattermost/types": "11.9.0",
|
||||
"@testing-library/cypress": "10.1.3",
|
||||
"@types/authenticator": "1.1.4",
|
||||
"@types/express": "5.0.6",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/lodash": "4.17.25",
|
||||
"@types/lodash.intersection": "4.4.9",
|
||||
"@types/lodash.mapkeys": "4.6.9",
|
||||
"@types/lodash.without": "4.4.9",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/mochawesome": "6.2.5",
|
||||
"@types/shelljs": "0.10.0",
|
||||
"authenticator": "1.1.5",
|
||||
"axios": "1.19.0",
|
||||
"chai": "6.2.2",
|
||||
"chalk": "6.0.0",
|
||||
"client-oauth2": "github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49",
|
||||
"cross-env": "10.1.0",
|
||||
"cypress": "15.20.1",
|
||||
"cypress-file-upload": "5.0.8",
|
||||
"cypress-multi-reporters": "2.0.5",
|
||||
"cypress-plugin-tab": "2.0.0",
|
||||
"cypress-real-events": "1.15.0",
|
||||
"cypress-wait-until": "3.0.2",
|
||||
"dayjs": "1.11.23",
|
||||
"deepmerge": "4.3.1",
|
||||
"dotenv": "17.4.2",
|
||||
"eslint": "9.39.3",
|
||||
"eslint-plugin-cypress": "6.4.1",
|
||||
"eslint-plugin-no-only-tests": "3.4.0",
|
||||
"express": "5.2.1",
|
||||
"extract-zip": "2.0.1",
|
||||
"globals": "17.11.0",
|
||||
"jiti": "2.7.0",
|
||||
"knex": "3.3.0",
|
||||
"localforage": "1.10.0",
|
||||
"lodash.intersection": "4.4.0",
|
||||
"lodash.mapkeys": "4.6.0",
|
||||
"lodash.without": "4.4.0",
|
||||
"lodash.xor": "4.5.0",
|
||||
"mime": "4.1.0",
|
||||
"mime-types": "3.0.2",
|
||||
"mocha": "11.8.0",
|
||||
"mocha-junit-reporter": "2.2.1",
|
||||
"mocha-multi-reporters": "1.5.1",
|
||||
"mochawesome": "8.0.1",
|
||||
"mochawesome-merge": "5.1.1",
|
||||
"mochawesome-report-generator": "6.3.2",
|
||||
"moment-timezone": "0.6.3",
|
||||
"node-polyfill-webpack-plugin": "4.1.0",
|
||||
"pdf-parse": "2.4.5",
|
||||
"pg": "8.23.0",
|
||||
"shelljs": "0.10.0",
|
||||
"timezones.json": "1.7.2",
|
||||
"ts-loader": "9.6.2",
|
||||
"typescript": "6.0.3",
|
||||
"uuid": "14.0.1",
|
||||
"yargs": "18.1.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@mattermost/client": {
|
||||
"typescript": "^6"
|
||||
},
|
||||
"@mattermost/types": {
|
||||
"typescript": "^6"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"check-types": "tsc -b",
|
||||
"cypress:open": "cross-env TZ=Etc/UTC cypress open",
|
||||
"cypress:run": "cross-env TZ=Etc/UTC cypress run",
|
||||
"cypress:run:chrome": "cross-env TZ=Etc/UTC cypress run --browser chrome",
|
||||
"cypress:run:firefox": "cross-env TZ=Etc/UTC cypress run --browser firefox",
|
||||
"cypress:run:edge": "cross-env TZ=Etc/UTC cypress run --browser edge",
|
||||
"cypress:run:electron": "cross-env TZ=Etc/UTC cypress run --browser electron",
|
||||
"benchmarks:run-server": "cd mattermost && bin/mattermost",
|
||||
"start:webhook": "node webhook_serve.js",
|
||||
"pretest": "npm run clean",
|
||||
"test": "cross-env TZ=Etc/UTC cypress run",
|
||||
"test:smoke": "node run_tests.js --stage='@prod' --group='@smoke'",
|
||||
"test:ci": "node run_tests.js",
|
||||
"uniq-meta": "grep -r \"^// $META:\" cypress | grep -ow '@\\w*' | sort | uniq",
|
||||
"check": "eslint .",
|
||||
"fix": "eslint . --quiet --fix --cache"
|
||||
}
|
||||
}
|
||||
15
e2e-tests/cypress/reporter-config.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"reporterEnabled": "mocha-junit-reporter, mochawesome",
|
||||
"mochaJunitReporterReporterOptions": {
|
||||
"mochaFile": "results/junit/test_results[hash].xml",
|
||||
"toConsole": false
|
||||
},
|
||||
"mochawesomeReporterOptions": {
|
||||
"reportDir": "results/mochawesome-report",
|
||||
"reportFilename": "json/tests/[name]",
|
||||
"quiet": true,
|
||||
"overwrite": false,
|
||||
"html": false,
|
||||
"json": true
|
||||
}
|
||||
}
|
||||
273
e2e-tests/cypress/run_test_cycle.js
Normal file
@@ -0,0 +1,273 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
/*
|
||||
* This command, which is normally used in CI, runs Cypress test in full or partial
|
||||
* depending on test metadata and environment capabilities.
|
||||
* Spec file to run is dependent from Test Automation Dashboard and each test result is being
|
||||
* recorded on a per spec file basis.
|
||||
*
|
||||
* Usage: [ENVIRONMENT] node run_test_cycle.js
|
||||
*
|
||||
* Environment:
|
||||
* AUTOMATION_DASHBOARD_URL : Dashboard URL
|
||||
* AUTOMATION_DASHBOARD_TOKEN : Dashboard token
|
||||
* REPO : Project repository, ex. mattermost-webapp
|
||||
* BRANCH : Branch identifier from CI
|
||||
* BUILD_ID : Build identifier from CI
|
||||
* CI_BASE_URL : Test server base URL in CI
|
||||
*
|
||||
* Example:
|
||||
* 1. "node run_test_cycle.js"
|
||||
* - will run all the specs available from the Automation dashboard
|
||||
*/
|
||||
|
||||
const chalk = require('chalk').default;
|
||||
const cypress = require('cypress');
|
||||
|
||||
const {
|
||||
getSpecToTest,
|
||||
recordSpecResult,
|
||||
updateCycle,
|
||||
uploadScreenshot,
|
||||
} = require('./utils/dashboard');
|
||||
const {writeJsonToFile} = require('./utils/report');
|
||||
const {MOCHAWESOME_REPORT_DIR, RESULTS_DIR} = require('./utils/constants');
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const {
|
||||
BRANCH,
|
||||
BROWSER,
|
||||
BUILD_ID,
|
||||
CI_BASE_URL,
|
||||
HEADLESS,
|
||||
REPO,
|
||||
} = process.env;
|
||||
|
||||
async function runCypressTest(specExecution) {
|
||||
const browser = BROWSER || 'chrome';
|
||||
const headless = isHeadless();
|
||||
|
||||
const result = await cypress.run({
|
||||
browser,
|
||||
headless,
|
||||
spec: specExecution.file,
|
||||
config: {
|
||||
screenshotsFolder: `${MOCHAWESOME_REPORT_DIR}/screenshots`,
|
||||
videosFolder: `${MOCHAWESOME_REPORT_DIR}/videos`,
|
||||
trashAssetsBeforeRuns: false,
|
||||
},
|
||||
reporter: 'cypress-multi-reporters',
|
||||
reporterOptions: {
|
||||
reporterEnabled: 'mocha-junit-reporter, mochawesome',
|
||||
mochaJunitReporterReporterOptions: {
|
||||
mochaFile: 'results/junit/test_results[hash].xml',
|
||||
toConsole: false,
|
||||
},
|
||||
mochawesomeReporterOptions: {
|
||||
reportDir: MOCHAWESOME_REPORT_DIR,
|
||||
reportFilename: `json/${specExecution.file}`,
|
||||
quiet: true,
|
||||
overwrite: false,
|
||||
html: false,
|
||||
json: true,
|
||||
testMeta: {
|
||||
browser,
|
||||
headless,
|
||||
branch: BRANCH,
|
||||
buildId: BUILD_ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function saveResult(specExecution, result, testIndex) {
|
||||
// Write and update test environment details once
|
||||
if (testIndex === 0) {
|
||||
const environment = {
|
||||
cypress_version: result.cypressVersion,
|
||||
browser_name: result.browserName,
|
||||
browser_version: result.browserVersion,
|
||||
headless: isHeadless(),
|
||||
os_name: result.osName,
|
||||
os_version: result.osVersion,
|
||||
node_version: process.version,
|
||||
};
|
||||
|
||||
writeJsonToFile(environment, 'environment.json', RESULTS_DIR);
|
||||
await updateCycle(specExecution.cycle_id, environment);
|
||||
}
|
||||
|
||||
const {stats, tests, spec} = result.runs[0];
|
||||
|
||||
const specPatch = {
|
||||
file: spec.relative,
|
||||
tests: spec.tests,
|
||||
pass: stats.passes,
|
||||
fail: stats.failures,
|
||||
pending: stats.pending,
|
||||
skipped: stats.skipped,
|
||||
duration: stats.duration || 0,
|
||||
test_start_at: stats.startedAt,
|
||||
test_end_at: stats.endedAt,
|
||||
};
|
||||
|
||||
const testCases = [];
|
||||
for (let i = 0; i < tests.length; i++) {
|
||||
const test = tests[i];
|
||||
const attempts = test.attempts.pop();
|
||||
|
||||
const testCase = {
|
||||
title: test.title,
|
||||
full_title: test.title.join(' '),
|
||||
state: attempts.state,
|
||||
duration: attempts.duration || 0,
|
||||
code: trimToMaxLength(test.body),
|
||||
};
|
||||
|
||||
if (attempts.startedAt) {
|
||||
testCase.test_start_at = attempts.startedAt;
|
||||
}
|
||||
|
||||
if (test.displayError) {
|
||||
testCase.error_display = trimToMaxLength(test.displayError);
|
||||
}
|
||||
|
||||
const errorFrame = attempts.error && attempts.error.codeFrame && attempts.error.codeFrame.frame;
|
||||
if (errorFrame) {
|
||||
testCase.error_frame = trimToMaxLength(errorFrame);
|
||||
}
|
||||
|
||||
if (attempts.screenshots && attempts.screenshots.length > 0) {
|
||||
const path = attempts.screenshots[0].path;
|
||||
const screenshotUrl = await uploadScreenshot(path, REPO, BRANCH, BUILD_ID);
|
||||
if (typeof screenshotUrl === 'string' && !screenshotUrl.error) {
|
||||
testCase.screenshot = {url: screenshotUrl};
|
||||
}
|
||||
}
|
||||
|
||||
testCases.push(testCase);
|
||||
}
|
||||
|
||||
await recordSpecResult(specExecution.id, specPatch, testCases);
|
||||
}
|
||||
|
||||
function isHeadless() {
|
||||
return typeof HEADLESS === 'undefined' ? true : HEADLESS === 'true';
|
||||
}
|
||||
|
||||
function trimToMaxLength(text) {
|
||||
const maxLength = 5000;
|
||||
return text && text.length > maxLength ? text.substring(0, maxLength) : text;
|
||||
}
|
||||
|
||||
function printSummary(summary) {
|
||||
const obj = summary.reduce((acc, item) => {
|
||||
const {server, state, count} = item;
|
||||
if (!server) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (acc[server]) {
|
||||
acc[server][state] = count;
|
||||
} else {
|
||||
acc[server] = {[state]: count, server};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
Object.values(obj).sort((a, b) => {
|
||||
return a.server.localeCompare(b.server);
|
||||
}).forEach((item) => {
|
||||
const {server, done, started} = item;
|
||||
console.log(chalk.magenta(`${server}: done: ${done || 0}, started: ${started || 0}`));
|
||||
});
|
||||
}
|
||||
|
||||
const maxRetryCount = 5;
|
||||
async function runSpecFragment(count, retry) {
|
||||
console.log(chalk.magenta(`Preparing for: ${count + 1}`));
|
||||
|
||||
const spec = await getSpecToTest({
|
||||
repo: REPO,
|
||||
branch: BRANCH,
|
||||
build: BUILD_ID,
|
||||
server: CI_BASE_URL,
|
||||
});
|
||||
|
||||
// Retry on connection/timeout errors
|
||||
if (!spec || spec.code) {
|
||||
if (retry >= maxRetryCount) {
|
||||
return {
|
||||
tryNext: false,
|
||||
count,
|
||||
message: `Test ended due to multiple (${retry}) connection/timeout errors with the dashboard server.`,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(chalk.red(`Retry count: ${retry}`));
|
||||
return runSpecFragment(count, retry + 1);
|
||||
}
|
||||
|
||||
if (!spec.execution || !spec.execution.file) {
|
||||
return {
|
||||
tryNext: false,
|
||||
count,
|
||||
message: spec.message,
|
||||
};
|
||||
}
|
||||
|
||||
const currentTestCount = spec.summary.reduce((total, item) => {
|
||||
return total + parseInt(item.count, 10);
|
||||
}, 0);
|
||||
|
||||
printSummary(spec.summary);
|
||||
console.log(chalk.magenta(`\n(Testing ${currentTestCount} of ${spec.cycle.specs_registered}) - ${spec.execution.file}`));
|
||||
console.log(chalk.magenta(`At "${process.env.CI_BASE_URL}" server`));
|
||||
|
||||
const result = await runCypressTest(spec.execution);
|
||||
await saveResult(spec.execution, result, count);
|
||||
|
||||
const newCount = count + 1;
|
||||
|
||||
if (spec.cycle.specs_registered === currentTestCount) {
|
||||
return {
|
||||
tryNext: false,
|
||||
count: newCount,
|
||||
message: `Completed testing of all registered ${currentTestCount} spec/s.`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
tryNext: true,
|
||||
count: newCount,
|
||||
retry: 0,
|
||||
message: 'Continue testing',
|
||||
};
|
||||
}
|
||||
|
||||
async function runSpec(count = 0, retry = 0) {
|
||||
const fragment = await runSpecFragment(count, retry);
|
||||
if (fragment.tryNext) {
|
||||
return runSpec(fragment.count, fragment.retry);
|
||||
}
|
||||
|
||||
return {
|
||||
count: fragment.count,
|
||||
message: fragment.message,
|
||||
};
|
||||
}
|
||||
|
||||
runSpec().then(({count, message}) => {
|
||||
console.log(chalk.magenta(message));
|
||||
if (count > 0) {
|
||||
console.log(chalk.magenta(`This test runner has completed ${count} spec file/s.`));
|
||||
}
|
||||
});
|
||||
203
e2e-tests/cypress/run_tests.js
Normal file
@@ -0,0 +1,203 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
/*
|
||||
* This command, which normally use in CI, runs Cypress test in full or partial
|
||||
* depending on test metadata and environment capabilities.
|
||||
*
|
||||
* Usage: [ENVIRONMENT] node run_tests.js [options]
|
||||
*
|
||||
* Options:
|
||||
* --stage=[stage]
|
||||
* Selects spec files with matching stage. It can be of multiple values separated by comma.
|
||||
* E.g. "--stage='@prod,@dev'" will select files with either @prod or @dev.
|
||||
* --group=[group]
|
||||
* Selects spec files with matching group. It can be of multiple values separated by comma.
|
||||
* E.g. "--group='@channel,@messaging'" will select files with either @channel or @messaging.
|
||||
* --invert
|
||||
* Selected files are those not matching any of the specified stage or group.
|
||||
* --include-group=[group]
|
||||
* Include spec files with matching group. It can be of multiple values separated by comma.
|
||||
* E.g. "--include-group='@enterprise'" will select files including @enterprise.
|
||||
* --exclude-group=[group]
|
||||
* Exclude spec files with matching group. It can be of multiple values separated by comma.
|
||||
* E.g. "--exclude-group='@enterprise'" will select files except @enterprise.
|
||||
* --include-file=[filename or directory]
|
||||
* Include spec files with matching directory or filename pattern. Uses `find` command under the hood. It can be of multiple values separated by comma.
|
||||
* E.g. "--include-file='channel'" will include files recursively under `channel` directory/s.
|
||||
* E.g. "--include-file='*channel*'" will include files and files under directory/s recursively that matches the name with `*channel*`.
|
||||
* --exclude-file=[filename or directory]
|
||||
* Exclude spec files with matching directory or filename pattern. Uses `find` command under the hood. It can be of multiple values separated by comma.
|
||||
* E.g. "--exclude-file='channel'" will exclude files recursively under `channel` directory/s.
|
||||
* E.g. "--exclude-file='*channel*'" will exclude files and files under directory/s recursively that matches the name with `*channel*`.
|
||||
* --stress-test-count=[number of repeated executions]
|
||||
* The amount of times to run each spec file for. If greater than 1, report how the percentage of successes over total runs for each spec.
|
||||
|
||||
*
|
||||
* Environment:
|
||||
* BROWSER=[browser] : Chrome by default. Set to run test on other browser such as chrome, edge, electron and firefox.
|
||||
* The environment should have the specified browser to successfully run.
|
||||
* HEADLESS=[boolean] : Headless by default (true) or false to run on headed mode.
|
||||
* BRANCH=[branch] : Branch identifier from CI
|
||||
* BUILD_ID=[build_id] : Build identifier from CI
|
||||
* CI_BASE_URL=[ci_base_url] : Test server base URL in CI
|
||||
*
|
||||
* Example:
|
||||
* 1. "node run_tests.js"
|
||||
* - will run all the specs on default test environment, except those matching skipped metadata
|
||||
* 2. "node run_tests.js --stage='@prod'"
|
||||
* - will run all production tests, except those matching skipped metadata
|
||||
* 3. "node run_tests.js --stage='@prod' --invert"
|
||||
* - will run all non-production tests
|
||||
* 4. "BROWSER='chrome' HEADLESS='false' node run_tests.js --stage='@prod' --group='@channel,@messaging'"
|
||||
* - will run spec files matching stage and group values in Chrome (headed)
|
||||
* 5. "node run_tests.js --stage='@prod' --exclude-group='@enterprise'"
|
||||
* - will run all production tests except @enterprise group
|
||||
* - typical test run for Team Edition
|
||||
* 6. "node run_tests.js --stage='@prod' --part=1 --of=2"
|
||||
* - will run the first half (1 of 2) of all production tests
|
||||
* - will be used for parallel testing where each part could run separately against its own test server
|
||||
*/
|
||||
|
||||
const os = require('os');
|
||||
|
||||
const chalk = require('chalk').default;
|
||||
const cypress = require('cypress');
|
||||
const argv = require('yargs')(process.argv.slice(2)).argv;
|
||||
|
||||
const {getSortedTestFiles} = require('./utils/file');
|
||||
const {getTestFilesIdentifier} = require('./utils/even_distribution');
|
||||
const {writeJsonToFile} = require('./utils/report');
|
||||
const {MOCHAWESOME_REPORT_DIR, RESULTS_DIR} = require('./utils/constants');
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
async function runTests() {
|
||||
const {
|
||||
BRANCH,
|
||||
BROWSER,
|
||||
BUILD_ID,
|
||||
HEADLESS,
|
||||
} = process.env;
|
||||
|
||||
const browser = BROWSER || 'chrome';
|
||||
const headless = typeof HEADLESS === 'undefined' ? true : HEADLESS === 'true';
|
||||
const platform = os.platform();
|
||||
const stressTestCount = argv.stressTestCount > 1 ? argv.stressTestCount : 1;
|
||||
const testPasses = {};
|
||||
|
||||
const sortedFilesObject = getSortedTestFiles(platform, browser, headless);
|
||||
const sortedFiles = sortedFilesObject.sortedFiles.flatMap((el) => Array(stressTestCount).fill(el));
|
||||
const numberOfTestFiles = sortedFiles.length;
|
||||
|
||||
if (!numberOfTestFiles) {
|
||||
console.log(chalk.red('Nothing to test!'));
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
start,
|
||||
end,
|
||||
count,
|
||||
} = getTestFilesIdentifier(numberOfTestFiles, argv.part, argv.of);
|
||||
|
||||
for (let i = start, j = 0; i < end && j < count; i++, j++) {
|
||||
printMessage(sortedFiles, i, j + 1, count);
|
||||
|
||||
const testFile = sortedFiles[i];
|
||||
let testFileAttempt = 1;
|
||||
if (testFile in testPasses === false) {
|
||||
testPasses[testFile] = {attempt: 1};
|
||||
} else {
|
||||
testPasses[testFile].attempt += 1;
|
||||
testFileAttempt = testPasses[testFile].attempt;
|
||||
}
|
||||
|
||||
const result = await cypress.run({
|
||||
browser,
|
||||
headless,
|
||||
spec: testFile,
|
||||
config: {
|
||||
screenshotsFolder: `${MOCHAWESOME_REPORT_DIR}/screenshots`,
|
||||
videosFolder: `${MOCHAWESOME_REPORT_DIR}/videos`,
|
||||
trashAssetsBeforeRuns: false,
|
||||
retries: stressTestCount > 1 ? 0 : 2,
|
||||
},
|
||||
env: {
|
||||
firstTest: j === 0,
|
||||
},
|
||||
reporter: 'cypress-multi-reporters',
|
||||
reporterOptions: {
|
||||
reporterEnabled: 'mocha-junit-reporter, mochawesome',
|
||||
mochaJunitReporterReporterOptions: {
|
||||
mochaFile: 'results/junit/test_results[hash].xml',
|
||||
toConsole: false,
|
||||
},
|
||||
mochawesomeReporterOptions: {
|
||||
reportDir: MOCHAWESOME_REPORT_DIR,
|
||||
reportFilename: `json/${testFile}`,
|
||||
quiet: true,
|
||||
overwrite: false,
|
||||
html: false,
|
||||
json: true,
|
||||
testMeta: {
|
||||
platform,
|
||||
browser,
|
||||
headless,
|
||||
branch: BRANCH,
|
||||
buildId: BUILD_ID,
|
||||
testFileAttempt,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const testCase of result.runs[0].tests) {
|
||||
const testCaseTitle = testCase.title.join(' - ');
|
||||
if (testCaseTitle in testPasses[testFile] === false) {
|
||||
testPasses[testFile][testCaseTitle] = 0;
|
||||
}
|
||||
if (testCase.state === 'passed') {
|
||||
testPasses[testFile][testCaseTitle] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Write test environment details once only
|
||||
if (i === 0) {
|
||||
const environment = {
|
||||
cypress_version: result.cypressVersion,
|
||||
browser_name: result.browserName,
|
||||
browser_version: result.browserVersion,
|
||||
headless,
|
||||
os_name: result.osName,
|
||||
os_version: result.osVersion,
|
||||
node_version: process.version,
|
||||
};
|
||||
|
||||
writeJsonToFile(environment, 'environment.json', RESULTS_DIR);
|
||||
}
|
||||
}
|
||||
writeJsonToFile(testPasses, 'testPasses.json', RESULTS_DIR);
|
||||
}
|
||||
|
||||
function printMessage(testFiles, overallIndex, currentItem, lastItem) {
|
||||
const {invert, excludeGroup, group, stage} = argv;
|
||||
|
||||
const testFile = testFiles[overallIndex];
|
||||
const testStage = stage ? `Stage: "${stage}" ` : '';
|
||||
const withGroup = group || excludeGroup;
|
||||
const groupMessage = group ? `"${group}"` : 'All';
|
||||
const excludeGroupMessage = excludeGroup ? `except "${excludeGroup}"` : '';
|
||||
const testGroup = withGroup ? `Group: ${groupMessage} ${excludeGroupMessage}` : '';
|
||||
|
||||
// Log which files were being tested
|
||||
console.log(chalk.magenta.bold(`${invert ? 'All Except --> ' : ''}${testStage}${stage && withGroup ? '| ' : ''}${testGroup}`));
|
||||
console.log(chalk.magenta(`(Testing ${overallIndex + 1} of ${testFiles.length}) - `, testFile));
|
||||
if (process.env.CI_BASE_URL) {
|
||||
console.log(chalk.magenta(`Testing ${currentItem}/${lastItem} in "${process.env.CI_BASE_URL}" server`));
|
||||
}
|
||||
}
|
||||
|
||||
runTests();
|
||||
109
e2e-tests/cypress/save_report.js
Normal file
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
/*
|
||||
* This is used for sending data to automation dashboard and
|
||||
* publishing quick summary to community channels.
|
||||
*
|
||||
* Usage: [ENV] node save_report.js
|
||||
*
|
||||
* Environment variables:
|
||||
* BRANCH=[branch] : Branch identifier from CI
|
||||
* BUILD_ID=[build_id] : Build identifier from CI
|
||||
* BUILD_TAG=[build_tag] : Docker image used to run the test
|
||||
*
|
||||
* For saving test cases to Test Management
|
||||
* - TM4J_ENABLE=true|false
|
||||
* - TM4J_API_KEY=[api_key]
|
||||
* - JIRA_PROJECT_KEY=[project_key], e.g. "MM",
|
||||
* - TM4J_FOLDER_ID=[folder_id], e.g. 847997
|
||||
* For sending hooks to Mattermost channels
|
||||
* - FULL_REPORT, WEBHOOK_URL and DIAGNOSTIC_WEBHOOK_URL
|
||||
* - If AUTOMATION_DASHBOARD_FRONTEND_URL is set, a link to the corresponding cycle will be included in the report message
|
||||
* Test type
|
||||
* - TYPE=[type]; valid values: "PR", "RELEASE", "MASTER", "MASTER_UNSTABLE", "CLOUD", "CLOUD_UNSTABLE", "NONE"
|
||||
* Server type
|
||||
* - SERVER_TYPE=[type]; used for the 'Test Server' field in the webhook. Any string representing the server type is valid, common values are "onprem" and "cloud"
|
||||
*/
|
||||
|
||||
const {merge} = require('mochawesome-merge');
|
||||
const generator = require('mochawesome-report-generator');
|
||||
|
||||
const {
|
||||
generateDiagnosticReport,
|
||||
generateShortSummary,
|
||||
generateTestReport,
|
||||
removeOldGeneratedReports,
|
||||
sendReport,
|
||||
readJsonFromFile,
|
||||
writeJsonToFile,
|
||||
} = require('./utils/report');
|
||||
const {MOCHAWESOME_REPORT_DIR, RESULTS_DIR} = require('./utils/constants');
|
||||
const {createTestCycle, createTestExecutions} = require('./utils/test_cases');
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const saveReport = async () => {
|
||||
const {
|
||||
BRANCH,
|
||||
BUILD_ID,
|
||||
BUILD_TAG,
|
||||
DIAGNOSTIC_WEBHOOK_URL,
|
||||
DIAGNOSTIC_USER_ID,
|
||||
DIAGNOSTIC_TEAM_ID,
|
||||
TM4J_ENABLE,
|
||||
TM4J_CYCLE_KEY,
|
||||
TYPE,
|
||||
WEBHOOK_URL,
|
||||
} = process.env;
|
||||
|
||||
removeOldGeneratedReports();
|
||||
|
||||
// Merge all json reports into one single json report
|
||||
const jsonReport = await merge({files: [`${MOCHAWESOME_REPORT_DIR}/**/*.json`]});
|
||||
writeJsonToFile(jsonReport, 'all.json', MOCHAWESOME_REPORT_DIR);
|
||||
|
||||
// Generate the html report file
|
||||
await generator.create(
|
||||
jsonReport,
|
||||
{
|
||||
reportDir: MOCHAWESOME_REPORT_DIR,
|
||||
reportTitle: `Build:${BUILD_ID} Branch: ${BRANCH} Tag: ${BUILD_TAG}`,
|
||||
},
|
||||
);
|
||||
|
||||
// Generate short summary, write to file and then send report via webhook
|
||||
const summary = generateShortSummary(jsonReport);
|
||||
console.log(summary);
|
||||
writeJsonToFile(summary, 'summary.json', MOCHAWESOME_REPORT_DIR);
|
||||
|
||||
// Create or use an existing test cycle
|
||||
let testCycle = {};
|
||||
if (TM4J_ENABLE === 'true') {
|
||||
const {start, end} = jsonReport.stats;
|
||||
testCycle = TM4J_CYCLE_KEY ? {key: TM4J_CYCLE_KEY} : await createTestCycle(start, end);
|
||||
}
|
||||
|
||||
// Send test report to "QA: UI Test Automation" channel via webhook
|
||||
if (TYPE && TYPE !== 'NONE' && WEBHOOK_URL) {
|
||||
const environment = readJsonFromFile(`${RESULTS_DIR}/environment.json`);
|
||||
const data = generateTestReport(summary, false, undefined, environment, testCycle.key);
|
||||
await sendReport('summary report to Community channel', WEBHOOK_URL, data);
|
||||
}
|
||||
|
||||
// Send diagnostic report via webhook
|
||||
// Send on "RELEASE" type only
|
||||
if (TYPE === 'RELEASE' && DIAGNOSTIC_WEBHOOK_URL && DIAGNOSTIC_USER_ID && DIAGNOSTIC_TEAM_ID) {
|
||||
const data = generateDiagnosticReport(summary, {userId: DIAGNOSTIC_USER_ID, teamId: DIAGNOSTIC_TEAM_ID});
|
||||
await sendReport('test info for diagnostic analysis', DIAGNOSTIC_WEBHOOK_URL, data);
|
||||
}
|
||||
|
||||
// Save test cases to Test Management
|
||||
if (TM4J_ENABLE === 'true') {
|
||||
await createTestExecutions(jsonReport, testCycle);
|
||||
}
|
||||
};
|
||||
|
||||
saveReport();
|
||||
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable headers/header-format */
|
||||
|
||||
// taken from https://github.com/guilryder/chrome-extensions/tree/master/xframe_ignore
|
||||
|
||||
/*global chrome*/
|
||||
|
||||
const HEADERS_TO_STRIP_LOWERCASE = [
|
||||
'content-security-policy',
|
||||
'x-frame-options',
|
||||
];
|
||||
|
||||
chrome.webRequest.onHeadersReceived.addListener(
|
||||
(details) => {
|
||||
return {
|
||||
responseHeaders: details.responseHeaders.filter((header) => {
|
||||
return HEADERS_TO_STRIP_LOWERCASE.indexOf(header.name.toLowerCase()) < 0;
|
||||
}),
|
||||
};
|
||||
}, {
|
||||
urls: ['<all_urls>'],
|
||||
}, ['blocking', 'responseHeaders']);
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"update_url": "https://clients2.google.com/service/update2/crx",
|
||||
"manifest_version": 2,
|
||||
"name": "Ignore X-Frame headers",
|
||||
"description": "Drops X-Frame-Options and Content-Security-Policy HTTP response headers, allowing all pages to be iframed.",
|
||||
"version": "1.1",
|
||||
"background": {
|
||||
"scripts": [
|
||||
"background.js"
|
||||
]
|
||||
},
|
||||
"permissions": [
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
BIN
e2e-tests/cypress/tests/fixtures/MM-logo-horizontal.png
vendored
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
e2e-tests/cypress/tests/fixtures/bot-default-avatar.png
vendored
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
22
e2e-tests/cypress/tests/fixtures/client_billing.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"mastercard":{
|
||||
"cardNumber":"5555555555554444",
|
||||
"expDate":"4242",
|
||||
"cvc":"412"
|
||||
},
|
||||
"visa":{
|
||||
"cardNumber":"4242424242424242",
|
||||
"expDate":"4242",
|
||||
"cvc":"412"
|
||||
},
|
||||
"unionpay":{
|
||||
"cardNumber":"6200000000000005",
|
||||
"expDate":"1244",
|
||||
"cvc":"123"
|
||||
},
|
||||
"invalidvisa":{
|
||||
"cardNumber":"4242424242424141",
|
||||
"expDate":"1212",
|
||||
"cvc":"12"
|
||||
}
|
||||
}
|
||||
416
e2e-tests/cypress/tests/fixtures/console-example-inputs.json
vendored
Normal file
@@ -0,0 +1,416 @@
|
||||
[
|
||||
{
|
||||
"section": "about.license",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/about/license",
|
||||
"selector": "remove-button"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "reporting.system_analytics",
|
||||
"disabledInputs": []
|
||||
},
|
||||
{
|
||||
"section": "reporting.team_statistics",
|
||||
"disabledInputs": []
|
||||
},
|
||||
{
|
||||
"section": "reporting.server_logs",
|
||||
"disabledInputs": []
|
||||
},
|
||||
{
|
||||
"section": "user_management.system_users",
|
||||
"disabledInputs": []
|
||||
},
|
||||
{
|
||||
"section": "user_management.groups",
|
||||
"disabledInputs": []
|
||||
},
|
||||
{
|
||||
"section": "user_management.teams",
|
||||
"disabledInputs": []
|
||||
},
|
||||
{
|
||||
"section": "user_management.channel",
|
||||
"disabledInputs": []
|
||||
},
|
||||
{
|
||||
"section": "user_management.permissions",
|
||||
"disabledInputs": []
|
||||
},
|
||||
{
|
||||
"section": "environment.web_server",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/environment/web_server",
|
||||
"selector": "ServiceSettings.ListenAddressinput"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "site.customization",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "admin_console/site_config/customization",
|
||||
"selector": "TeamSettings.SiteNameinput"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "site.localization",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "admin_console/site_config/localization",
|
||||
"selector": "LocalizationSettings.DefaultServerLocaledropdown"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "site.users_and_teams",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "admin_console/site_config/users_and_teams",
|
||||
"selector": "TeamSettings.MaxUsersPerTeamnumber"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "site.notifications",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "admin_console/environment/notifications",
|
||||
"selector": "TeamSettings.EnableConfirmNotificationsToChanneltrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "site.announcement_banner",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "admin_console/site_config/announcement_banner",
|
||||
"selector": "AnnouncementSettings.EnableBannertrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "site.emoji",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "admin_console/site_config/emoji",
|
||||
"selector": "ServiceSettings.EnableEmojiPickertrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "site.posts",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "admin_console/site_config/posts",
|
||||
"selector": "ServiceSettings.EnableLinkPreviewstrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "site.file_sharing_downloads",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "admin_console/site_config/file_sharing_downloads",
|
||||
"selector": "FileSettings.EnableFileAttachmentstrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "site.public_links",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "admin_console/site_config/public_links",
|
||||
"selector": "FileSettings.EnablePublicLinktrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "site.notices",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "admin_console/site_config/notices",
|
||||
"selector": "AnnouncementSettings.AdminNoticesEnabledtrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "environment.database",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/environment/database",
|
||||
"selector": "maxIdleConnsinput"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "environment.elasticsearch",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/environment/elasticsearch",
|
||||
"selector": "enableIndexingtrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "environment.storage",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/environment/file_storage",
|
||||
"selector": "FileSettings.DriverNamedropdown"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "environment.image_proxy",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/environment/image_proxy",
|
||||
"selector": "ImageProxySettings.Enabletrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "environment.smtp",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/environment/smtp",
|
||||
"selector": "EmailSettings.EnableSMTPAuthtrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "environment.push_notification_server",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/environment/push_notification_server",
|
||||
"selector": "pushNotificationServerTypedropdown"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "environment.high_availability",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/environment/high_availability",
|
||||
"selector": "Enabletrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "environment.rate_limiting",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/environment/rate_limiting",
|
||||
"selector": "RateLimitSettings.Enabletrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "environment.logging",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/environment/logging",
|
||||
"selector": "LogSettings.ConsoleLeveldropdown"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "environment.session_lengths",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/environment/session_lengths",
|
||||
"selector": "sessionLengthWebInDaysinput"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "environment.metrics",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/environment/performance_monitoring",
|
||||
"selector": "MetricsSettings.ListenAddressinput"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "environment.developer",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/environment/developer",
|
||||
"selector": "ServiceSettings.EnableTestingtrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "authentication.signup",
|
||||
"disabledInputs":[
|
||||
{
|
||||
"path": "/admin_console/authentication/signup",
|
||||
"selector": "TeamSettings.EnableUserCreationfalse"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "authentication.email",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/authentication/email",
|
||||
"selector": "EmailSettings.EnableSignUpWithEmailfalse"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "authentication.password",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/authentication/password",
|
||||
"selector": "passwordMinimumLengthinput"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "authentication.mfa",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/authentication/mfa",
|
||||
"selector": "ServiceSettings.EnableMultifactorAuthenticationfalse"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "authentication.ldap",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/authentication/ldap",
|
||||
"selector": "LdapSettings.Enablefalse"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "authentication.saml",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/authentication/saml",
|
||||
"selector": "SamlSettings.Enablefalse"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "authentication.openid",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/authentication/openid",
|
||||
"selector": "openidTypedropdown"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "authentication.guest_access",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/authentication/guest_access",
|
||||
"selector": "GuestAccountsSettings.Enablefalse"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "plugins",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/plugins/plugin_management",
|
||||
"selector": "marketplaceUrlinput"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "integrations.integration_management",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/integrations/integration_management",
|
||||
"selector": "ServiceSettings.EnableIncomingWebhookstrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "integrations.bot_accounts",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/integrations/bot_accounts",
|
||||
"selector": "ServiceSettings.EnableBotAccountCreationtrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "integrations.gif",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/integrations/gif",
|
||||
"selector": "ServiceSettings.EnableGifPickertrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "integrations.cors",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/integrations/cors",
|
||||
"selector": "ServiceSettings.AllowCorsFrominput"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "compliance.data_retention",
|
||||
"disabledInputs": []
|
||||
},
|
||||
{
|
||||
"section": "compliance.message_export",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/compliance/export",
|
||||
"selector": "enableComplianceExporttrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "compliance.audits",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/compliance/monitoring",
|
||||
"selector": "ComplianceSettings.Enabletrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "compliance.custom_terms_of_service",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/compliance/custom_terms_of_service",
|
||||
"selector": "SupportSettings.CustomTermsOfServiceEnabledtrue"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "experimental.experimental_features",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/experimental/features",
|
||||
"selector": "ExperimentalSettings.LinkMetadataTimeoutMillisecondsnumber"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "experimental.feature_flags",
|
||||
"disabledInputs": [
|
||||
{
|
||||
"path": "/admin_console/experimental/feature_flags",
|
||||
"selector": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
7
e2e-tests/cypress/tests/fixtures/date_time_format.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
module.exports = {
|
||||
TIME_12_HOUR: 'h:mm A', // no leading zeros
|
||||
TIME_24_HOUR: 'HH:mm', // with leading zeros
|
||||
};
|
||||
BIN
e2e-tests/cypress/tests/fixtures/favicon-16x16.png
vendored
Normal file
|
After Width: | Height: | Size: 356 B |
BIN
e2e-tests/cypress/tests/fixtures/favicon-default-16x16.png
vendored
Normal file
|
After Width: | Height: | Size: 559 B |
BIN
e2e-tests/cypress/tests/fixtures/favicon-mentions-16x16.png
vendored
Normal file
|
After Width: | Height: | Size: 382 B |
BIN
e2e-tests/cypress/tests/fixtures/favicon-unread-16x16.png
vendored
Normal file
|
After Width: | Height: | Size: 514 B |
BIN
e2e-tests/cypress/tests/fixtures/gif-image-file-resized.gif
vendored
Normal file
|
After Width: | Height: | Size: 114 KiB |
26
e2e-tests/cypress/tests/fixtures/hooks/message_menus.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"attachments": [{
|
||||
"pretext": "This is the attachment pretext.",
|
||||
"text": "This is the attachment text.",
|
||||
"actions": [{
|
||||
"name": "Select an option...",
|
||||
"integration": {
|
||||
"url": "http://localhost:3000/message_menus",
|
||||
"context": {
|
||||
"action": "do_something"
|
||||
}
|
||||
},
|
||||
"type": "select",
|
||||
"options": [{
|
||||
"text": "Option 1",
|
||||
"value": "option1"
|
||||
}, {
|
||||
"text": "Option 2",
|
||||
"value": "option2"
|
||||
}, {
|
||||
"text": "Option 3",
|
||||
"value": "option3"
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
}
|
||||
17
e2e-tests/cypress/tests/fixtures/hooks/message_menus_with_datasource.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"attachments": [{
|
||||
"pretext": "This is the attachment pretext.",
|
||||
"text": "This is the attachment text.",
|
||||
"actions": [{
|
||||
"name": "Select an option...",
|
||||
"integration": {
|
||||
"url": "http://localhost:3000/message_menus_datasource",
|
||||
"context": {
|
||||
"action": "do_something"
|
||||
}
|
||||
},
|
||||
"type": "select",
|
||||
"data_source": "channels"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
BIN
e2e-tests/cypress/tests/fixtures/image-1000x40.jpg
vendored
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
e2e-tests/cypress/tests/fixtures/image-1600x40.jpg
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
e2e-tests/cypress/tests/fixtures/image-20x20.jpg
vendored
Normal file
|
After Width: | Height: | Size: 956 B |
BIN
e2e-tests/cypress/tests/fixtures/image-400x40.jpg
vendored
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
e2e-tests/cypress/tests/fixtures/image-400x400.jpg
vendored
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
e2e-tests/cypress/tests/fixtures/image-40x400.jpg
vendored
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
e2e-tests/cypress/tests/fixtures/image-50x50.jpg
vendored
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
e2e-tests/cypress/tests/fixtures/image-60x60.jpg
vendored
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
e2e-tests/cypress/tests/fixtures/image-small-height.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
e2e-tests/cypress/tests/fixtures/image-small-width.png
vendored
Normal file
|
After Width: | Height: | Size: 13 KiB |
72
e2e-tests/cypress/tests/fixtures/interactive_message_menus_options.json
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"many-options": [
|
||||
{"text": "Afghanistan", "value": "AF"},
|
||||
{"text": "Åland Islands", "value": "AX"},
|
||||
{"text": "Albania", "value": "AL"},
|
||||
{"text": "Algeria", "value": "DZ"},
|
||||
{"text": "American Samoa", "value": "AS"},
|
||||
{"text": "AndorrA", "value": "AD"},
|
||||
{"text": "Angola", "value": "AO"},
|
||||
{"text": "Anguilla", "value": "AI"},
|
||||
{"text": "Antarctica", "value": "AQ"},
|
||||
{"text": "Antigua and Barbuda", "value": "AG"},
|
||||
{"text": "Argentina", "value": "AR"},
|
||||
{"text": "Armenia", "value": "AM"},
|
||||
{"text": "Aruba", "value": "AW"},
|
||||
{"text": "Australia", "value": "AU"},
|
||||
{"text": "Austria", "value": "AT"},
|
||||
{"text": "Azerbaijan", "value": "AZ"},
|
||||
{"text": "Bahamas", "value": "BS"},
|
||||
{"text": "Bahrain", "value": "BH"},
|
||||
{"text": "Bangladesh", "value": "BD"},
|
||||
{"text": "Barbados", "value": "BB"},
|
||||
{"text": "Belarus", "value": "BY"},
|
||||
{"text": "Belgium", "value": "BE"},
|
||||
{"text": "Belize", "value": "BZ"},
|
||||
{"text": "Benin", "value": "BJ"},
|
||||
{"text": "Bermuda", "value": "BM"},
|
||||
{"text": "Bhutan", "value": "BT"},
|
||||
{"text": "Bolivia", "value": "BO"},
|
||||
{"text": "Bosnia and Herzegovina", "value": "BA"},
|
||||
{"text": "Botswana", "value": "BW"},
|
||||
{"text": "Bouvet Island", "value": "BV"},
|
||||
{"text": "Brazil", "value": "BR"},
|
||||
{"text": "British Indian Ocean Territory", "value": "IO"},
|
||||
{"text": "Brunei Darussalam", "value": "BN"},
|
||||
{"text": "Bulgaria", "value": "BG"},
|
||||
{"text": "Burkina Faso", "value": "BF"},
|
||||
{"text": "Burundi", "value": "BI"},
|
||||
{"text": "Cambodia", "value": "KH"},
|
||||
{"text": "Cameroon", "value": "CM"},
|
||||
{"text": "Canada", "value": "CA"},
|
||||
{"text": "Cape Verde", "value": "CV"},
|
||||
{"text": "Cayman Islands", "value": "KY"},
|
||||
{"text": "Central African Republic", "value": "CF"},
|
||||
{"text": "Chad", "value": "TD"},
|
||||
{"text": "Chile", "value": "CL"},
|
||||
{"text": "China", "value": "CN"},
|
||||
{"text": "Christmas Island", "value": "CX"},
|
||||
{"text": "Cocos (Keeling) Islands", "value": "CC"},
|
||||
{"text": "Colombia", "value": "CO"},
|
||||
{"text": "Comoros", "value": "KM"},
|
||||
{"text": "Congo", "value": "CG"},
|
||||
{"text": "Congo, The Democratic Republic of the", "value": "CD"},
|
||||
{"text": "Cook Islands", "value": "CK"},
|
||||
{"text": "Costa Rica", "value": "CR"},
|
||||
{"text": "Cote D\"Ivoire", "value": "CI"},
|
||||
{"text": "Croatia", "value": "HR"},
|
||||
{"text": "Cuba", "value": "CU"},
|
||||
{"text": "Cyprus", "value": "CY"},
|
||||
{"text": "Czech Republic", "value": "CZ"}
|
||||
],
|
||||
"distinct-options": [
|
||||
{"text": "Apple", "value": "apple"},
|
||||
{"text": "Orange", "value": "orange"},
|
||||
{"text": "Banana", "value": "banana"},
|
||||
{"text": "Grapes", "value": "grapes"},
|
||||
{"text": "Melon", "value": "melon"},
|
||||
{"text": "Mango", "value": "mango"},
|
||||
{"text": "Mango Raw", "value": "mangoraw"},
|
||||
{"text": "Avocado", "value": "avocado"}
|
||||
]
|
||||
}
|
||||
1885
e2e-tests/cypress/tests/fixtures/interactive_post.json
vendored
Normal file
BIN
e2e-tests/cypress/tests/fixtures/jpg-image-file.jpg
vendored
Normal file
|
After Width: | Height: | Size: 89 KiB |
8
e2e-tests/cypress/tests/fixtures/ldap-add-user.ldif
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
dn: uid=e2etest.four,ou=e2etest,dc=mm,dc=test,dc=com
|
||||
changetype: add
|
||||
objectclass: iNetOrgPerson
|
||||
sn: FourLDAP
|
||||
cn: TestLDAP
|
||||
uid: e2etest.four
|
||||
mail: e2etest.four@mmtest.com
|
||||
userPassword: Password1
|
||||
43
e2e-tests/cypress/tests/fixtures/ldap-reset-data.ldif
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
dn: uid=e2etest.one,ou=e2etest,dc=mm,dc=test,dc=com
|
||||
changetype: delete
|
||||
|
||||
dn: uid=e2etest.two,ou=e2etest,dc=mm,dc=test,dc=com
|
||||
changetype: delete
|
||||
|
||||
dn: uid=e2etest.three,ou=e2etest,dc=mm,dc=test,dc=com
|
||||
changetype: delete
|
||||
|
||||
dn: uid=e2etest.four,ou=e2etest,dc=mm,dc=test,dc=com
|
||||
changetype: delete
|
||||
|
||||
dn: ou=e2etest,dc=mm,dc=test,dc=com
|
||||
changetype: add
|
||||
objectclass: organizationalunit
|
||||
|
||||
# generic test users
|
||||
dn: uid=e2etest.one,ou=e2etest,dc=mm,dc=test,dc=com
|
||||
changetype: add
|
||||
objectclass: iNetOrgPerson
|
||||
sn: OneLDAP
|
||||
cn: TestLDAP
|
||||
uid: e2etest.one
|
||||
mail: e2etest.one@mmtest.com
|
||||
userPassword: Password1
|
||||
|
||||
dn: uid=e2etest.two,ou=e2etest,dc=mm,dc=test,dc=com
|
||||
changetype: add
|
||||
objectclass: iNetOrgPerson
|
||||
sn: TwoLDAP
|
||||
cn: TestLDAP
|
||||
uid: e2etest.two
|
||||
mail: e2etest.two@mmtest.com
|
||||
userPassword: Password1
|
||||
|
||||
dn: uid=e2etest.three,ou=e2etest,dc=mm,dc=test,dc=com
|
||||
changetype: add
|
||||
objectclass: iNetOrgPerson
|
||||
sn: ThreeLDAP
|
||||
cn: TestLDAP
|
||||
uid: e2etest.three.ldap
|
||||
mail: e2etest.three@mmtest.com
|
||||
userPassword: Password1
|
||||
44
e2e-tests/cypress/tests/fixtures/ldap_users.json
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"dev-1": {
|
||||
"username": "dev.one",
|
||||
"password": "Password1",
|
||||
"email": "success+devone@simulator.amazonses.com",
|
||||
"userType": "Admin"
|
||||
},
|
||||
"dev-2": {
|
||||
"username": "dev.two",
|
||||
"password": "Password1",
|
||||
"email": "success+devtwo@simulator.amazonses.com",
|
||||
"userType": "Admin"
|
||||
},
|
||||
"test-1": {
|
||||
"username": "test.one",
|
||||
"password": "Password1",
|
||||
"email": "success+testone@simulator.amazonses.com",
|
||||
"userType": ""
|
||||
},
|
||||
"test-2": {
|
||||
"username": "test.two",
|
||||
"password": "Password1",
|
||||
"email": "success+testtwo@simulator.amazonses.com",
|
||||
"userType": ""
|
||||
},
|
||||
"test-3": {
|
||||
"username": "test.three",
|
||||
"password": "Password1",
|
||||
"email": "success+testthree@simulator.amazonses.com",
|
||||
"userType": ""
|
||||
},
|
||||
"board-1": {
|
||||
"username": "board.one",
|
||||
"password": "Password1",
|
||||
"email": "success+boardone@simulator.amazonses.com",
|
||||
"userType": ""
|
||||
},
|
||||
"board-2": {
|
||||
"username": "board.two",
|
||||
"password": "Password1",
|
||||
"email": "success+boardtwo@simulator.amazonses.com",
|
||||
"userType": ""
|
||||
}
|
||||
}
|
||||
2
e2e-tests/cypress/tests/fixtures/long_text_post.txt
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
The quick, brown fox jumps over a lazy dog. DJs flock by when MTV ax quiz prog. Junk MTV quiz graced by fox whelps. Bawds jog, flick quartz, vex nymphs. Waltz, bad nymph, for quick jigs vex! Fox nymphs grab quick-jived waltz. Brick quiz whangs jumpy veldt fox. Bright vixens jump; dozy fowl quack. Quick wafting zephyrs vex bold Jim. Quick zephyrs blow, vexing daft Jim. Sex-charged fop blew my junk TV quiz. How quickly daft jumping zebras vex. Two driven jocks help fax my big quiz. Quick, Baz, get my woven flax jodhpurs! "Now fax quiz Jack!" my brave ghost pled. Five quacking zephyrs jolt my wax bed. Flummoxed by job, kvetching W. zaps Iraq. Cozy sphinx waves quart jug of bad milk. A very bad quack might jinx zippy fowls. Few quips galvanized the mock jury box. Quick brown dogs jump over the lazy fox. The jay, pig, fox, zebra, and my wolves quack! Blowzy red vixens fight for a quick jump. Joaquin Phoenix was gazed by MTV for luck. A wizard’s job is to vex chumps quickly in fog. Watch "Jeopardy!", Alex Trebek's fun TV quiz game. Woven silk pyjamas exchanged for blue quartz. Brawny gods just flocked up to quiz and vex him. Adjusting quiver and bow, Zompyc[1] killed the fox. My faxed joke won a pager in the cable TV quiz show. Amazingly few discotheques provide jukeboxes. My girl wove six dozen plaid jackets before she quit. Six big devils from Japan quickly forgot how to waltz. Big July earthquakes confound zany experimental vow. Foxy parsons quiz and cajole the lovably dim wiki-girl. Have a pick: twenty six letters - no forcing a jumbled quiz! Crazy Fredericka bought many very exquisite opal jewels. Sixty zippers were quickly picked from the woven jute bag. A quick movement of the enemy will jeopardize six gunboats. All questions asked by five watch experts amazed the judge. Jack quietly moved up front and seized the big ball of wax. The quick, brown fox jumps over a lazy dog. DJs flock by when MTV ax quiz prog. Junk MTV quiz graced by fox whelps. Bawds jog, flick quartz, vex nymphs. Waltz, bad nymph, for quick jigs vex! Fox nymphs grab quick-jived waltz. Brick quiz whangs jumpy veldt fox. Bright vixens jump; dozy fowl quack. Quick wafting zephyrs vex bold Jim. Quick zephyrs blow, vexing daft Jim. Sex-charged fop blew my junk TV quiz. How quickly daft jumping zebras vex. Two driven jocks help fax my big quiz. Quick, Baz, get my woven flax jodhpurs! "Now fax quiz Jack!" my brave ghost pled. Five quacking zephyrs jolt my wax bed. Flummoxed by job, kvetching W. zaps Iraq. Cozy sphinx waves quart jug of bad milk. A very bad quack might jinx zippy fowls. Few quips galvanized the mock jury box. Quick brown dogs jump over the lazy fox. The jay, pig, fox, zebra, and my wolves quack! Blowzy red vixens fight for a quick jump. Joaquin Phoenix was gazed by MTV for luck. A wizard’s job is to vex chumps quickly in fog. Watch "Jeopardy!", Alex Trebek's fun TV quiz game. Woven silk pyjamas exchanged for blue quartz. Brawny gods just flocked up to quiz and vex him. Adjusting quiver and bow, Zompyc[1] killed the fox. My faxed joke won a pager in the cable TV quiz show. Amazingly few discotheques provide jukeboxes. My girl wove six dozen plaid jackets before she quit. Six big devils from Japan quickly forgot how to waltz. Big July earthquakes confound zany experimental vow. Foxy parsons quiz and cajole the lovably dim wiki-girl. Have a pick: twenty six letters - no forcing a jumbled quiz! Crazy Fredericka bought many very exquisite opal jewels. Sixty zippers were quickly picked from the woven jute bag. A quick movement of the enemy will jeopardize six gunboats. All questions asked by five watch experts amazed the judge. Jack quietly moved up front and seized the big ball of wax. The quick, brown fox jumps over a lazy dog. DJs flock by when MTV ax quiz prog. Junk MTV quiz graced by fox whelps. Bawds jog, flick quartz, vex nymphs. Waltz, bad nymph, for quick jigs vex! Fox nymphs grab quick-jived waltz. Brick quiz whangs jumpy veldt fox. Hello this is a long post, with more than 4000 characters, plus multiple attachments.
|
||||
The quick, brown fox jumps over a lazy dog. DJs flock by when MTV ax quiz prog. Junk MTV quiz graced by fox whelps. Bawds jog, flick quartz, vex nymphs. Waltz, bad nymph, for quick jigs vex! Fox nymphs grab quick-jived waltz. Brick quiz whangs jumpy veldt fox. Bright vixens jump; dozy fowl quack. Quick wafting zephyrs vex bold Jim. Quick zephyrs blow, vexing daft Jim. Sex-charged fop blew my junk TV quiz. How quickly daft jumping zebras vex. Two driven jocks help fax my big quiz. Quick, Baz, get my woven flax jodhpurs! "Now fax quiz Jack!" my brave ghost pled. Five quacking zephyrs jolt my wax bed. Flummoxed by job, kvetching W. zaps Iraq. Cozy sphinx waves quart jug of bad milk. A very bad quack might jinx zippy fowls. Few quips galvanized the mock jury box. Quick brown dogs jump over the lazy fox. The jay, pig, fox, zebra, and my wolves quack! Blowzy red vixens fight for a quick jump. Joaquin Phoenix was gazed by MTV for luck. A wizard’s job is to vex chumps quickly in fog. Watch "Jeopardy!", Alex Trebek's fun TV quiz game. Woven silk pyjamas exchanged for blue quartz. Brawny gods just flocked up to quiz and vex him. Adjusting quiver and bow, Zompyc[1] killed the fox. My faxed joke won a pager in the cable TV quiz show. Amazingly few discotheques provide jukeboxes. My girl wove six dozen plaid jackets before she quit. Six big devils from Japan quickly forgot how to waltz. Big July earthquakes confound zany experimental vow. Foxy parsons quiz and cajole the lovably dim wiki-girl. Have a pick: twenty six letters - no forcing a jumbled quiz! Crazy Fredericka bought many very exquisite opal jewels. Sixty zippers were quickly picked from the woven jute bag. A quick movement of the enemy will jeopardize six gunboats. All questions asked by five watch experts amazed the judge. Jack quietly moved up front and seized the big ball of wax. The quick, brown fox jumps over a lazy dog. DJs flock by when MTV ax quiz prog. Junk MTV quiz graced by fox whelps. Bawds jog, flick quartz, vex nymphs. Waltz, bad nymph, for quick jigs vex! Fox nymphs grab quick-jived waltz. Brick quiz whangs jumpy veldt fox. Bright vixens jump; dozy fowl quack. Quick wafting zephyrs vex bold Jim. Quick zephyrs blow, vexing daft Jim. Sex-charged fop blew my junk TV quiz. How quickly daft jumping zebras vex. Two driven jocks help fax my big quiz. Quick, Baz, get my woven flax jodhpurs! "Now fax quiz Jack!" my brave ghost pled. Five quacking zephyrs jolt my wax bed. Flummoxed by job, kvetching W. zaps Iraq. Cozy sphinx waves quart jug of bad milk. A very bad quack might jinx zippy fowls. Few quips galvanized the mock jury box. Quick brown dogs jump over the lazy fox. The jay, pig, fox, zebra, and my wolves quack! Blowzy red vixens fight for a quick jump. Joaquin Phoenix was gazed by MTV for luck. A wizard’s job is to vex chumps quickly in fog. Watch "Jeopardy!", Alex Trebek's fun TV quiz game. Woven silk pyjamas exchanged for blue quartz. Brawny gods just flocked up to quiz and vex him. Adjusting quiver and bow, Zompyc[1] killed the fox. My faxed joke won a pager in the cable TV quiz show. Amazingly few discotheques provide jukeboxes. My girl wove six dozen plaid jackets before she quit. Six big devils from Japan quickly forgot how to waltz. Big July earthquakes confound zany experimental vow. Foxy parsons quiz and cajole the lovably dim wiki-girl. Have a pick: twenty six letters - no forcing a jumbled quiz! Crazy Fredericka bought many very exquisite opal jewels. Sixty zippers were quickly picked from the woven jute bag. A quick movement of the enemy will jeopardize six gunboats. All questions asked by five watch experts amazed the judge. Jack quietly moved up front and seized the big ball of wax. The quick, brown fox jumps over a lazy dog. DJs flock by when MTV ax quiz prog. Junk MTV quiz graced by fox whelps. Bawds jog, flick quartz, vex nymphs. Waltz, bad nymph, for quick jigs vex! Fox nymphs grab quick-jived waltz. Brick quiz whangs jumpy veldt fox. Hello this is a long post, with more than 4000 characters, plus multiple attachments.
|
||||
BIN
e2e-tests/cypress/tests/fixtures/m4a-audio-file.m4a
vendored
Normal file
1
e2e-tests/cypress/tests/fixtures/markdown/markdown_basic.html
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<h1 class="markdown__heading">Basic Markdown Testing</h1><p>Tests for text style, code blocks, in-line code and images, lines, block quotes, and headings.</p>
|
||||
2
e2e-tests/cypress/tests/fixtures/markdown/markdown_basic.md
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Basic Markdown Testing
|
||||
Tests for text style, code blocks, in-line code and images, lines, block quotes, and headings.
|
||||
11
e2e-tests/cypress/tests/fixtures/markdown/markdown_block_quotes_1.html
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
<h3 class="markdown__heading">Block Quotes</h3><blockquote>
|
||||
<p>This text should render in a block quote.</p>
|
||||
</blockquote>
|
||||
<p><strong>The following text should render in two block quotes separated by one line of text:</strong></p>
|
||||
<blockquote>
|
||||
<p>Block quote 1</p>
|
||||
</blockquote>
|
||||
<p>Text between block quotes</p>
|
||||
<blockquote>
|
||||
<p>Block quote 2</p>
|
||||
</blockquote>
|
||||
10
e2e-tests/cypress/tests/fixtures/markdown/markdown_block_quotes_1.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
### Block Quotes
|
||||
|
||||
>This text should render in a block quote.
|
||||
|
||||
**The following text should render in two block quotes separated by one line of text:**
|
||||
> Block quote 1
|
||||
|
||||
Text between block quotes
|
||||
|
||||
> Block quote 2
|
||||
6
e2e-tests/cypress/tests/fixtures/markdown/markdown_block_quotes_2.md
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
### Block Quotes
|
||||
|
||||
**The following markdown should render within the block quote:**
|
||||
> #### Heading 4
|
||||
> _Italics_, *Italics*, **Bold**, ***Bold-italics***, **_Bold-italics_**, ~~Strikethrough~~
|
||||
> :) :-) ;) :-O :bamboo: :gift_heart: :dolls:
|
||||
4
e2e-tests/cypress/tests/fixtures/markdown/markdown_carriage_return.html
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<h3 class="markdown__heading">Carriage Return</h3><p>Line #1 followed by one blank line</p>
|
||||
<p>Line #2 followed by one blank line</p>
|
||||
<p>Line #3 followed by Line #4
|
||||
Line #4</p>
|
||||
8
e2e-tests/cypress/tests/fixtures/markdown/markdown_carriage_return.md
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
### Carriage Return
|
||||
|
||||
Line #1 followed by one blank line
|
||||
|
||||
Line #2 followed by one blank line
|
||||
|
||||
Line #3 followed by Line #4
|
||||
Line #4
|
||||
4
e2e-tests/cypress/tests/fixtures/markdown/markdown_carriage_return_two_lines.html
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<p><strong>The following should appear as a carriage return separating two lines of text:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="post-code__overlay"><span class="post-code__clipboard"><i role="button" class="icon icon-content-copy"></i></span></div><div class="hljs"><code>Line #1 followed by a blank line
|
||||
|
||||
Line #2 following a blank line</code></div></div>
|
||||
6
e2e-tests/cypress/tests/fixtures/markdown/markdown_carriage_return_two_lines.md
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
**The following should appear as a carriage return separating two lines of text:**
|
||||
```
|
||||
Line #1 followed by a blank line
|
||||
|
||||
Line #2 following a blank line
|
||||
```
|
||||
1
e2e-tests/cypress/tests/fixtures/markdown/markdown_code_block.html
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<h3 class="markdown__heading">Code Blocks</h3><div class="post-code post-code--wrap"><div class="post-code__overlay"><span class="post-code__clipboard" aria-label="Copy code" role="button"><i class="icon icon-content-copy"></i></span></div><div class="hljs"><code>This text should render in a code block</code></div></div>
|
||||
5
e2e-tests/cypress/tests/fixtures/markdown/markdown_code_block.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
### Code Blocks
|
||||
|
||||
```
|
||||
This text should render in a code block
|
||||
```
|
||||
116
e2e-tests/cypress/tests/fixtures/markdown/markdown_code_syntax.html
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
<h1 class="markdown__heading">Code Syntax Highlighting</h1><p>Verify the following code blocks render as code blocks and highlight properly. </p>
|
||||
<h3 class="markdown__heading">Diff</h3><div class="post-code"><span class="post-code__language">Diff</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10</div><code><span class="hljs-comment">*** /path/to/original ''timestamp''</span>
|
||||
<span class="hljs-comment">--- /path/to/new ''timestamp''</span>
|
||||
<span class="hljs-comment">***************</span>
|
||||
<span class="hljs-comment">*** 1 ****</span>
|
||||
<span class="hljs-addition">! This is a line.</span>
|
||||
<span class="hljs-comment">--- 1 ---</span>
|
||||
<span class="hljs-addition">! This is a replacement line.</span>
|
||||
It is important to spell
|
||||
<span class="hljs-deletion">-removed line</span>
|
||||
<span class="hljs-addition">+new line</span></code></div></div><h3 class="markdown__heading">Makefile</h3><div class="post-code"><span class="post-code__language">Makefile</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3
|
||||
4
|
||||
5</div><code>CC=gcc
|
||||
CFLAGS=-I.
|
||||
|
||||
<span class="hljs-section">hellomake: hellomake.o hellofunc.o</span>
|
||||
<span class="hljs-variable">$(CC)</span> -o hellomake hellomake.o hellofunc.o -I.</code></div></div><h3 class="markdown__heading">JSON</h3><div class="post-code"><span class="post-code__language">JSON</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3</div><code>{<span class="hljs-attr">"employees"</span>:[
|
||||
{<span class="hljs-attr">"firstName"</span>:<span class="hljs-string">"John"</span>, <span class="hljs-attr">"lastName"</span>:<span class="hljs-string">"Doe"</span>},
|
||||
]}</code></div></div><h3 class="markdown__heading">Markdown</h3><div class="post-code"><span class="post-code__language">Markdown</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3</div><code><span class="hljs-strong">**bold**</span>
|
||||
<span class="hljs-emphasis">*italics*</span>
|
||||
[<span class="hljs-string">link</span>](<span class="hljs-link">www.example.com</span>)</code></div></div><h3 class="markdown__heading">JavaScript</h3><div class="post-code"><span class="post-code__language">JavaScript</span><div class="hljs"><div class="post-code__line-numbers">1</div><code><span class="hljs-built_in">document</span>.write(<span class="hljs-string">'Hello, world!'</span>);</code></div></div><h3 class="markdown__heading">CSS</h3><div class="post-code"><span class="post-code__language">CSS</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3</div><code><span class="hljs-selector-tag">body</span> {
|
||||
<span class="hljs-attribute">background-color</span>: red;
|
||||
}</code></div></div><h3 class="markdown__heading">Objective C</h3><div class="post-code"><span class="post-code__language">Objective C</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6</div><code><span class="hljs-meta">#import <span class="hljs-meta-string"><stdio.h></span></span>
|
||||
|
||||
<span class="hljs-keyword">int</span> main (<span class="hljs-keyword">void</span>)
|
||||
{
|
||||
printf (<span class="hljs-string">"Hello world!\n"</span>);
|
||||
}</code></div></div><h3 class="markdown__heading">Python</h3><div class="post-code"><span class="post-code__language">Python</span><div class="hljs"><div class="post-code__line-numbers">1</div><code><span class="hljs-built_in">print</span> <span class="hljs-string">"Hello, world!"</span></code></div></div><h3 class="markdown__heading">XML</h3><div class="post-code"><span class="post-code__language">HTML, XML</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3
|
||||
4
|
||||
5</div><code><span class="hljs-tag"><<span class="hljs-name">employees</span>></span>
|
||||
<span class="hljs-tag"><<span class="hljs-name">employee</span>></span>
|
||||
<span class="hljs-tag"><<span class="hljs-name">firstName</span>></span>John<span class="hljs-tag"></<span class="hljs-name">firstName</span>></span> <span class="hljs-tag"><<span class="hljs-name">lastName</span>></span>Doe<span class="hljs-tag"></<span class="hljs-name">lastName</span>></span>
|
||||
<span class="hljs-tag"></<span class="hljs-name">employee</span>></span>
|
||||
<span class="hljs-tag"></<span class="hljs-name">employees</span>></span></code></div></div><h3 class="markdown__heading">Perl</h3><div class="post-code"><span class="post-code__language">Perl</span><div class="hljs"><div class="post-code__line-numbers">1</div><code><span class="hljs-keyword">print</span> <span class="hljs-string">"Hello, World!\n"</span>;</code></div></div><h3 class="markdown__heading">Bash</h3><div class="post-code"><span class="post-code__language">Bash</span><div class="hljs"><div class="post-code__line-numbers">1</div><code><span class="hljs-built_in">echo</span> <span class="hljs-string">"Hello World"</span></code></div></div><h3 class="markdown__heading">PHP</h3><div class="post-code"><span class="post-code__language">PHP</span><div class="hljs"><div class="post-code__line-numbers">1</div><code> <span class="hljs-meta"><?php</span> <span class="hljs-keyword">echo</span> <span class="hljs-string">'<p>Hello World</p>'</span>; <span class="hljs-meta">?></span> </code></div></div><h3 class="markdown__heading">CoffeeScript</h3><div class="post-code"><span class="post-code__language">CoffeeScript</span><div class="hljs"><div class="post-code__line-numbers">1</div><code><span class="hljs-built_in">console</span>.log(“Hello world!”);</code></div></div><h3 class="markdown__heading">C</h3><div class="post-code"><span class="post-code__language">C#</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8</div><code><span class="hljs-keyword">using</span> System;
|
||||
<span class="hljs-keyword">class</span> <span class="hljs-title">Program</span>
|
||||
{
|
||||
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Main</span>(<span class="hljs-params"><span class="hljs-built_in">string</span>[] args</span>)</span>
|
||||
{
|
||||
Console.WriteLine(<span class="hljs-string">"Hello, world!"</span>);
|
||||
}
|
||||
}</code></div></div><h3 class="markdown__heading">C++</h3><div class="post-code"><span class="post-code__language">C/C++</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7</div><code><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string"><iostream.h></span></span>
|
||||
|
||||
main()
|
||||
{
|
||||
<span class="hljs-built_in">cout</span> << <span class="hljs-string">"Hello World!"</span>;
|
||||
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
|
||||
}</code></div></div><h3 class="markdown__heading">SQL</h3><div class="post-code"><span class="post-code__language">SQL</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2</div><code><span class="hljs-keyword">SELECT</span> column_name,column_name
|
||||
<span class="hljs-keyword">FROM</span> table_name;</code></div></div><h3 class="markdown__heading">Go</h3><div class="post-code"><span class="post-code__language">Go</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3
|
||||
4
|
||||
5</div><code><span class="hljs-keyword">package</span> main
|
||||
<span class="hljs-keyword">import</span> <span class="hljs-string">"fmt"</span>
|
||||
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
|
||||
fmt.Println(<span class="hljs-string">"Hello, 世界"</span>)
|
||||
}</code></div></div><h3 class="markdown__heading">Ruby</h3><div class="post-code"><span class="post-code__language">Ruby</span><div class="hljs"><div class="post-code__line-numbers">1</div><code>puts <span class="hljs-string">"Hello, world!"</span></code></div></div><h3 class="markdown__heading">Java</h3><div class="post-code"><span class="post-code__language">Java</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12</div><code><span class="hljs-keyword">import</span> javax.swing.JFrame; <span class="hljs-comment">//Importing class JFrame</span>
|
||||
<span class="hljs-keyword">import</span> javax.swing.JLabel; <span class="hljs-comment">//Importing class JLabel</span>
|
||||
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HelloWorld</span> </span>{
|
||||
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> </span>{
|
||||
JFrame frame = <span class="hljs-keyword">new</span> JFrame(); <span class="hljs-comment">//Creating frame</span>
|
||||
frame.setTitle(<span class="hljs-string">"Hi!"</span>); <span class="hljs-comment">//Setting title frame</span>
|
||||
frame.add(<span class="hljs-keyword">new</span> JLabel(<span class="hljs-string">"Hello, world!"</span>));<span class="hljs-comment">//Adding text to frame</span>
|
||||
frame.pack(); <span class="hljs-comment">//Setting size to smallest</span>
|
||||
frame.setLocationRelativeTo(<span class="hljs-keyword">null</span>); <span class="hljs-comment">//Centering frame</span>
|
||||
frame.setVisible(<span class="hljs-keyword">true</span>); <span class="hljs-comment">//Showing frame</span>
|
||||
}
|
||||
}</code></div></div><h3 class="markdown__heading">Latex Equation</h3><div class="post-body--code tex"><span class="katex-display fleqn"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mfrac><mi>d</mi><mrow><mi>d</mi><mi>x</mi></mrow></mfrac><mrow><mo fence="true">(</mo><msubsup><mo>∫</mo><mn>0</mn><mi>x</mi></msubsup><mi>f</mi><mo stretchy="false">(</mo><mi>u</mi><mo stretchy="false">)</mo><mtext> </mtext><mi>d</mi><mi>u</mi><mo fence="true">)</mo></mrow><mo>=</mo><mi>f</mi><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo><mi mathvariant="normal">.</mi></mrow><annotation encoding="application/x-tex">\frac{d}{dx}\left( \int_{0}^{x} f(u)\,du\right)=f(x).</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:2.40003em;vertical-align:-0.95003em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.37144em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">d</span><span class="mord mathnormal">x</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal">d</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.686em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="minner"><span class="mopen delimcenter" style="top:0em;"><span class="delimsizing size3">(</span></span><span class="mop"><span class="mop op-symbol large-op" style="margin-right:0.44445em;position:relative;top:-0.0011249999999999316em;">∫</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.414292em;"><span style="top:-1.7880500000000001em;margin-left:-0.44445em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">0</span></span></span></span><span style="top:-3.8129000000000004em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">x</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.9119499999999999em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mopen">(</span><span class="mord mathnormal">u</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathnormal">d</span><span class="mord mathnormal">u</span><span class="mclose delimcenter" style="top:0em;"><span class="delimsizing size3">)</span></span></span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mord">.</span></span></span></span></span></div>
|
||||
7
e2e-tests/cypress/tests/fixtures/markdown/markdown_escape_characters.html
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<h3 class="markdown__heading">Escaped Characters</h3><p><strong>The following text should render the same as the raw text:</strong>
|
||||
Raw: <span class="codespan__pre-wrap"><code>\\teamlinux\IT-Stuff\WorkingStuff</code></span>
|
||||
Markdown: \\teamlinux\IT-Stuff\WorkingStuff</p>
|
||||
<p><strong>The following text should escape out the first backslash so only one backslash appears:</strong>
|
||||
Raw: <span class="codespan__pre-wrap"><code>\\()#</code></span>
|
||||
Markdown: \()#</p>
|
||||
<p>The end of this long post will be hidden until you choose to <span class="codespan__pre-wrap"><code>Show More</code></span>.</p>
|
||||
11
e2e-tests/cypress/tests/fixtures/markdown/markdown_escape_characters.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
### Escaped Characters
|
||||
|
||||
**The following text should render the same as the raw text:**
|
||||
Raw: `\\teamlinux\IT-Stuff\WorkingStuff`
|
||||
Markdown: \\teamlinux\IT-Stuff\WorkingStuff
|
||||
|
||||
**The following text should escape out the first backslash so only one backslash appears:**
|
||||
Raw: `\\()#`
|
||||
Markdown: \\()#
|
||||
|
||||
The end of this long post will be hidden until you choose to `Show More`.
|
||||
1
e2e-tests/cypress/tests/fixtures/markdown/markdown_headings.html
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<h3 class="markdown__heading">Headings</h3><h1 class="markdown__heading">Heading 1 font size</h1><h2 class="markdown__heading">Heading 2 font size</h2><h3 class="markdown__heading">Heading 3 font size</h3><h4 class="markdown__heading">Heading 4 font size</h4><h5 class="markdown__heading">Heading 5 font size</h5><h6 class="markdown__heading">Heading 6 font size</h6>
|
||||
8
e2e-tests/cypress/tests/fixtures/markdown/markdown_headings.md
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
### Headings
|
||||
|
||||
# Heading 1 font size
|
||||
## Heading 2 font size
|
||||
### Heading 3 font size
|
||||
#### Heading 4 font size
|
||||
##### Heading 5 font size
|
||||
###### Heading 6 font size
|
||||
6
e2e-tests/cypress/tests/fixtures/markdown/markdown_inline_code.html
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
<h3 class="markdown__heading">In-line Code</h3><p>The word <span class="codespan__pre-wrap"><code>monospace</code></span> should render as in-line code.</p>
|
||||
<p>The following markdown in-line code should not render:
|
||||
<span class="codespan__pre-wrap"><code>_Italics_</code></span>, <span class="codespan__pre-wrap"><code>*Italics*</code></span>, <span class="codespan__pre-wrap"><code>**Bold**</code></span>, <span class="codespan__pre-wrap"><code>***Bold-italics***</code></span>, <span class="codespan__pre-wrap"><code>**Bold-italics_**</code></span>, <span class="codespan__pre-wrap"><code>~~Strikethrough~~</code></span>, <span class="codespan__pre-wrap"><code>:)</code></span> , <span class="codespan__pre-wrap"><code>:-)</code></span> , <span class="codespan__pre-wrap"><code>;)</code></span> , <span class="codespan__pre-wrap"><code>:-O</code></span> , <span class="codespan__pre-wrap"><code>:bamboo:</code></span> , <span class="codespan__pre-wrap"><code>:gift_heart:</code></span> , <span class="codespan__pre-wrap"><code>:dolls:</code></span> , <span class="codespan__pre-wrap"><code># Heading 1</code></span>, <span class="codespan__pre-wrap"><code>## Heading 2</code></span>, <span class="codespan__pre-wrap"><code>### Heading 3</code></span>, <span class="codespan__pre-wrap"><code>#### Heading 4</code></span>, <span class="codespan__pre-wrap"><code>##### Heading 5</code></span>, <span class="codespan__pre-wrap"><code>###### Heading 6</code></span></p>
|
||||
<p>This GIF link should not preview: <span class="codespan__pre-wrap"><code>http://i.giphy.com/xNrM4cGJ8u3ao.gif</code></span>
|
||||
This link should not auto-link: <span class="codespan__pre-wrap"><code>https://en.wikipedia.org/wiki/Dolphin</code></span></p>
|
||||
<p>This sentence with <span class="codespan__pre-wrap"><code>in-line code</code></span> should appear on one line.</p>
|
||||
13
e2e-tests/cypress/tests/fixtures/markdown/markdown_inline_code.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
### In-line Code
|
||||
|
||||
The word `monospace` should render as in-line code.
|
||||
|
||||
The following markdown in-line code should not render:
|
||||
`_Italics_`, `*Italics*`, `**Bold**`, `***Bold-italics***`, `**Bold-italics_**`, `~~Strikethrough~~`, `:)` , `:-)` , `;)` , `:-O` , `:bamboo:` , `:gift_heart:` , `:dolls:` , `# Heading 1`, `## Heading 2`, `### Heading 3`, `#### Heading 4`, `##### Heading 5`, `###### Heading 6`
|
||||
|
||||
This GIF link should not preview: `http://i.giphy.com/xNrM4cGJ8u3ao.gif`
|
||||
This link should not auto-link: `https://en.wikipedia.org/wiki/Dolphin`
|
||||
|
||||
This sentence with `
|
||||
in-line code
|
||||
` should appear on one line.
|
||||
3
e2e-tests/cypress/tests/fixtures/markdown/markdown_inline_images_1.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
### In-line Images
|
||||
|
||||
Mattermost/platform build status: [](https://raw.githubusercontent.com/mattermost/mattermost/master/e2e-tests/cypress/tests/fixtures/mattermost-icon_128x128.png)
|
||||
3
e2e-tests/cypress/tests/fixtures/markdown/markdown_inline_images_2.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
### In-line Images
|
||||
|
||||
GitHub favicon: 
|
||||
4
e2e-tests/cypress/tests/fixtures/markdown/markdown_inline_images_3.md
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
### In-line Images
|
||||
|
||||
GIF Image:
|
||||

|
||||
4
e2e-tests/cypress/tests/fixtures/markdown/markdown_inline_images_4.md
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
### In-line Images
|
||||
|
||||
4K Wallpaper Image (11Mb):
|
||||

|
||||
4
e2e-tests/cypress/tests/fixtures/markdown/markdown_inline_images_5.md
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
### In-line Images
|
||||
|
||||
Panorama Image:
|
||||

|
||||
3
e2e-tests/cypress/tests/fixtures/markdown/markdown_inline_images_6.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
### In-line Images
|
||||
|
||||

|
||||
26
e2e-tests/cypress/tests/fixtures/markdown/markdown_latex.html
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
<div class="post-code"><span class="post-code__language">LaTeX</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7</div><code><span class="hljs-keyword">\documentclass</span>{article}
|
||||
|
||||
<span class="hljs-keyword">\begin</span>{document}
|
||||
|
||||
Hello World!
|
||||
|
||||
<span class="hljs-keyword">\end</span>{document}</code></div></div><p>AND/OR</p>
|
||||
<div class="post-code"><span class="post-code__language">LaTeX</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7</div><code><span class="hljs-keyword">\documentclass</span>{article}
|
||||
|
||||
<span class="hljs-keyword">\begin</span>{document}
|
||||
|
||||
Hello World!
|
||||
|
||||
<span class="hljs-keyword">\end</span>{document}</code></div></div>
|
||||
21
e2e-tests/cypress/tests/fixtures/markdown/markdown_latex.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
```texcode
|
||||
\documentclass{article}
|
||||
|
||||
\begin{document}
|
||||
|
||||
Hello World!
|
||||
|
||||
\end{document}
|
||||
```
|
||||
|
||||
AND/OR
|
||||
|
||||
```latexcode
|
||||
\documentclass{article}
|
||||
|
||||
\begin{document}
|
||||
|
||||
Hello World!
|
||||
|
||||
\end{document}
|
||||
```
|
||||
8
e2e-tests/cypress/tests/fixtures/markdown/markdown_lines.html
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
<h3 class="markdown__heading">Lines</h3><p>Three lines should render with text between them:</p>
|
||||
<p>Text above line</p>
|
||||
<hr>
|
||||
<p>Text between lines</p>
|
||||
<hr>
|
||||
<p>Text between lines</p>
|
||||
<hr>
|
||||
<p>Text below line</p>
|
||||
16
e2e-tests/cypress/tests/fixtures/markdown/markdown_lines.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
### Lines
|
||||
|
||||
Three lines should render with text between them:
|
||||
|
||||
Text above line
|
||||
|
||||
***
|
||||
|
||||
Text between lines
|
||||
|
||||
---
|
||||
|
||||
Text between lines
|
||||
___
|
||||
|
||||
Text below line
|
||||
137
e2e-tests/cypress/tests/fixtures/markdown/markdown_list.html
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
<h1 class="markdown__heading">Markdown List Testing</h1><p>Verify that all list types render as expected.</p>
|
||||
<h3 class="markdown__heading">Single-Item Ordered List</h3><p><strong>Expected:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="hljs"><code>7. Single Item</code></div></div><p><strong>Actual:</strong></p>
|
||||
<ol class="markdown__list" style="counter-reset: list 6;">
|
||||
<li><span>Single Item</span></li></ol><h3 class="markdown__heading">Multi-Item Ordered List</h3><p><strong>Expected:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="hljs"><code>1. One
|
||||
2. Two
|
||||
3. Three</code></div></div><p><strong>Actual:</strong></p>
|
||||
<ol class="markdown__list" style="counter-reset: list 0;">
|
||||
<li><span>One</span></li><li><span>Two</span></li><li><span>Three</span></li></ol><h3 class="markdown__heading">Nested Ordered List</h3><p><strong>Expected:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="hljs"><code>1. Alpha
|
||||
1. Bravo
|
||||
2. Charlie
|
||||
3. Delta
|
||||
1. Echo
|
||||
2. Foxtrot</code></div></div><p><strong>Actual:</strong></p>
|
||||
<ol class="markdown__list" style="counter-reset: list 0;">
|
||||
<li><span>Alpha<ol class="markdown__list" style="counter-reset: list 0;">
|
||||
<li><span>Bravo</span></li></ol></span></li><li><span>Charlie</span></li><li><span>Delta<ol class="markdown__list" style="counter-reset: list 0;">
|
||||
<li><span>Echo</span></li><li><span>Foxtrot</span></li></ol></span></li></ol><h3 class="markdown__heading">Single-Item Unordered List</h3><p><strong>Expected:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="hljs"><code>• Single Item</code></div></div><p><strong>Actual:</strong></p>
|
||||
<ul class="markdown__list">
|
||||
<li><span>Single Item</span></li></ul><h3 class="markdown__heading">Multi-Item Unordered List</h3><p><strong>Expected:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="hljs"><code>• One
|
||||
• Two
|
||||
• Three</code></div></div><p><strong>Actual:</strong></p>
|
||||
<ul class="markdown__list">
|
||||
<li><span>One</span></li><li><span>Two</span></li><li><span>Three</span></li></ul><h3 class="markdown__heading">Multi-Item Unordered List with Line Break (Break should not render)</h3><p><strong>Expected:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="hljs"><code>• Item A
|
||||
• Item B
|
||||
• Item C
|
||||
• Item D</code></div></div><p><strong>Actual:</strong></p>
|
||||
<ul class="markdown__list">
|
||||
<li><span>Item A</span></li><li><span><p>Item B</p>
|
||||
</span></li><li><span><p>Item C</p>
|
||||
</span></li><li><span>Item D</span></li></ul><h3 class="markdown__heading">Nested Unordered List</h3><p><strong>Expected:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="hljs"><code>• Alpha
|
||||
• Bravo
|
||||
• Charlie
|
||||
• Delta
|
||||
• Echo
|
||||
• Foxtrot</code></div></div><p><strong>Actual:</strong></p>
|
||||
<ul class="markdown__list">
|
||||
<li><span>Alpha<ul class="markdown__list">
|
||||
<li><span>Bravo</span></li></ul></span></li><li><span>Charlie</span></li><li><span>Delta<ul class="markdown__list">
|
||||
<li><span>Echo</span></li><li><span>Foxtrot</span></li></ul></span></li></ul><h3 class="markdown__heading">Mixed List Starting Ordered</h3><p><strong>Expected:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="hljs"><code>1. One
|
||||
2. Two
|
||||
3. Three</code></div></div><p><strong>Actual:</strong></p>
|
||||
<ol class="markdown__list" style="counter-reset: list 0;">
|
||||
<li><span>One</span></li><li><span>Two</span></li><li><span>Three</span></li></ol><h3 class="markdown__heading">Mixed List Starting Unordered</h3><p><strong>Expected:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="hljs"><code>• Monday
|
||||
• Tuesday
|
||||
• Wednesday</code></div></div><p><strong>Actual:</strong></p>
|
||||
<ul class="markdown__list">
|
||||
<li><span>Monday</span></li><li><span>Tuesday</span></li><li><span>Wednesday</span></li></ul><h3 class="markdown__heading">Nested Mixed List</h3><p><strong>Expected:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="hljs"><code>• Alpha
|
||||
1. Bravo
|
||||
• Charlie
|
||||
• Delta
|
||||
• Echo
|
||||
• Foxtrot
|
||||
• Golf
|
||||
1. Hotel
|
||||
• India
|
||||
1. Juliet
|
||||
2. Kilo
|
||||
• Lima
|
||||
• Mike
|
||||
1. November
|
||||
4. Oscar
|
||||
5. Papa</code></div></div><p><strong>Actual:</strong></p>
|
||||
<ul class="markdown__list">
|
||||
<li><span>Alpha<ol class="markdown__list" style="counter-reset: list 0;">
|
||||
<li><span>Bravo<ul class="markdown__list">
|
||||
<li><span>Charlie</span></li><li><span>Delta</span></li></ul></span></li></ol></span></li><li><span>Echo</span></li><li><span>Foxtrot<ul class="markdown__list">
|
||||
<li><span>Golf<ol class="markdown__list" style="counter-reset: list 0;">
|
||||
<li><span>Hotel</span></li></ol></span></li><li><span>India<ol class="markdown__list" style="counter-reset: list 0;">
|
||||
<li><span>Juliet</span></li><li><span>Kilo</span></li></ol></span></li><li><span>Lima</span></li></ul></span></li><li><span>Mike<ol class="markdown__list" style="counter-reset: list 0;">
|
||||
<li><span>November<ol class="markdown__list" style="counter-reset: list 3;">
|
||||
<li><span>Oscar<ol class="markdown__list" style="counter-reset: list 4;">
|
||||
<li><span>Papa</span></li></ol></span></li></ol></span></li></ol></span></li></ul><h3 class="markdown__heading">Ordered Lists Separated by Carriage Returns</h3><p><strong>Expected:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="hljs"><code>1. One
|
||||
• Two
|
||||
2. Two
|
||||
3. Three</code></div></div><p><strong>Actual:</strong></p>
|
||||
<ol class="markdown__list" style="counter-reset: list 0;">
|
||||
<li><span><p>One</p>
|
||||
<ul class="markdown__list">
|
||||
<li><span>Two</span></li></ul></span></li><li><span><p>Two</p>
|
||||
</span></li><li><span>Three</span></li></ol><h3 class="markdown__heading">New Line After a List</h3><p><strong>Expected:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="hljs"><code>1. One
|
||||
2. Two
|
||||
|
||||
This text should be on a new line.</code></div></div><p><strong>Actual:</strong></p>
|
||||
<ol class="markdown__list" style="counter-reset: list 0;">
|
||||
<li><span>One</span></li><li><span>Two</span></li></ol><p>This text should be on a new line.</p>
|
||||
<h3 class="markdown__heading">Task Lists</h3><p><strong>Expected:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="hljs"><code>[ ] One
|
||||
[ ] Subpoint one
|
||||
- Normal Bullet
|
||||
[ ] Two
|
||||
[x] Completed item</code></div></div><p><strong>Actual:</strong></p>
|
||||
<ul class="markdown__list">
|
||||
<li class="list-item--task-list"><input type="checkbox" disabled=""> One<ul class="markdown__list">
|
||||
<li class="list-item--task-list"><input type="checkbox" disabled=""> Subpoint one</li><li><span>Normal Bullet</span></li></ul></li><li class="list-item--task-list"><input type="checkbox" disabled=""> Two</li><li class="list-item--task-list"><input type="checkbox" disabled="" checked=""> Completed item</li></ul><h3 class="markdown__heading">Numbered Task Lists</h3><p><strong>Expected:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="hljs"><code>1. [ ] One
|
||||
2. [ ] Two
|
||||
3. [x] Completed item</code></div></div><p><strong>Actual:</strong></p>
|
||||
<ol class="markdown__list" style="counter-reset: list 0;">
|
||||
<li class="list-item--task-list"><input type="checkbox" disabled=""> One</li><li class="list-item--task-list"><input type="checkbox" disabled=""> Two</li><li class="list-item--task-list"><input type="checkbox" disabled="" checked=""> Completed item</li></ol><h3 class="markdown__heading">Multiple Lists</h3><p><strong>Expected:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="hljs"><code>List A:
|
||||
|
||||
1. One
|
||||
|
||||
List B:
|
||||
|
||||
2. Two</code></div></div><p>List A:</p>
|
||||
<ol class="markdown__list" style="counter-reset: list 0;">
|
||||
<li><span>One</span></li></ol><p>List B:</p>
|
||||
<ol class="markdown__list" style="counter-reset: list 1;">
|
||||
<li><span>Two</span></li></ol><h3 class="markdown__heading">Lists with blank lines before and after</h3><p><strong>Expected:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="hljs"><code>Line with blank line after
|
||||
|
||||
Line with blank line after and before
|
||||
|
||||
1. Bullet
|
||||
2. Bullet
|
||||
3. Bullet
|
||||
|
||||
Line with blank line after and before
|
||||
|
||||
Line with blank line before</code></div></div><p>Line with blank line after </p>
|
||||
<p>Line with blank line after and before </p>
|
||||
<ol class="markdown__list" style="counter-reset: list 0;">
|
||||
<li><span>Bullet </span></li><li><span>Bullet </span></li><li><span>Bullet </span></li></ol><p>Line with blank line after and before </p>
|
||||
<p>Line with blank line before</p>
|
||||
3
e2e-tests/cypress/tests/fixtures/markdown/markdown_not_autolink.html
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<p><strong>The following links should not auto-link or generate previews:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="post-code__overlay"><span class="post-code__clipboard"><i role="button" class="icon icon-content-copy"></i></span></div><div class="hljs"><code>GIF: http://i.giphy.com/xNrM4cGJ8u3ao.gif
|
||||
Website: https://en.wikipedia.org/wiki/Dolphin</code></div></div>
|
||||
5
e2e-tests/cypress/tests/fixtures/markdown/markdown_not_autolink.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
**The following links should not auto-link or generate previews:**
|
||||
```
|
||||
GIF: http://i.giphy.com/xNrM4cGJ8u3ao.gif
|
||||
Website: https://en.wikipedia.org/wiki/Dolphin
|
||||
```
|
||||
23
e2e-tests/cypress/tests/fixtures/markdown/markdown_not_in_code_block.html
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
<p><strong>The following markdown should not render:</strong></p>
|
||||
<div class="post-code post-code--wrap"><div class="post-code__overlay"><span class="post-code__clipboard"><i role="button" class="icon icon-content-copy"></i></span></div><div class="hljs"><code>_Italics_
|
||||
*Italics*
|
||||
**Bold**
|
||||
***Bold-italics***
|
||||
**Bold-italics_**
|
||||
~~Strikethrough~~
|
||||
:) :-) ;) ;-) :o :O :-o :-O
|
||||
:bamboo: :gift_heart: :dolls: :school_satchel: :mortar_board:
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
> Block Quote
|
||||
- List
|
||||
- List Sub-item
|
||||
[Link](http://i.giphy.com/xNrM4cGJ8u3ao.gif)
|
||||
[](https://github.com/mattermost/platform)
|
||||
| Left-Aligned Text | Center Aligned Text | Right Aligned Text |
|
||||
| :------------ |:---------------:| -----:|
|
||||
| Left column 1 | this text | $100 |</code></div></div>
|
||||
25
e2e-tests/cypress/tests/fixtures/markdown/markdown_not_in_code_block.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
**The following markdown should not render:**
|
||||
```
|
||||
_Italics_
|
||||
*Italics*
|
||||
**Bold**
|
||||
***Bold-italics***
|
||||
**Bold-italics_**
|
||||
~~Strikethrough~~
|
||||
:) :-) ;) ;-) :o :O :-o :-O
|
||||
:bamboo: :gift_heart: :dolls: :school_satchel: :mortar_board:
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
> Block Quote
|
||||
- List
|
||||
- List Sub-item
|
||||
[Link](http://i.giphy.com/xNrM4cGJ8u3ao.gif)
|
||||
[](https://github.com/mattermost/platform)
|
||||
| Left-Aligned Text | Center Aligned Text | Right Aligned Text |
|
||||
| :------------ |:---------------:| -----:|
|
||||
| Left column 1 | this text | $100 |
|
||||
```
|
||||
50
e2e-tests/cypress/tests/fixtures/markdown/markdown_postgres.html
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
<div class="post-code"><span class="post-code__language">PostgreSQL</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7</div><code><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR REPLACE</span> <span class="hljs-keyword">FUNCTION</span> snitch() <span class="hljs-keyword">RETURNS</span> <span class="hljs-type">event_trigger</span> <span class="hljs-keyword">AS</span> $$<span class="pgsql">
|
||||
<span class="hljs-keyword">BEGIN</span>
|
||||
<span class="hljs-keyword">RAISE</span> <span class="hljs-keyword">NOTICE</span> <span class="hljs-string">'snitch: % %'</span>, <span class="hljs-built_in">tg_event</span>, <span class="hljs-built_in">tg_tag</span>;
|
||||
<span class="hljs-keyword">END</span>;
|
||||
$$</span> <span class="hljs-keyword">LANGUAGE</span> plpgsql;
|
||||
|
||||
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">EVENT TRIGGER</span> snitch <span class="hljs-keyword">ON</span> ddl_command_start <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">PROCEDURE</span> snitch();</code></div></div><p>and</p>
|
||||
<div class="post-code"><span class="post-code__language">PostgreSQL</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7</div><code><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR REPLACE</span> <span class="hljs-keyword">FUNCTION</span> snitch() <span class="hljs-keyword">RETURNS</span> <span class="hljs-type">event_trigger</span> <span class="hljs-keyword">AS</span> $$<span class="pgsql">
|
||||
<span class="hljs-keyword">BEGIN</span>
|
||||
<span class="hljs-keyword">RAISE</span> <span class="hljs-keyword">NOTICE</span> <span class="hljs-string">'snitch: % %'</span>, <span class="hljs-built_in">tg_event</span>, <span class="hljs-built_in">tg_tag</span>;
|
||||
<span class="hljs-keyword">END</span>;
|
||||
$$</span> <span class="hljs-keyword">LANGUAGE</span> plpgsql;
|
||||
|
||||
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">EVENT TRIGGER</span> snitch <span class="hljs-keyword">ON</span> ddl_command_start <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">PROCEDURE</span> snitch();</code></div></div><p>and</p>
|
||||
<div class="post-code"><span class="post-code__language">PostgreSQL</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7</div><code><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR REPLACE</span> <span class="hljs-keyword">FUNCTION</span> snitch() <span class="hljs-keyword">RETURNS</span> <span class="hljs-type">event_trigger</span> <span class="hljs-keyword">AS</span> $$<span class="pgsql">
|
||||
<span class="hljs-keyword">BEGIN</span>
|
||||
<span class="hljs-keyword">RAISE</span> <span class="hljs-keyword">NOTICE</span> <span class="hljs-string">'snitch: % %'</span>, <span class="hljs-built_in">tg_event</span>, <span class="hljs-built_in">tg_tag</span>;
|
||||
<span class="hljs-keyword">END</span>;
|
||||
$$</span> <span class="hljs-keyword">LANGUAGE</span> plpgsql;
|
||||
|
||||
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">EVENT TRIGGER</span> snitch <span class="hljs-keyword">ON</span> ddl_command_start <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">PROCEDURE</span> snitch();</code></div></div><p>or</p>
|
||||
<div class="post-code"><span class="post-code__language">PostgreSQL</span><div class="hljs"><div class="post-code__line-numbers">1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6</div><code>CREATE OR REPLACE FUNCTION add(x int, y int)
|
||||
RETURNS int
|
||||
LANGUAGE SQL
|
||||
AS $myfunc$
|
||||
SELECT x + y
|
||||
$myfunc$</code></div></div>
|
||||
41
e2e-tests/cypress/tests/fixtures/markdown/markdown_postgres.md
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
```postgres
|
||||
CREATE OR REPLACE FUNCTION snitch() RETURNS event_trigger AS $$
|
||||
BEGIN
|
||||
RAISE NOTICE 'snitch: % %', tg_event, tg_tag;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE EVENT TRIGGER snitch ON ddl_command_start EXECUTE PROCEDURE snitch();
|
||||
```
|
||||
and
|
||||
|
||||
```pgsql
|
||||
CREATE OR REPLACE FUNCTION snitch() RETURNS event_trigger AS $$
|
||||
BEGIN
|
||||
RAISE NOTICE 'snitch: % %', tg_event, tg_tag;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE EVENT TRIGGER snitch ON ddl_command_start EXECUTE PROCEDURE snitch();
|
||||
```
|
||||
and
|
||||
|
||||
```postgresql
|
||||
CREATE OR REPLACE FUNCTION snitch() RETURNS event_trigger AS $$
|
||||
BEGIN
|
||||
RAISE NOTICE 'snitch: % %', tg_event, tg_tag;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE EVENT TRIGGER snitch ON ddl_command_start EXECUTE PROCEDURE snitch();
|
||||
```
|
||||
or
|
||||
|
||||
```pgsql
|
||||
CREATE OR REPLACE FUNCTION add(x int, y int)
|
||||
RETURNS int
|
||||
LANGUAGE SQL
|
||||
AS $myfunc$
|
||||
SELECT x + y
|
||||
$myfunc$
|
||||
```
|
||||