Merge remote-tracking branch 'origin/master' into plugin-metrics

This commit is contained in:
streamer45 2023-10-06 09:51:15 -06:00
commit 593ce26f37
No known key found for this signature in database
GPG Key ID: C31222BC9269BBBC
3503 changed files with 149982 additions and 55143 deletions

View File

@ -8,7 +8,7 @@ insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
[*.go]
[*.{go,sql}]
indent_style = tab
[*.{js,jsx,json,html,ts,tsx}]
@ -21,12 +21,21 @@ indent_size = 2
[i18n/**.json]
indent_size = 2
[Makefile]
[Makefile,*.mk]
indent_style = tab
[*.scss]
indent_style = space
indent_size = 4
[*.md]
indent_style = space
indent_size = 4
trim_trailing_whitespace = false
[.npm-upgrade.json]
insert_final_newline = false
[*.yml,*.yaml]
indent_style = space
indent_size = 2

View File

@ -8,10 +8,6 @@ on:
type: string
required: false
defaults:
run:
shell: bash
jobs:
update-initial-status:
runs-on: ubuntu-22.04
@ -84,25 +80,46 @@ jobs:
npm run check
smoketests:
runs-on: ubuntu-latest-8-cores
strategy:
matrix:
#
# Note that smoketests should be run only on ubuntu, for QA purposes.
# But it's useful to be able to run and debug the smoketests for different OSes.
# Notes:
# - For MacOS: works on developer machines, but uses too many resources to be able to run on Github Actions
# - for Windows: cannot currently run on Github Actions, since the runners do not support running linux containers, at the moment
#
#os: [ubuntu-latest-8-cores, windows-2022, macos-12-xl]
os: [ubuntu-latest-8-cores]
runs-on: "${{ matrix.os }}"
needs:
- cypress-check
- playwright-check
defaults:
run:
shell: bash
working-directory: e2e-tests
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
with:
ref: ${{ inputs.commit_sha || github.sha }}
- name: ci/setup-macos-docker
if: runner.os == 'macos'
# https://github.com/actions/runner-images/issues/17#issuecomment-1537238473
run: |
brew install docker docker-compose
colima start
mkdir -p ~/.docker/cli-plugins
ln -sfn /usr/local/opt/docker-compose/bin/docker-compose ~/.docker/cli-plugins/docker-compose
sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock
- name: ci/e2e-smoketests
run: |
make
- name: ci/e2e-smoketests-store-results
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: e2e-smoketests-results
name: e2e-smoketests-results-${{ matrix.os }}
path: |
e2e-tests/cypress/logs/
e2e-tests/cypress/results/

54
.github/workflows/migration.yml vendored Normal file
View File

@ -0,0 +1,54 @@
name: Database Migration Test
on:
workflow_call:
jobs:
test:
name: MySQL -> Postgres Migration
runs-on: ubuntu-22.04
env:
COMPOSE_PROJECT_NAME: ghactions
BUILD_IMAGE: mattermost/mattermost-build-server:20230904_golang-1.20.7
defaults:
run:
working-directory: server
steps:
- name: Checkout mattermost project
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: Run docker compose
run: |
cd build
docker-compose --ansi never run --rm start_dependencies
docker-compose --ansi never exec -T minio sh -c 'mkdir -p /data/mattermost-test';
docker-compose --ansi never ps
- name: Generate test-data
run: |
docker run --net ${COMPOSE_PROJECT_NAME}_mm-test \
--ulimit nofile=8096:8096 \
--env-file=build/dotenv/migration.env \
-v $(go env GOCACHE):/go/cache \
-e GOCACHE=/go/cache \
-v $PWD:/mattermost \
-w /mattermost \
$BUILD_IMAGE \
make test-data
- name: Migrate the DB and compare
run: |
docker run --net ${COMPOSE_PROJECT_NAME}_mm-test \
--ulimit nofile=8096:8096 \
--env-file=build/dotenv/migration.env \
-v $(go env GOCACHE):/go/cache \
-e GOCACHE=/go/cache \
-v $PWD:/mattermost \
-w /mattermost \
$BUILD_IMAGE \
make test-migration
- name: Upload artifacts
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: Migration logs
path: server/migration.log
retention-days: 7
- name: Stop docker compose
run: |
cd build
docker-compose --ansi never stop

View File

@ -15,14 +15,14 @@ on:
required: true
type: string
env:
go-version: "1.19.5"
go-version: "1.20.7"
jobs:
test:
name: ${{ inputs.name }}
runs-on: ubuntu-latest-8-cores
env:
COMPOSE_PROJECT_NAME: ghactions
BUILD_IMAGE: mattermost/mattermost-build-server:20230118_golang-1.19.5
BUILD_IMAGE: mattermost/mattermost-build-server:20230904_golang-1.20.7
steps:
- name: Checkout mattermost project
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
@ -30,10 +30,15 @@ jobs:
uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
with:
go-version: ${{ env.go-version }}
cache-dependency-path: server/go.sum
- name: Run setup-go-work
run: |
cd server
make setup-go-work
- name: Setup needed prepackaged plugins
run: |
cd server
make prepackaged-plugins PLUGIN_PACKAGES=mattermost-plugin-jira-v3.2.5
- name: Run docker compose
run: |
cd server/build
@ -43,6 +48,9 @@ jobs:
docker-compose --ansi never ps
- name: Run mmctl Tests
run: |
if [[ ${{ github.ref_name }} == 'master' ]]; then
export MMCTL_TESTFLAGS="-timeout 30m -race"
fi
docker run --net ghactions_mm-test \
--ulimit nofile=8096:8096 \
--env-file=server/build/dotenv/test.env \
@ -52,7 +60,7 @@ jobs:
-v $PWD:/mattermost \
-w /mattermost/server \
$BUILD_IMAGE \
make test-mmctl-coverage BUILD_NUMBER=$GITHUB_HEAD_REF-$GITHUB_RUN_ID
make test-mmctl BUILD_NUMBER=$GITHUB_HEAD_REF-$GITHUB_RUN_ID
- name: Stop docker compose
run: |
cd server/build

View File

@ -3,45 +3,52 @@ on:
# Only the default branch is supported.
branch_protection_rule:
schedule:
- cron: '44 6 * * *'
- cron: "44 6 * * *"
# Declare default permissions as read only.
permissions: read-all
jobs:
analysis:
name: Scorecards analysis
name: Scorecard analysis
if: github.repository_owner == 'mattermost'
runs-on: ubuntu-latest
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
actions: read
contents: read
# Needed to publish results and get a badge (see publish_results below).
id-token: write
steps:
- name: "Checkout code"
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@c1aec4ac820532bab364f02a81873c555a0ba3a1 # v1.0.4
uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v2.1.2
with:
results_file: results.sarif
results_format: sarif
# Read-only PAT token. To create it,
# follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation.
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
# - you want to enable the Branch-Protection check on a *public* repository, or
# - you are installing Scorecard on a *private* repository
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
repo_token: ${{ secrets.SCORECARD_READ_TOKEN }}
# Publish the results to enable scorecard badges. For more details, see
# https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories, `publish_results` will automatically be set to `false`,
# regardless of the value entered here.
# Public repositories:
# - Publish results to OpenSSF REST API for easy access by consumers
# - Allows the repository to include the Scorecard badge.
# - See https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories:
# - `publish_results` will always be set to `false`, regardless
# of the value entered here.
publish_results: true
# Upload the results as artifacts (optional).
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@82c141cc518b40d92cc801eee768e7aafc9c2fa2 # v2.3.1
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0
with:
name: SARIF file
path: results.sarif
@ -49,6 +56,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5 # v1.0.26
uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4
with:
sarif_file: results.sarif

View File

@ -7,7 +7,7 @@ on:
- release-*
- mono-repo*
env:
go-version: "1.19.5"
go-version: "1.20.7"
jobs:
master-ci:

View File

@ -6,7 +6,7 @@ on:
- "e2e-tests/**"
- ".github/**"
env:
go-version: "1.19.5"
go-version: "1.20.7"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

View File

@ -6,7 +6,7 @@ on:
workflow_call:
env:
go-version: "1.19.5"
go-version: "1.20.7"
jobs:
check-mocks:
@ -297,3 +297,7 @@ jobs:
name: server-build-artifact
path: server/build/
retention-days: 14
test-migration:
name: MySQL to PostgreSQL Migration
uses: ./.github/workflows/migration.yml
secrets: inherit

View File

@ -15,14 +15,14 @@ on:
required: true
type: string
env:
go-version: "1.19.5"
go-version: "1.20.7"
jobs:
test:
name: ${{ inputs.name }}
runs-on: ubuntu-latest-8-cores
env:
COMPOSE_PROJECT_NAME: ghactions
BUILD_IMAGE: mattermost/mattermost-build-server:20230118_golang-1.19.5
BUILD_IMAGE: mattermost/mattermost-build-server:20230904_golang-1.20.7
steps:
- name: Checkout mattermost project
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
@ -76,12 +76,25 @@ jobs:
report_paths: server/report.xml
check_name: ${{ inputs.name }} (Results)
require_tests: true
- name: Report retried tests via webhook
if: ${{ steps.report.outputs.retried > 0 }}
- name: Report retried tests via webhook (master || release-*)
if: ${{ steps.report.outputs.retried > 0 && (github.ref_name == 'master' || startsWith(github.ref_name, 'release-')) }}
run: |
curl \
--fail \
-X POST \
-H "Content-Type: application/json" \
-d "{\"text\":\"#### ⚠️ One or more flaky tests detected ⚠️\\n* Failing job: [github.com/mattermost/mattermost:${{ inputs.name }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\\n* If this is your pull request, double check your code to ensure you haven't introduced a flaky test.\\n* If this occurred on master or a release branch, reply to this message if you're willing to help.\\n* Submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.\\n* Finally, reply to this message with a link to the created JIRA ticket.\\ncc @devs\"}" \
-d "{\"text\":\"#### ⚠️ One or more flaky tests detected ⚠️\\n* Failing job: [github.com/mattermost/mattermost:${{ inputs.name }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\\n* Ideally, this would have been caught in a pull request, but now a volunteer is required. If you're willing to help, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.\\n* Finally, reply to this message with a link to the created JIRA ticket.\\n\"}" \
${{ secrets.MM_COMMUNITY_DEVELOPERS_INCOMING_WEBHOOK_FROM_GH_ACTIONS }}
- name: Report retried tests (pull request)
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
if: ${{ steps.report.outputs.retried > 0 && !(github.ref_name == 'master' || startsWith(github.ref_name, 'release-')) }}
with:
script: |
const body = `#### ⚠️ One or more flaky tests detected ⚠️\n* Failing job: [github.com/mattermost/mattermost:${{ inputs.name }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\n* Double check your code to ensure you haven't introduced a flaky test.\n* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.`
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
})

3
.gitignore vendored
View File

@ -44,6 +44,9 @@ e2e-tests/playwright/tests/**/*-window.png
e2e-tests/playwright/tests/accessibility/**/*-snapshots
e2e-tests/playwright/.eslintcache
# ignore temporary added configuration for pgloader
server/tests/temp.load
# Enterprise & products imports files
imports/imports.go

View File

@ -12,7 +12,7 @@ include:
variables:
BUILD: "yes"
IMAGE_BUILD_SERVER: $CI_REGISTRY/mattermost/ci/images/mattermost-build-server:20230118_golang-1.19.5
IMAGE_BUILD_SERVER: $CI_REGISTRY/mattermost/ci/images/mattermost-build-server:20230904_golang-1.20.7
IMAGE_BUILD_DOCKER: $CI_REGISTRY/mattermost/ci/devops-images/mattermost-build-docker:23.0.1-0
IMAGE_DIND: $CI_REGISTRY/mattermost/ci/devops-images/docker-dind:23.0.1-0

2
.nvmrc
View File

@ -1 +1 @@
16.10.0
18.17

View File

@ -41,7 +41,6 @@ build-v4: node_modules playbooks
@cat $(V4_SRC)/schemes.yaml >> $(V4_YAML)
@cat $(V4_SRC)/service_terms.yaml >> $(V4_YAML)
@cat $(V4_SRC)/sharedchannels.yaml >> $(V4_YAML)
@cat $(V4_SRC)/opengraph.yaml >> $(V4_YAML)
@cat $(V4_SRC)/reactions.yaml >> $(V4_YAML)
@cat $(V4_SRC)/actions.yaml >> $(V4_YAML)
@cat $(V4_SRC)/bots.yaml >> $(V4_YAML)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1,38 +0,0 @@
/api/v4/opengraph:
post:
tags:
- OpenGraph
summary: Get open graph metadata for url
description: >
Get Open Graph Metadata for a specif URL. Use the Open Graph protocol to
get some generic metadata about a URL. Used for creating link previews.
__Minimum server version__: 3.10
##### Permissions
No permission required but must be logged in.
operationId: OpenGraph
requestBody:
content:
application/json:
schema:
type: object
required:
- url
properties:
url:
type: string
description: The URL to get Open Graph Metadata.
required: true
responses:
"200":
description: Open Graph retrieval successful
content:
application/json:
schema:
$ref: "#/components/schemas/OpenGraph"
"501":
$ref: "#/components/responses/NotImplemented"

View File

@ -3,10 +3,23 @@
export MME2E_DC_SERVER="docker compose -p mmserver -f $(readlink -f ../../server/build/gitlab-dc.postgres.yml) -f $(readlink -f ./server.override.yml)"
export MME2E_DC_DASHBOARD="docker compose -p mmdashboard -f $(readlink -f ./dashboard/docker/docker-compose.yml) -f $(readlink -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)
# Default the optional variables that are used in the docker-compose file
# Default values for optional variables
export SERVER_IMAGE_DEFAULT="mattermostdevelopment/mm-ee-test:$(git rev-parse --short=7 HEAD)"
export BROWSER_DEFAULT="chrome"
case $MME2E_OSTYPE in
# OS specific defaults overrides
darwin )
BROWSER_DEFAULT="electron" ;;
* )
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}
# Function definitions
mme2e_log () { echo "[$(date +%Y-%m-%dT%H:%M:%S%Z)]" "$@"; }
@ -69,7 +82,14 @@ mme2e_legacy_setup () {
# These functions are needed until every pipeline depending on the `server/build/gitlab-dc.*.yml` files is adapted to not use external docker networking anymore
# After that is fixed, this function and the external network in the docker-compose files may be removed
export COMPOSE_PROJECT_NAME=mmserver_legacy
docker network inspect ${COMPOSE_PROJECT_NAME} >/dev/null 2>&1 || docker network create ${COMPOSE_PROJECT_NAME}
case $MME2E_OSTYPE in
windows )
# https://learn.microsoft.com/en-us/virtualization/windowscontainers/container-networking/network-drivers-topologies
DOCKER_NETWORK_DRIVER="nat" ;;
* )
DOCKER_NETWORK_DRIVER="bridge" ;;
esac
docker network inspect ${COMPOSE_PROJECT_NAME} >/dev/null 2>&1 || docker network create --driver=${DOCKER_NETWORK_DRIVER} ${COMPOSE_PROJECT_NAME}
}
# Utility alias, for interactive shell usage. Can be reversed with 'unalias docker-compose' in your shell

View File

@ -1,24 +1,23 @@
---
version: '3.1'
version: "3.1"
services:
dashboard:
image: mattermostdevelopment/mirrored-node:16.17.0
environment:
PG_URI: postgres://mmuser:mostest@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
user: "${MME2E_UID}"
volumes:
- "../:/app"
- "../../dashboard.entrypoint.sh:/usr/local/bin/dashboard.entrypoint.sh:ro"
- "../../.env.dashboard:/var/local/.env.dashboard:rw"
working_dir: /app
entrypoint: /usr/local/bin/dashboard.entrypoint.sh
ports:
- 4000:4000
dashboard:
image: mattermostdevelopment/mirrored-node:18.17
environment:
PG_URI: postgres://mmuser:mostest@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
volumes:
- "../:/app"
- "../../dashboard.entrypoint.sh:/usr/local/bin/dashboard.entrypoint.sh:ro"
- "../../.env.dashboard:/var/local/.env.dashboard:rw"
working_dir: /app
entrypoint: /usr/local/bin/dashboard.entrypoint.sh
ports:
- 4000:4000

View File

@ -18,13 +18,18 @@ mme2e_log "Starting the dashboard"
${MME2E_DC_DASHBOARD} up -d db dashboard
mme2e_log "Generating the dashboard's local URL"
MME2E_DC_DASHBOARD_NETWORK=$(${MME2E_DC_DASHBOARD} ps -q dashboard | xargs -l docker inspect | jq -r '.[0].NetworkSettings.Networks | (keys|.[0])')
MME2E_DC_DASHBOARD_GATEWAY=$(docker network inspect $MME2E_DC_DASHBOARD_NETWORK | jq -r '.[0].IPAM.Config[0].Gateway')
AUTOMATION_DASHBOARD_URL="http://${MME2E_DC_DASHBOARD_GATEWAY}:4000/api"
case $MME2E_OSTYPE in
darwin )
AUTOMATION_DASHBOARD_IP=$(ifconfig $(route get 1.1.1.1 | grep interface | awk '{print $2}') | grep -w inet | awk '{print $2}') ;;
* )
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
AUTOMATION_DASHBOARD_URL="http://${AUTOMATION_DASHBOARD_IP}:4000/api"
mme2e_log "Generating a signed JWT token for accessing the dashboard"
${MME2E_DC_DASHBOARD} exec -T -u $MME2E_UID dashboard npm i
AUTOMATION_DASHBOARD_TOKEN=$(${MME2E_DC_DASHBOARD} exec -T -u $MME2E_UID dashboard node script/sign.js | awk '{ print $2; }') # The token secret is specified in the dashboard.override.yml file
${MME2E_DC_DASHBOARD} exec -T dashboard npm i
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

View File

@ -1,9 +1,8 @@
---
# Image hashes in this file are for amd64 systems
# NB: paths relative to the `server/build` directory, which contains the original compose file that this yaml is overriding
version: '2.4'
version: "2.4"
services:
server:
image: "${SERVER_IMAGE}"
restart: always
@ -28,7 +27,7 @@ services:
MM_FEATUREFLAGS_ONBOARDINGTOURTIPS: "false"
MM_SERVICEENVIRONMENT: "test"
volumes:
- "server-config:/mattermost/config"
- "server-config:/mattermost/config"
ports:
- "8065:8065"
depends_on:
@ -54,12 +53,12 @@ services:
condition: service_healthy
webhook-interactions:
image: mattermostdevelopment/mirrored-node:16.10.0
image: mattermostdevelopment/mirrored-node:${NODE_VERSION_REQUIRED}
command: sh -c "npm install -g axios express client-oauth2@larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49 && exec node webhook_serve.js"
environment:
NODE_PATH: /usr/local/lib/node_modules/
healthcheck:
test: [ "CMD", "curl", "-s", "-o/dev/null", "127.0.0.1:3000" ]
test: ["CMD", "curl", "-s", "-o/dev/null", "127.0.0.1:3000"]
interval: 10s
timeout: 15s
retries: 12
@ -72,16 +71,17 @@ services:
- webhook-interactions
cypress:
image: "mattermostdevelopment/mirrored-cypress-browsers-public:node16.14.2-slim-chrome100-ff99-edge"
entrypoint: [ "/bin/bash", "-c" ]
command: [ "until [ -f /var/run/mm_terminate ]; do sleep 5; done" ]
image: "cypress/browsers:node-18.16.1-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1"
### Temorarily disabling this image, until both the amd64 and arm64 version are mirrored
# image: "mattermostdevelopment/mirrored-cypress-browsers-public:node-18.16.1-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1"
entrypoint: ["/bin/bash", "-c"]
command: ["until [ -f /var/run/mm_terminate ]; do sleep 5; done"]
env_file:
- "../../e2e-tests/.ci/.env.dashboard"
- "../../e2e-tests/.ci/.env.cypress"
environment:
REPO: "mattermost-server"
# Cypress configuration
BROWSER: "chrome"
HEADLESS: "true"
CYPRESS_baseUrl: "http://server:8065"
CYPRESS_dbConnection: "postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable&connect_timeout=10"
@ -107,25 +107,25 @@ services:
# https://github.com/cypress-io/cypress/issues/1243
CI: "1"
# Ensure we're independent from the global node environment
PATH: /app/node_modules/.bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PATH: /cypress/node_modules/.bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ulimits:
nofile:
soft: 8096
hard: 1048576
working_dir: /app
working_dir: /cypress
volumes:
- "../../e2e-tests/cypress/:/app"
- "../../e2e-tests/cypress/:/cypress"
utils:
image: "mattermostdevelopment/mirrored-golang:1.19.5"
entrypoint: [ "/bin/bash", "-c" ]
command: [ "until [ -f /var/run/mm_terminate ]; do sleep 5; done" ]
image: "mattermost/mattermost-build-server:20230904_golang-1.20.7"
entrypoint: ["/bin/bash", "-c"]
command: ["until [ -f /var/run/mm_terminate ]; do sleep 5; done"]
environment:
MM_SERVICEENVIRONMENT: "test" # Currently required for running the config_generator with the right environment
working_dir: "/opt/mattermost-server"
volumes:
- "../../:/opt/mattermost-server"
- "server-config:/opt/server-config"
- "../../:/opt/mattermost-server"
- "server-config:/opt/server-config"
volumes:
server-config:

View File

@ -29,6 +29,7 @@ mme2e_generate_envfile_from_var_names >.env.cypress <<EOF
BRANCH
BUILD_ID
CI_BASE_URL
BROWSER
AUTOMATION_DASHBOARD_URL
AUTOMATION_DASHBOARD_TOKEN
EOF
@ -42,7 +43,6 @@ mme2e_log "Creating E2E containers and generating server config"
${MME2E_DC_SERVER} create
${MME2E_DC_SERVER} up -d -- utils
${MME2E_DC_SERVER} exec -T -w /opt/mattermost-server/server -- utils bash <<EOF
apt update && apt install -y jq
OUTPUT_CONFIG=/tmp/config_generated.json go run scripts/config_generator/main.go
jq "
.ServiceSettings.SiteURL=\"http://server:8065\"

View File

@ -7,20 +7,20 @@ stop: stop-server stop-dashboard clean-env-placeholders
.PHONY: start-server prepare-server run-cypress stop-server restart-server
start-server:
./.ci/server.start.sh
bash ./.ci/server.start.sh
prepare-server:
./.ci/server.prepare.sh
bash ./.ci/server.prepare.sh
run-cypress:
./.ci/server.run_cypress.sh
bash ./.ci/server.run_cypress.sh
stop-server:
./.ci/server.stop.sh
bash ./.ci/server.stop.sh
restart-server: stop-server start-server
.PHONY: start-dashboard stop-dashboard
start-dashboard:
./.ci/dashboard.start.sh
bash ./.ci/dashboard.start.sh
stop-dashboard:
./.ci/dashboard.stop.sh
bash ./.ci/dashboard.stop.sh
.PHONY: print-env-placeholders clean-env-placeholders
print-env-placeholders:

View File

@ -4,30 +4,35 @@ This directory contains the E2E testing code for the Mattermost web client.
### How to run locally
The E2E testing scripts depend on the following tools being installed on your system: `docker`, `docker-compose`, `make`, `git`, `jq`, and some common utilities (`coreutils`, `findutils`, `bash`, `awk`, `sed`, `grep`)
##### For test case development
Please refer to the [dedicated developer documentation](https://developers.mattermost.com/contribute/more-info/webapp/e2e-testing/) for instructions.
##### 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`, 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. Create the `.ci/env` file, and populate the variables you need, keeping in mind that:
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:
* The following variables will be passed over to the server container: `MM_LICENSE` (no enterprise features will be available if this is unset), and the exploded `MM_ENV` (a comma-separated list of env var specifications)
* The following variables will be passed over to the cypress container: `BRANCH`, `BUILD_ID`, `CI_BASE_URL`, `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
* The `TEST_FILTER` variable can also be set, to customize which tests you want cypress to run
* All variables are optional, and will be set to sane defaults
* The following variables 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 to run. If not specified, only the smoke tests will run. Please check the `e2e-tests/cypress/run_tests.js` file for details about its format.
2. (optional) `make start-dashboard`: start the automation-dashboard in the background
* This also 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 env, the latter variables will take precedence
* 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
3. `make`: start and prepare the server, then run the cypress tests
* You can track the progress of the run in the `http://localhost:4000/cycles` dashboard, if you launched it locally
4. `make stop`: tears down the server (and the dashboard, if running), then cleans up the env placeholder 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.
- Aside from some exceptions (e.g. `TEST_FILTER`), most of the variables in `.ci/env` must be set before the `make start-server` command is run. Modifying that file afterwards 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 you started the dashboard locally in the past, but want to point to another dashboard later, you can run `make clean-env-placeholders` to remove references to the local dashboard (you'll likely need to restart the server)
- Dynamically set variables for the server or cypress should be managed within the `.env.*` files, rather than in the docker-compose files, to streamline their management.
##### How to control which tests to run
The `TEST_FILTER` variable will control which test files to run Cypress tests against. Please check the `e2e-tests/cypress/run_tests.js` file for details about its format.
- If new variables need to be passed to any of the containers:
* If their value is fixed (e.g. a static server configuration): these may be simply added to the `.ci/server.override.yml` 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.

View File

@ -95,6 +95,7 @@
"format": ["PascalCase"]
}
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/no-unused-vars": [
2,

File diff suppressed because it is too large Load Diff

View File

@ -1,64 +1,64 @@
{
"devDependencies": {
"@babel/eslint-parser": "7.21.8",
"@babel/eslint-plugin": "7.19.1",
"@cypress/request": "2.88.11",
"@mattermost/types": "7.10.0",
"@testing-library/cypress": "9.0.0",
"@types/async": "3.2.20",
"@types/authenticator": "1.1.1",
"@types/express": "4.17.17",
"@types/fs-extra": "11.0.1",
"@types/lodash": "4.14.194",
"@babel/eslint-parser": "7.22.15",
"@babel/eslint-plugin": "7.22.10",
"@cypress/request": "3.0.1",
"@mattermost/client": "9.0.0",
"@mattermost/types": "9.0.0",
"@testing-library/cypress": "10.0.1",
"@types/async": "3.2.21",
"@types/authenticator": "1.1.2",
"@types/express": "4.17.18",
"@types/fs-extra": "11.0.2",
"@types/lodash": "4.14.199",
"@types/lodash.intersection": "4.4.7",
"@types/lodash.mapkeys": "4.6.7",
"@types/lodash.without": "4.4.7",
"@types/mime-types": "2.1.1",
"@types/mochawesome": "6.2.1",
"@types/pdf-parse": "1.1.1",
"@types/recursive-readdir": "2.2.1",
"@types/shelljs": "0.8.12",
"@types/uuid": "9.0.1",
"@typescript-eslint/eslint-plugin": "5.59.2",
"@typescript-eslint/parser": "5.59.2",
"@types/mime-types": "2.1.2",
"@types/mochawesome": "6.2.2",
"@types/pdf-parse": "1.1.2",
"@types/recursive-readdir": "2.2.2",
"@types/shelljs": "0.8.13",
"@types/uuid": "9.0.4",
"@typescript-eslint/eslint-plugin": "6.7.4",
"@typescript-eslint/parser": "6.7.4",
"async": "3.2.4",
"authenticator": "1.1.5",
"aws-sdk": "2.1371.0",
"axios": "1.4.0",
"axios-retry": "3.4.0",
"chai": "4.3.7",
"aws-sdk": "2.1468.0",
"axios": "1.5.1",
"axios-retry": "3.8.0",
"chai": "4.3.10",
"chalk": "4.1.2",
"client-oauth2": "github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49",
"cross-env": "7.0.3",
"cypress": "12.11.0",
"cypress": "13.3.0",
"cypress-file-upload": "5.0.8",
"cypress-multi-reporters": "1.6.3",
"cypress-plugin-tab": "1.0.5",
"cypress-wait-until": "1.7.2",
"dayjs": "1.11.7",
"cypress-wait-until": "2.0.1",
"dayjs": "1.11.10",
"deepmerge": "4.3.1",
"dotenv": "16.0.3",
"eslint": "8.39.0",
"eslint-import-resolver-webpack": "0.13.2",
"eslint-plugin-cypress": "2.13.3",
"dotenv": "16.3.1",
"eslint": "8.50.0",
"eslint-import-resolver-webpack": "0.13.7",
"eslint-plugin-cypress": "2.15.1",
"eslint-plugin-header": "3.1.1",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-mattermost": "github:mattermost/eslint-plugin-mattermost#5b0c972eacf19286e4c66221b39113bf8728a99e",
"eslint-plugin-no-only-tests": "3.1.0",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react": "7.33.2",
"express": "4.18.2",
"extract-zip": "2.0.1",
"knex": "2.4.2",
"knex": "2.5.1",
"localforage": "1.10.0",
"lodash.intersection": "4.4.0",
"lodash.mapkeys": "4.6.0",
"lodash.without": "4.4.0",
"lodash.xor": "4.5.0",
"mattermost-redux": "5.33.1",
"mime": "3.0.0",
"mime-types": "2.1.35",
"mocha": "10.2.0",
"mocha-junit-reporter": "2.2.0",
"mocha-junit-reporter": "2.2.1",
"mocha-multi-reporters": "1.5.1",
"mochawesome": "7.1.3",
"mochawesome-merge": "4.3.0",
@ -67,16 +67,23 @@
"mysql": "2.18.1",
"path": "0.12.7",
"pdf-parse": "1.1.1",
"pg": "8.10.0",
"pg": "8.11.3",
"recursive-readdir": "2.2.3",
"shelljs": "0.8.5",
"timezones.json": "1.7.0",
"typescript": "5.0.4",
"uuid": "9.0.0",
"timezones.json": "1.7.1",
"typescript": "5.2.2",
"uuid": "9.0.1",
"yargs": "17.7.2"
},
"overrides": {
"@mattermost/client": {
"typescript": "^5.0.4"
},
"@mattermost/types": {
"typescript": "^5.0.4"
}
},
"scripts": {
"postinstall": "patch-package",
"check-types": "tsc -b",
"cypress:open": "cross-env TZ=Etc/UTC cypress open",
"cypress:run": "cross-env TZ=Etc/UTC cypress run",
@ -92,8 +99,5 @@
"uniq-meta": "grep -r \"^// $META:\" cypress | grep -ow '@\\w*' | sort | uniq",
"check": "eslint --ext .js,.ts . --quiet --cache",
"fix": "eslint --ext .js,.ts . --quiet --fix --cache"
},
"dependencies": {
"patch-package": "7.0.0"
}
}

View File

@ -1,13 +0,0 @@
diff --git a/node_modules/@testing-library/cypress/dist/index.js b/node_modules/@testing-library/cypress/dist/index.js
index 9a03c94..b2d3aac 100644
--- a/node_modules/@testing-library/cypress/dist/index.js
+++ b/node_modules/@testing-library/cypress/dist/index.js
@@ -38,7 +38,7 @@ function createQuery(queryName, implementationName) {
};
const log = options.log !== false && Cypress.log({
name: queryName,
- type: this.get('prev').get('chainerId') === this.get('chainerId') ? 'child' : 'parent',
+ type: this.get('prev') && this.get('prev').get('chainerId') === this.get('chainerId') ? 'child' : 'parent',
message: inputArr,
timeout: options.timeout,
consoleProps: () => consoleProps

View File

@ -1,76 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @boards
describe('Card badges', () => {
before(() => {
// # Login as new user
cy.apiInitSetup({loginAfter: true});
cy.clearLocalStorage();
});
it('MM-T5395 Shows and hides card badges', () => {
cy.visit('/boards');
// Create new board
cy.uiCreateNewBoard('Testing');
// Add a new card
cy.uiAddNewCard('Card');
// Add some comments
cy.log('**Add some comments**');
addComment('Some comment');
addComment('Another comment');
addComment('Additional comment');
// Add card description
cy.log('**Add card description**');
cy.findByText('Add a description...').click();
cy.findByRole('combobox').type('## Header\n- [ ] one\n- [x] two{esc}');
// Add checkboxes
cy.log('**Add checkboxes**');
cy.findByRole('button', {name: 'Add content'}).click();
cy.findByRole('button', {name: 'checkbox'}).click();
cy.focused().type('three{enter}');
cy.focused().type('four{enter}');
cy.focused().type('{esc}');
cy.findByDisplayValue('three').prev().click();
// Close card dialog
cy.log('**Close card dialog**');
cy.findByRole('button', {name: 'Close dialog'}).click();
cy.findByRole('dialog').should('not.exist');
// Show card badges
cy.log('**Show card badges**');
cy.findByRole('button', {name: 'Properties menu'}).click();
cy.findByRole('button', {name: 'Comments and description'}).click();
cy.findByTitle('This card has a description').should('exist');
cy.findByTitle('Comments').contains('3').should('exist');
cy.findByTitle('Checkboxes').contains('2/4').should('exist');
// Hide card badges
cy.log('**Hide card badges**');
cy.findByRole('button', {name: 'Comments and description'}).click();
cy.findByRole('button', {name: 'Properties menu'}).click();
cy.findByTitle('This card has a description').should('not.exist');
cy.findByTitle('Comments').should('not.exist');
cy.findByTitle('Checkboxes').should('not.exist');
});
const addComment = (text: string) => {
cy.findByText('Add a comment...').click();
cy.findByRole('combobox').type(text).blur();
cy.findByRole('button', {name: 'Send'}).click();
};
});

View File

@ -1,118 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @boards
describe('Card URL Property', () => {
before(() => {
// # Login as new user
cy.apiInitSetup({loginAfter: true});
cy.clearLocalStorage();
});
const url = 'https://mattermost.com';
const changedURL = 'https://mattermost.com/blog';
it('MM-T5396 Allows to create and edit URL property', () => {
cy.visit('/boards');
// Create new board
cy.uiCreateNewBoard('Testing');
// Add a new card
cy.uiAddNewCard('Card');
// Add URL property
cy.log('**Add URL property**');
cy.findByRole('button', {name: '+ Add a property'}).click();
cy.findByRole('button', {name: 'URL'}).click();
cy.findByRole('textbox', {name: 'URL'}).type('{enter}');
// Enter URL
cy.log('**Enter URL**');
cy.findByPlaceholderText('Empty').type(`${url}{enter}`);
// Check buttons
cy.log('**Check link**');
cy.get('.URLProperty').trigger('mouseover');
cy.log('**Check buttons**');
// Change URL
cy.log('**Change URL**');
cy.get('.URLProperty Button[title=\'Edit\']').click({force: true});
cy.findByRole('textbox', {name: url}).clear().type(`${changedURL}{enter}`);
cy.findByRole('link', {name: changedURL}).should('exist');
// Close card dialog
cy.log('**Close card dialog**');
cy.findByRole('button', {name: 'Close dialog'}).click();
cy.findByRole('dialog').should('not.exist');
// Show URL property
showURLProperty();
// Copy URL to clipboard
cy.log('**Copy URL to clipboard**');
cy.document().then((doc) => cy.spy(doc, 'execCommand')).as('exec');
cy.get('.URLProperty Button[title=\'Edit\']').should('not.exist');
cy.get('.URLProperty Button[title=\'Copy\']').click({force: true});
cy.findByText('Copied!').should('exist');
cy.findByText('Copied!').should('not.exist');
cy.get('@exec').should('have.been.calledOnceWith', 'copy');
// Add table view
addView('Table');
// Check buttons
cy.log('**Check buttons**');
cy.get('.URLProperty Button[title=\'Edit\']').should('exist');
cy.get('.URLProperty Button[title=\'Copy\']').should('exist');
cy.findByRole('button', {name: 'Copy'}).should('not.exist');
// Add gallery view
addView('Gallery');
showURLProperty();
// Check buttons
cy.log('**Check buttons**');
cy.get('.URLProperty Button[title=\'Edit\']').should('not.exist');
cy.get('.URLProperty Button[title=\'Copy\']').should('exist');
// Add calendar view
addView('Calendar');
showURLProperty();
// Check buttons
cy.log('**Check buttons**');
cy.get('.URLProperty Button[title=\'Edit\']').should('not.exist');
cy.get('.URLProperty Button[title=\'Copy\']').should('exist');
});
type ViewType = 'Board' | 'Table' | 'Gallery' | 'Calendar'
const addView = (type: ViewType) => {
cy.log(`**Add ${type} view**`);
cy.findByRole('button', {name: 'View menu'}).click();
cy.findByText('Add view').trigger('mouseover');
cy.findByRole('button', {name: type}).click();
cy.findByRole('textbox', {name: `${type} view`}).should('exist');
};
const showURLProperty = () => {
cy.log('**Show URL property**');
cy.findByRole('button', {name: 'Properties'}).click();
cy.findByRole('button', {name: 'URL'}).click();
cy.findByRole('button', {name: 'Properties'}).click();
cy.findByRole('link', {name: changedURL}).should('exist');
};
});

View File

@ -1,90 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// ***************************************************************
// Stage: @prod
// Group: @boards
describe('channels > channel header', {testIsolation: true}, () => {
let testTeam: {name: string};
let testUser: {username: string};
before(() => {
cy.apiInitSetup().then(({team, user}) => {
testTeam = team;
testUser = user;
// # Login as testUser
cy.apiLogin(testUser);
});
});
describe('App Bar enabled', () => {
it('webapp should hide the Boards channel header button', () => {
cy.apiAdminLogin();
cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: true}});
// # Login as testUser
cy.apiLogin(testUser);
// # Navigate directly to a channel
cy.visit(`/${testTeam.name}/channels/town-square`);
// * Verify channel header button is not showing
cy.get('#channel-header').within(() => {
cy.get('[data-testid="boardsIcon"]').should('not.exist');
});
});
});
describe('App Bar disabled', () => {
beforeEach(() => {
cy.apiAdminLogin();
cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: false}});
// # Login as testUser
cy.apiLogin(testUser);
});
it('webapp should show the Boards channel header button', () => {
// # Navigate directly to a channel
cy.visit(`/${testTeam.name}/channels/town-square`);
// * Verify channel header button is showing
cy.get('#channel-header').within(() => {
cy.get('#incidentIcon').should('exist');
});
});
it('tooltip text should show "Boards" for Boards channel header button', () => {
// # Navigate directly to a channel
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Hover over the channel header icon
cy.get('#channel-header').within(() => {
cy.get('[data-testid="boardsIcon"]').trigger('mouseover');
});
// * Verify tooltip text
cy.get('#pluginTooltip').contains('Boards');
});
it('webapp should make the Boards channel header button active when opened', () => {
// # Navigate directly to a channel
cy.visit(`/${testTeam.name}/channels/town-square`);
cy.get('#channel-header').within(() => {
// # Click the channel header button
cy.get('[data-testid="boardsIcon"]').as('icon').click();
// * Verify channel header button is showing active className
cy.get('@icon').parent().
should('have.class', 'channel-header__icon--active-inverted');
});
});
});
});

View File

@ -1,339 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @boards
import timeouts from '../../fixtures/timeouts';
describe('Create and delete board / card', () => {
const timestamp = new Date().toLocaleString();
const boardTitle = `Test Board (${timestamp})`;
const cardTitle = `Test Card (${timestamp})`;
beforeEach(() => {
// # Login as new user
cy.apiAdminLogin().apiInitSetup({loginAfter: true});
cy.clearLocalStorage();
});
it('MM-T4274 Create an Empty Board', () => {
cy.visit('/boards');
// Tests for template selector
cy.findByText('Use this template').should('exist').click();
// Some options are present
cy.contains('Meeting Agenda').should('exist');
cy.contains('Personal Goals').should('exist');
cy.contains('Project Tasks').should('exist');
// Create empty board
cy.findByText('Create an empty board').should('exist').click({force: true});
cy.get('.BoardComponent').should('exist');
// Change Title
cy.findByPlaceholderText('Untitled board').should('be.visible').wait(timeouts.HALF_SEC).as('editableTitle');
cy.get('@editableTitle').should('be.visible').
clear().
type('Testing').
type('{enter}').
should('have.value', 'Testing');
});
it('MM-T4275 Set up Board description', () => {
cy.visit('/boards');
// # Create an empty board and change tile to Testing
cy.findByText('Create an empty board').should('exist').click({force: true});
cy.get('.BoardComponent').should('exist');
// # Change Title
cy.findByPlaceholderText('Untitled board').should('be.visible').wait(timeouts.HALF_SEC);
// * Assert that the title is changed to "testing"
cy.findByPlaceholderText('Untitled board').
clear().
type('Testing').
type('{enter}').
should('have.value', 'Testing');
// # "Add icon" and "Show description" options appear
cy.findByText('Add icon').should('exist').click({force: true});
cy.findByText('show description').should('exist').click({force: true});
// # Click on "Add a description" below the board title and type "for testing purposes only"
cy.findByText('Add a description...').should('be.visible').wait(timeouts.HALF_SEC);
// * Assert that the editable description should be visible
cy.findByText('Add a description...').should('be.visible');
cy.findByText('Add a description...').click({force: true});
cy.get('.description').
click().
get('.description .MarkdownEditorInput').
type('for testing purposes only');
// # Click to other element to give some time for the description to be saved.
cy.findByPlaceholderText('Untitled board').click();
// * Assert that the description is changed to "for testing purposes only"
cy.findByText('for testing purposes only').should('be.visible');
// # Hide Description options should appear and click on it to hide description
cy.findByText('hide description').should('exist').click({force: true});
// * Assert that description should not appear"
cy.get('.description').should('not.exist');
// # Show Description options should appear and click on it to show description
cy.findByText('show description').should('exist').click({force: true});
// * Assert that the description "for testing purposes should be visible"
cy.findByText('for testing purposes only').should('be.visible');
});
it('MM-T4276 Set up Board emoji', () => {
cy.visit('/boards');
// # Create an empty board and change tile to Testing
cy.findByText('Create an empty board').should('exist').click({force: true});
cy.get('.BoardComponent').should('exist');
// # Change Title
cy.findByPlaceholderText('Untitled board').should('be.visible').wait(timeouts.HALF_SEC);
// * Assert that the title is changed to "testing"
cy.findByPlaceholderText('Untitled board').
clear().
type('Testing').
type('{enter}').
should('have.value', 'Testing');
// # "Add icon" and "Show description" options appear
cy.findByText('Add icon').should('exist');
cy.findByText('show description').should('exist');
// # Click on "Add icon"
cy.findByText('Add icon').should('exist').click({force: true});
// * Assert that a random emoji is selected and added at the beginning of the board title
cy.get('.IconSelector').should('exist');
// # Click on the emoji next to the board title
cy.get('.IconSelector .MenuWrapper').should('exist').click({force: true});
// * Assert that Dropdown menu with 3 options appears
cy.findByText('Random').should('exist');
cy.findByText('Pick icon').should('exist');
cy.findByText('Remove icon').should('exist');
// # Hover your mouse over the "Pick Icon" option
cy.findByText('Pick icon').trigger('mouseover');
// * Assert that emoji picker menu appears
cy.get('.IconSelector .menu-contents').should('exist');
// # Click on the emoji from the picker
cy.get('.EmojiPicker').should('exist').and('be.visible').within(() => {
// # Click on the emoji
cy.get("[aria-label='😀, grinning']").should('exist');
cy.get("[aria-label='😀, grinning']").eq(0).click({force: true});
});
// * Assert that Selected emoji is now displayed next to the board title
cy.get('.IconSelector span').contains('😀');
// # Click on the emoji next to the board title
cy.get('.IconSelector .MenuWrapper').should('exist').click({force: true});
// * Assert that Dropdown menu with 3 options appears
cy.findByText('Random').should('exist');
cy.findByText('Pick icon').should('exist');
cy.findByText('Remove icon').should('exist');
// # Click "Remove icon"
cy.findByText('Remove icon').click({force: true});
// * Assert that Icon next to the board title is removed
cy.get('.IconSelector').should('not.exist');
});
it('MM-T5397 Can create and delete a board and a card', () => {
// Visit a page and create new empty board
cy.visit('/boards');
cy.uiCreateEmptyBoard();
// Change board title
cy.log('**Change board title**');
cy.get('.Editable.title').
type(boardTitle).
type('{enter}').
should('have.value', boardTitle);
// Rename board view
cy.log('**Rename board view**');
const boardViewTitle = `Test board (${timestamp})`;
cy.get(".ViewHeader>.viewSelector>.Editable[title='Board view']").should('exist');
cy.get('.ViewHeader>.viewSelector>.Editable').
clear().
type(boardViewTitle).
type('{esc}');
cy.get(`.ViewHeader .Editable[title='${boardViewTitle}']`).should('exist');
// Create card
cy.log('**Create card**');
cy.get('.ViewHeader').contains('New').click();
cy.get('.CardDetail').should('exist');
//Check title has focus when card is created
cy.log('**Check title has focus when card is created**');
cy.get('.CardDetail .EditableArea.title').
should('have.focus');
// Change card title
cy.log('**Change card title**');
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.get('.CardDetail .EditableArea.title').
click().
should('have.focus').
type(cardTitle).
should('have.value', cardTitle);
// Close card dialog
cy.log('**Close card dialog**');
cy.get('.Dialog Button[title=\'Close dialog\']').
should('be.visible').
click();
// Create a card by clicking on the + button
cy.log('**Create a card by clicking on the + button**');
cy.get('.KanbanColumnHeader button .AddIcon').click();
cy.get('.CardDetail').should('exist');
cy.get('.Dialog.dialog-back .wrapper').click({force: true});
// Create table view
cy.log('**Create table view**');
cy.get('.ViewHeader').get('.DropdownIcon').first().parent().click();
cy.get('.ViewHeader').contains('Add view').trigger('mouseover');
cy.get('.ViewHeader').
contains('Add view').
parent().
contains('Table').
click();
cy.get(".ViewHeader .Editable[title='Table view']").should('exist');
cy.get(`.TableRow [value='${cardTitle}']`).should('exist');
// Rename table view
cy.log('**Rename table view**');
const tableViewTitle = `Test table (${timestamp})`;
cy.get(".ViewHeader .Editable[title='Table view']").
clear().
type(tableViewTitle).
type('{esc}');
cy.get(`.ViewHeader .Editable[title='${tableViewTitle}']`).should('exist');
// Sort the table
cy.log('**Sort the table**');
cy.get('.ViewHeader').contains('Sort').click();
cy.get('.ViewHeader').
contains('Sort').
parent().
contains('Name').
click();
// Delete board
cy.log('**Delete board**');
cy.get('.Sidebar .octo-sidebar-list').then((el) => {
cy.log(el.text());
});
cy.get('.Sidebar .octo-sidebar-list').
contains(boardTitle).
parent().
find('.MenuWrapper').
find('button.IconButton').
click({force: true});
cy.contains('Delete board').click({force: true});
cy.get('.DeleteBoardDialog button.danger').click({force: true});
cy.contains(boardTitle).should('not.exist');
});
it('MM-T4433 Scrolls the kanban board when dragging card to edge', () => {
// Visit a page and create new empty board
cy.visit('/boards');
cy.uiCreateEmptyBoard();
// Create 10 empty groups
cy.log('**Create new empty groups**');
for (let i = 0; i < 10; i++) {
cy.contains('+ Add a group').scrollIntoView().should('be.visible').click();
cy.get('.KanbanColumnHeader .Editable[value=\'New group\']').should('have.length', i + 1);
}
// Create empty card in last group
cy.log('**Create new empty card in first group**');
cy.get('.octo-board-column').last().contains('+ New').scrollIntoView().click();
cy.get('.Dialog').should('exist');
cy.get('.Dialog Button[title=\'Close dialog\']').should('be.visible').click();
cy.get('.KanbanCard').scrollIntoView().should('exist');
// Drag card to right corner and expect scroll to occur
cy.get('.Kanban').invoke('scrollLeft').should('not.equal', 0);
cy.get('.KanbanCard').
trigger('dragstart');
// wait necessary to trigger scroll animation for some time
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.get('.Kanban').
trigger('dragover', {clientX: 400, clientY: Cypress.config().viewportHeight / 2}).
wait(timeouts.TEN_SEC).
trigger('dragend');
cy.get('.Kanban').invoke('scrollLeft').should('equal', 0);
});
it('MM-T5398 cut/undo/redo work in comments', () => {
const isMAC = navigator.userAgent.indexOf('Mac') !== -1;
const ctrlKey = isMAC ? 'meta' : 'ctrl';
// Visit a page and create new empty board
cy.visit('/boards');
cy.uiCreateEmptyBoard();
// Create card
cy.log('**Create card**');
cy.get('.ViewHeader').contains('New').click();
cy.get('.CardDetail').should('exist');
cy.log('**Add comment**');
cy.get('.CommentsList').
should('exist').
findAllByTestId('preview-element').
click();
cy.get('.CommentsList .MarkdownEditor').
type('Test Text');
cy.log('**Cut comment**');
cy.get('.CommentsList .MarkdownEditor').
type('{selectAll}').
trigger('cut').
should('have.text', '');
cy.log('**Undo comment**');
cy.get('.CommentsList .MarkdownEditor').
type(`{${ctrlKey}+z}`).
should('have.text', 'Test Text');
cy.log('**Redo comment**');
cy.get('.CommentsList .MarkdownEditor').
type(`{shift+${ctrlKey}+z}`).
should('have.text', '');
});
});

View File

@ -1,68 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @boards
describe('Group board by different properties', () => {
before(() => {
// # Login as new user
cy.apiInitSetup({loginAfter: true});
cy.clearLocalStorage();
});
it('MM-T4291 Group by different property', () => {
cy.visit('/boards');
// Create new board
cy.uiCreateNewBoard('Testing');
// Add a new group
cy.uiAddNewGroup('Group 1');
// Add a new card to the group
cy.log('**Add a new card to the group**');
cy.findAllByRole('button', {name: '+ New'}).eq(1).click();
cy.findByRole('dialog').should('exist');
cy.findByTestId('select-non-editable').findByText('Group 1').should('exist');
cy.get('#mainBoardBody').findByText('Untitled').should('exist');
// Add new select property
cy.log('**Add new select property**');
cy.findAllByRole('button', {name: '+ Add a property'}).click();
cy.findAllByRole('button', {name: 'Select'}).click();
cy.findByRole('textbox', {name: 'Select'}).type('{enter}');
cy.findByRole('dialog').findByRole('button', {name: 'Select'}).should('exist');
// Close card dialog
cy.log('**Close card dialog**');
cy.findByRole('button', {name: 'Close dialog'}).should('exist').click();
cy.findByRole('dialog').should('not.exist');
// Group by new select property
cy.log('**Group by new select property**');
cy.findByRole('button', {name: /Group by:/}).click();
cy.findByRole('button', {name: 'Status'}).get('.CheckIcon').should('exist');
cy.findByRole('button', {name: 'Select'}).click();
cy.findByTitle(/empty Select property/).contains('No Select');
cy.get('#mainBoardBody').findByText('Untitled').should('exist');
// Add another new group
cy.log('**Add another new group**');
cy.findByRole('button', {name: '+ Add a group'}).click();
cy.findByRole('textbox', {name: 'New group'}).should('exist');
// Add a new card to another group
cy.log('**Add a new card to another group**');
cy.findAllByRole('button', {name: '+ New'}).eq(1).click();
cy.findByRole('dialog').should('exist');
cy.findAllByTestId('select-non-editable').last().findByText('New group').should('exist');
});
});

View File

@ -1,104 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @boards
describe('Manage groups', () => {
beforeEach(() => {
// # Login as new user
cy.apiAdminLogin().apiInitSetup({loginAfter: true});
cy.clearLocalStorage();
});
it('MM-T4284 Adding a group', () => {
// Visit a page and create new empty board
cy.visit('/boards');
cy.uiCreateEmptyBoard();
cy.contains('+ Add a group').click({force: true});
cy.get('.KanbanColumnHeader .Editable[value=\'New group\']').should('exist');
cy.get('.KanbanColumnHeader .Editable[value=\'New group\']').
clear().
type('Group 1').
blur();
cy.get('.KanbanColumnHeader .Editable[value=\'Group 1\']').should('exist');
});
it('MM-T4285 Adding group color', () => {
// Visit a page and create new empty board
cy.visit('/boards');
cy.uiCreateEmptyBoard();
cy.contains('+ Add a group').click({force: true});
cy.get('.KanbanColumnHeader .Editable[value=\'New group\']').should('exist');
cy.get('.KanbanColumnHeader').last().within(() => {
cy.get('.icon-dots-horizontal').click({force: true});
cy.get('.menu-options').should('exist').first().within(() => {
cy.contains('Hide').should('exist');
cy.contains('Delete').should('exist');
// Some colours
cy.contains('Brown').should('exist');
cy.contains('Gray').should('exist');
cy.contains('Orange').should('exist');
// Click on green
cy.contains('Green').should('be.visible').click();
});
});
cy.get('.KanbanColumnHeader').last().within(() => {
cy.get('.Label.propColorGreen').should('exist');
});
});
it('MM-T4287 Hiding/unhiding a group', () => {
// Step 1: Create an empty board and add a group
cy.visit('/boards');
cy.uiCreateEmptyBoard();
cy.contains('+ Add a group').click({force: true});
cy.get('.KanbanColumnHeader .Editable[value=\'New group\']').should('exist');
cy.get('.KanbanColumnHeader .Editable[value=\'New group\']').
clear().
type('Group 1').
blur();
cy.get('.KanbanColumnHeader .Editable[value=\'Group 1\']').should('exist');
// Step 2: Click on the three dots next to "Group 1"
cy.get('.KanbanColumnHeader').last().within(() => {
cy.get('.icon-dots-horizontal').click({force: true});
cy.get('.menu-options').should('exist').first().within(() => {
cy.contains('Hide').should('exist');
cy.contains('Delete').should('exist');
// Some colours
cy.contains('Brown').should('exist');
cy.contains('Gray').should('exist');
cy.contains('Orange').should('exist');
});
});
// Step 3: Click on "Hide"
cy.contains('Hide').click({force: true});
cy.get('.octo-board-hidden-item').contains('Group 1').should('exist');
cy.get('.KanbanColumnHeader .Editable[value=\'Group 1\']').should('not.exist');
// Step 4: Click "Group 1", then click "Show" in the dropdown
cy.contains('Group 1').click({force: true});
cy.contains('Show').click({force: true});
cy.get('.octo-board-hidden-item').should('not.exist');
cy.get('.KanbanColumnHeader .Editable[value=\'Group 1\']').should('exist');
});
});

View File

@ -34,7 +34,7 @@ describe('Verify Accessibility Support in different sections in Settings and Pro
{key: 'desktop', label: 'Desktop Notifications', type: 'radio'},
{key: 'email', label: 'Email Notifications', type: 'radio'},
{key: 'push', label: 'Mobile Push Notifications', type: 'radio'},
{key: 'keys', label: 'Words That Trigger Mentions', type: 'checkbox'},
{key: 'keysWithNotification', label: 'Keywords that trigger Notifications', type: 'checkbox'},
{key: 'comments', label: 'Reply notifications', type: 'radio'},
],
display: [

View File

@ -130,10 +130,10 @@ describe('Authentication', () => {
cy.get('.admin-console__header').should('be.visible').and('have.text', 'Password');
cy.findByTestId('passwordMinimumLengthinput').should('be.visible').and('have.value', '8');
cy.findByLabelText('At least one lowercase letter').get('input').should('not.be.checked');
cy.findByLabelText('At least one uppercase letter').get('input').should('not.be.checked');
cy.findByLabelText('At least one number').get('input').should('not.be.checked');
cy.findByLabelText('At least one symbol (e.g. "~!@#$%^&*()")').get('input').should('not.be.checked');
cy.findByText('At least one lowercase letter').siblings().should('not.be.checked');
cy.findByText('At least one uppercase letter').siblings().should('not.be.checked');
cy.findByText('At least one number').siblings().should('not.be.checked');
cy.findByText('At least one symbol (e.g. "~!@#$%^&*()")').siblings().should('not.be.checked');
if (!isCloudLicensed) {
cy.findByTestId('maximumLoginAttemptsinput').should('be.visible').and('have.value', '10');

View File

@ -16,6 +16,7 @@ describe('Leave an archived channel', () => {
let testTeam;
let offTopicUrl;
const channelType = {
all: 'Channel Type: All',
public: 'Channel Type: Public',
archived: 'Channel Type: Archived',
};
@ -98,19 +99,18 @@ describe('Leave an archived channel', () => {
cy.get('#showMoreChannels').click();
// # More channels modal opens
cy.get('#browseChannelsModal').should('be.visible').within(() => {
// # Click on dropdown
cy.findByText(channelType.public).should('be.visible').click();
cy.get('#browseChannelsModal').should('be.visible');
// # Click archived channels
cy.findByText('Archived Channels').click();
// # Click on dropdown
cy.findByText(channelType.all).should('be.visible').click();
// # Modal should contain created channel
cy.get('#moreChannelsList').should('contain', channel.display_name);
});
// # Click archived channels
cy.findByText('Archived channels').click();
cy.get('body').typeWithForce('{esc}');
// # Modal should contain created channel
cy.get('#moreChannelsList').should('contain', channel.display_name);
});
cy.get('body').typeWithForce('{esc}');
});
it('MM-T1699 - Browse Channels for all channel types shows archived channels option', () => {
@ -146,12 +146,12 @@ describe('Leave an archived channel', () => {
cy.get('#showMoreChannels').click();
// # More channels modal opens
cy.get('#browseChannelsModal').should('be.visible').within(() => {
// # Public channel list opens by default
cy.findByText(channelType.public).should('be.visible').click();
cy.get('#browseChannelsModal').should('be.visible').then(() => {
// # All channel list opens by default
cy.findByText(channelType.all).should('be.visible').click();
// # Click on archived channels
cy.findByText('Archived Channels').click();
cy.findByText('Archived channels').click();
// # Channel list should contain newly created channels
cy.get('#moreChannelsList').should('contain', archivedPrivateChannel.name);
@ -199,12 +199,12 @@ describe('Leave an archived channel', () => {
cy.get('#showMoreChannels').click();
// # More channels modal opens
cy.get('#browseChannelsModal').should('be.visible').within(() => {
// # Public channels are shown by default
cy.findByText(channelType.public).should('be.visible').click();
cy.get('#browseChannelsModal').should('be.visible').then(() => {
// # All channels are shown by default
cy.findByText(channelType.all).should('be.visible').click();
// # Go to archived channels
cy.findByText('Archived Channels').click();
cy.findByText('Archived channels').click();
// # Channel list should contain both archived public channels
cy.get('#moreChannelsList').should('contain', archivedPublicChannel1.display_name);
@ -253,12 +253,12 @@ describe('Leave an archived channel', () => {
cy.get('#showMoreChannels').click();
// # More channels modal opens
cy.get('#browseChannelsModal').should('be.visible').within(() => {
// # Show public channels is visible by default
cy.findByText(channelType.public).should('be.visible').click();
cy.get('#browseChannelsModal').should('be.visible').then(() => {
// # Show all channels is visible by default
cy.findByText(channelType.all).should('be.visible').click();
// # Go to archived channels
cy.findByText('Archived Channels').click();
cy.findByText('Archived channels').click();
// # Channel list should contain only the private channel user is a member of
cy.get('#moreChannelsList').should('contain', archivedPrivateChannel1.name);
@ -287,12 +287,12 @@ describe('Leave an archived channel', () => {
// # Click on browse channels
cy.get('#showMoreChannels').click();
// # More channels modal opens and lands on public channels
cy.get('#browseChannelsModal').should('be.visible').within(() => {
cy.findByText(channelType.public).should('be.visible').click();
// # More channels modal opens and lands on all channels
cy.get('#browseChannelsModal').should('be.visible').then(() => {
cy.findByText(channelType.all).should('be.visible').click();
// # Go to archived channels
cy.findByText('Archived Channels').click();
cy.findByText('Archived channels').click();
// # More channels list should contain the archived channel
cy.get('#moreChannelsList').should('contain', archivedChannel.display_name);
@ -323,7 +323,8 @@ describe('Leave an archived channel', () => {
cy.get('#showMoreChannels').click();
// # Modal should not contain the created channel
cy.get('#channelsMoreDropdown').should('not.exist');
cy.findByText(channelType.all).should('be.visible').click();
cy.findByText('Archived channels').should('not.exist');
cy.get('#moreChannelsList').should('not.contain', channel.name);
});
cy.get('body').typeWithForce('{esc}');

View File

@ -15,6 +15,7 @@ import * as TIMEOUTS from '../../../fixtures/timeouts';
import {createPrivateChannel} from '../enterprise/elasticsearch_autocomplete/helpers';
const channelType = {
all: 'Channel Type: All',
public: 'Channel Type: Public',
archived: 'Channel Type: Archived',
};
@ -69,8 +70,8 @@ describe('Channels', () => {
cy.uiBrowseOrCreateChannel('Browse channels').click();
cy.get('#browseChannelsModal').should('be.visible').within(() => {
// * Dropdown should be visible, defaulting to "Public Channels"
cy.get('#channelsMoreDropdown').should('be.visible').and('contain', channelType.public).wait(TIMEOUTS.HALF_SEC);
// * Dropdown should be visible, defaulting to "All Channels"
cy.get('#menuWrapper').should('be.visible').and('contain', channelType.all).wait(TIMEOUTS.HALF_SEC);
cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC);
cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 1).within(() => {
@ -111,27 +112,27 @@ describe('Channels', () => {
// # Go to LHS and click 'Browse channels'
cy.uiBrowseOrCreateChannel('Browse channels').click();
cy.get('#browseChannelsModal').should('be.visible').within(() => {
// # CLick dropdown to open selection
cy.get('#channelsMoreDropdown').should('be.visible').click().within((el) => {
// # Click on archived channels item
cy.findByText('Archived Channels').should('be.visible').click();
cy.get('#browseChannelsModal').should('be.visible');
// * Channel test should be visible as an archived channel in the list
cy.wrap(el).should('contain', channelType.archived);
});
// # CLick dropdown to open selection
cy.get('#menuWrapper').should('be.visible').click();
cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC);
cy.get('#moreChannelsList').children().should('have.length', 1).within(() => {
cy.findByText(testChannel.display_name).should('be.visible');
});
cy.get('#searchChannelsTextbox').clear();
// # Click on archived channels item
cy.findByText('Archived channels').should('be.visible').click();
// * Test channel should be visible as a archived channel in the list
cy.get('#moreChannelsList').should('be.visible').within(() => {
// # Click to view archived channel
cy.findByText(testChannel.display_name).scrollIntoView().should('be.visible').click();
});
// * Menu text should be updated to reflect the selection
cy.get('#menuWrapper').should('contain', channelType.archived);
cy.get('#searchChannelsTextbox').should('be.visible').type(testChannel.display_name).wait(TIMEOUTS.HALF_SEC);
cy.get('#moreChannelsList').children().should('have.length', 1).within(() => {
cy.findByText(testChannel.display_name).should('be.visible');
});
cy.get('#searchChannelsTextbox').clear();
// * Test channel should be visible as a archived channel in the list
cy.get('#moreChannelsList').should('be.visible').within(() => {
// # Click to view archived channel
cy.findByText(testChannel.display_name).scrollIntoView().should('be.visible').click();
});
// * Assert that channel is archived and new messages can't be posted.
@ -177,7 +178,7 @@ describe('Channels', () => {
});
});
it('MM-T1702 Search works when changing public/archived options in the dropdown', () => {
it('MM-T1702 Search works when changing public/all options in the dropdown', () => {
cy.apiAdminLogin();
cy.apiUpdateConfig({
TeamSettings: {
@ -231,32 +232,34 @@ describe('Channels', () => {
// # Go to LHS and click 'Browse channels'
cy.uiBrowseOrCreateChannel('Browse channels').click();
// * Dropdown should be visible, defaulting to "Public Channels"
cy.get('#channelsMoreDropdown').should('be.visible').within((el) => {
cy.wrap(el).should('contain', channelType.public);
// * Dropdown should be visible, defaulting to "All channels"
cy.get('#menuWrapper').should('be.visible').within((el) => {
cy.wrap(el).should('contain', channelType.all);
});
// * Users should be able to type and search
cy.get('#searchChannelsTextbox').should('be.visible').type('iv').wait(TIMEOUTS.HALF_SEC);
cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 1).within(() => {
cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 2);
cy.get('#moreChannelsList').should('be.visible').within(() => {
cy.findByText(newChannel.display_name).should('be.visible');
});
cy.get('#browseChannelsModal').should('be.visible').within(() => {
// * Users should be able to switch to "Archived Channels" list
cy.get('#channelsMoreDropdown').should('be.visible').and('contain', channelType.public).click().within((el) => {
// # Click on archived channels item
cy.findByText('Archived Channels').should('be.visible').click();
cy.get('#browseChannelsModal').should('be.visible');
// * Modal should show the archived channels list
cy.wrap(el).should('contain', channelType.archived);
}).wait(TIMEOUTS.HALF_SEC);
cy.get('#searchChannelsTextbox').clear();
cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 2);
cy.get('#moreChannelsList').within(() => {
cy.findByText(testArchivedChannel.display_name).should('be.visible');
cy.findByText(testPrivateArchivedChannel.display_name).should('be.visible');
});
// * Users should be able to switch to "Archived Channels" list
cy.get('#menuWrapper').should('be.visible').and('contain', channelType.all).click().wait(TIMEOUTS.HALF_SEC);
// # Click on archived channels item
cy.findByText('Archived channels').should('be.visible').click();
// * Modal menu should be updated accordingly
cy.get('#menuWrapper').should('contain', channelType.archived);
cy.get('#searchChannelsTextbox').clear();
cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 2);
cy.get('#moreChannelsList').within(() => {
cy.findByText(testArchivedChannel.display_name).should('be.visible');
cy.findByText(testPrivateArchivedChannel.display_name).should('be.visible');
});
});
});
@ -287,9 +290,10 @@ function verifyBrowseChannelsModal(isEnabled) {
// * Verify that the browse channels modal is open and with or without option to view archived channels
cy.get('#browseChannelsModal').should('be.visible').within(() => {
if (isEnabled) {
cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', channelType.public);
cy.get('#menuWrapper').should('be.visible').and('have.text', channelType.all);
} else {
cy.get('#channelsMoreDropdown').should('not.exist');
cy.get('#menuWrapper').click();
cy.findByText('Archived channels').should('not.exist');
}
});
}

View File

@ -14,7 +14,7 @@ function verifyNoChannelToJoinMessage(isVisible) {
cy.findByText('No public channels').should(isVisible ? 'be.visible' : 'not.exist');
}
describe('more public channels', () => {
describe('browse public channels', () => {
let testUser;
let otherUser;
let testTeam;
@ -41,7 +41,7 @@ describe('more public channels', () => {
});
});
it('MM-T1664 Channels do not disappear from More Channels modal', () => {
it('MM-T1664 Channels do not disappear from Browse Channels modal', () => {
// # Login as other user
cy.apiLogin(otherUser);
@ -51,8 +51,14 @@ describe('more public channels', () => {
// # Go to LHS and click 'Browse channels'
cy.uiBrowseOrCreateChannel('Browse channels').click();
// * Assert that the moreChannelsModel is visible
cy.findByRole('dialog', {name: 'Browse Channels'}).should('be.visible').within(() => {
// * Assert that the browse channel modal is visible
cy.findByRole('dialog', {name: 'Browse Channels'}).should('be.visible').then(() => {
// # Click on dropdown
cy.findByText('Channel Type: All').should('be.visible').click();
// # Click archived channels
cy.findByText('Public channels').should('be.visible').click();
// # Click hide joined checkbox if not already checked
cy.findByText('Hide Joined').should('be.visible').then(($checkbox) => {
if (!$checkbox.prop('checked')) {
@ -92,7 +98,13 @@ describe('more public channels', () => {
cy.uiBrowseOrCreateChannel('Browse channels').click();
// * Assert the moreChannelsModel is visible
cy.findByRole('dialog', {name: 'Browse Channels'}).should('be.visible').within(() => {
cy.findByRole('dialog', {name: 'Browse Channels'}).should('be.visible').then(() => {
// # Click on dropdown
cy.findByText('Channel Type: All').should('be.visible').click();
// # Click archived channels
cy.findByText('Public channels').should('be.visible').click();
// * Assert that the "No more channels to join" message is visible
verifyNoChannelToJoinMessage(true);
});

View File

@ -1,8 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {UserProfile} from 'mattermost-redux/types/users';
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
@ -13,7 +11,7 @@ import {UserProfile} from 'mattermost-redux/types/users';
// Group: @channels @channel
describe('Leave and Archive channel actions display as destructive', () => {
let testUser: UserProfile;
let testUser: Cypress.UserProfile;
let offTopicUrl: string;
before(() => {

View File

@ -30,6 +30,13 @@ describe('Leave channel', () => {
testUser = user;
testTeam = team;
cy.apiSaveUserPreference([{
user_id: user.id,
category: 'crt_thread_pane_step',
name: user.id,
value: '0',
}], user.id);
cy.apiLogin(testUser);
cy.apiCreateChannel(testTeam.id, 'channel', 'channel').then(({channel}) => {
testChannel = channel;

View File

@ -1,49 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @channel
describe('New Channel modal with Boards enabled', () => {
let testTeam;
before(() => {
cy.apiInitSetup().then(({team}) => {
testTeam = team;
});
cy.apiCreateCustomAdmin().then(({sysadmin}) => {
cy.apiLogin(sysadmin);
cy.visit(`/${testTeam.name}/channels/town-square`);
});
});
it('MM-T5141 New Channel is created with an associated Board', () => {
// # Create new channel with board
const channelName = 'Test Channel With Board';
cy.uiCreateChannel({
prefix: 'channel-',
isPrivate: false,
purpose: '',
name: channelName,
createBoard: 'Roadmap',
}).then(() => {
// * Verify that new channel is in the sidebar and is active
cy.url().should('include', `/${testTeam.name}/channels/test-channel`);
cy.get('#channelHeaderTitle').should('contain', channelName);
cy.get(`.SidebarChannel.active:contains(${channelName})`).should('be.visible');
// * Verify the board is created - check the message sent
cy.waitUntil(() => cy.getLastPost().then((el) => {
const postedMessageEl = el.find('.post-message__text > p')[0];
return Boolean(postedMessageEl && postedMessageEl.textContent.includes('linked the board'));
}));
});
});
});

View File

@ -10,29 +10,53 @@
// Stage: @prod
// Group: @channels @channel @channel_settings @smoke
import {getRandomId} from '../../../utils';
describe('Channel Settings', () => {
let testTeam: Cypress.Team;
let firstUser: Cypress.UserProfile;
let addedUsersChannel: Cypress.Channel;
let username: string;
let groupId: string;
const usernames: string[] = [];
const users: Cypress.UserProfile[] = [];
before(() => {
cy.apiInitSetup().then(({team, user}) => {
testTeam = team;
firstUser = user;
const teamId = testTeam.id;
// # Add 4 users
for (let i = 0; i < 4; i++) {
cy.apiCreateUser().then(({user: newUser}) => { // eslint-disable-line
cy.apiAddUserToTeam(testTeam.id, newUser.id);
// # Add 10 users
for (let i = 0; i < 10; i++) {
cy.apiCreateUser().then(({user: newUser}) => {
users.push(newUser);
cy.apiAddUserToTeam(teamId, newUser.id);
});
}
cy.apiCreateChannel(testTeam.id, 'channel-test', 'Channel').then(({channel}) => {
cy.apiCreateChannel(teamId, 'channel-test', 'Channel').then(({channel}) => {
addedUsersChannel = channel;
});
// # Change permission so that regular users can't add team members
cy.apiGetRolesByNames(['team_user']).then((result: any) => {
if (result.roles) {
const role = result.roles[0];
const permissions = role.permissions.filter((permission) => {
return !(['add_user_to_team'].includes(permission));
});
if (permissions.length !== role.permissions) {
cy.apiPatchRole(role.id, {permissions});
}
}
});
cy.apiLogin(firstUser);
}).then(() => {
groupId = getRandomId();
cy.apiCreateCustomUserGroup(`group${groupId}`, `group${groupId}`, [users[0].id, users[1].id]);
});
});
@ -42,7 +66,7 @@ describe('Channel Settings', () => {
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// # Add users to channel
addNumberOfUsersToChannel(1);
addNumberOfUsersToChannel(1, false);
cy.getLastPostId().then((id) => {
// * The system message should contain 'added to the channel by you'
@ -59,7 +83,7 @@ describe('Channel Settings', () => {
cy.apiCreateChannel(testTeam.id, 'channel-test', 'Channel').then(({channel}) => {
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
addNumberOfUsersToChannel(3);
addNumberOfUsersToChannel(3, false);
cy.getLastPostId().then((id) => {
cy.get(`#postMessageText_${id}`).should('contain', '2 others were added to the channel by you');
@ -82,10 +106,10 @@ describe('Channel Settings', () => {
cy.get('#addUsersToChannelModal').should('be.visible');
// # Type into the input box to search for a user
cy.get('#selectItems input').typeWithForce('u');
cy.get('#selectItems input').typeWithForce('user');
// # First add one user in order to see them disappearing from the list
cy.get('#multiSelectList > div').first().then((el) => {
cy.get('#multiSelectList > div').not(':contains("Already in channel")').first().then((el) => {
const childNodes = Array.from(el[0].childNodes);
childNodes.map((child: HTMLElement) => usernames.push(child.innerText));
@ -113,7 +137,7 @@ describe('Channel Settings', () => {
});
// Add two more users
addNumberOfUsersToChannel(2);
addNumberOfUsersToChannel(2, false);
// Verify that the system post reflects the number of added users
cy.getLastPostId().then((id) => {
@ -148,6 +172,179 @@ describe('Channel Settings', () => {
});
cy.get('body').type('{esc}');
});
it('Add group members to channel', () => {
cy.apiLogin(firstUser);
// # Create a new channel
cy.apiCreateChannel(testTeam.id, 'new-channel', 'New Channel').then(({channel}) => {
// # Visit the channel
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// # Open channel menu and click 'Add Members'
cy.uiOpenChannelMenu('Add Members');
// * Assert that modal appears
cy.get('#addUsersToChannelModal').should('be.visible');
// # Type 'group'+ id created in beforeAll into the input box
cy.get('#selectItems input').typeWithForce(`group${groupId}`);
// # Click the first row for a number of times
// cy.get('#multiSelectList').should('be.visible').first().click();
cy.get('#multiSelectList').should('exist').children().first().click();
// # Click the button "Add" to add user to a channel
cy.uiGetButton('Add').click();
// # Wait for the modal to disappear
cy.get('#addUsersToChannelModal').should('not.exist');
cy.getLastPostId().then((id) => {
// * The system message should contain 'added to the channel by you'
cy.get(`#postMessageText_${id}`).should('contain', 'added to the channel by you');
// # Verify username link
verifyMentionedUserAndProfilePopover(id);
});
// * Check that the number of channel members is 3
cy.get('#channelMemberCountText').
should('be.visible').
and('have.text', '3');
});
});
it('Add group members that are not team members', () => {
cy.apiAdminLogin();
// # Create a new user
cy.apiCreateUser().then(({user: newUser}) => {
const id = getRandomId();
// # Create a custom user group
cy.apiCreateCustomUserGroup(`newgroup${id}`, `newgroup${id}`, [newUser.id]).then(() => {
// # Create a new channel
cy.apiCreateChannel(testTeam.id, 'new-group-channel', 'New Group Channel').then(({channel}) => {
// # Visit a channel
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// # Open channel menu and click 'Add Members'
cy.uiOpenChannelMenu('Add Members');
// * Assert that modal appears
cy.get('#addUsersToChannelModal').should('be.visible');
// # Type 'group' into the input box
cy.get('#selectItems input').typeWithForce(`newgroup${id}`);
// # Click the first row for a number of times
// cy.get('#multiSelectList').should('be.visible').first().click();
cy.get('#multiSelectList').should('exist').children().first().click();
// * Check you get a warning when adding a non team member
cy.findByTestId('teamWarningBanner').should('contain', '1 user was not selected because they are not a part of this team');
// * Check the correct username is appearing in the team invite banner
cy.findByTestId('teamWarningBanner').should('contain', `@${newUser.username}`);
// # Click the button "Add" to add user to a channel
cy.uiGetButton('Cancel').click();
// # Wait for the modal to disappear
cy.get('#addUsersToChannelModal').should('not.exist');
});
});
});
});
it('Add group members and guests that are not team members', () => {
cy.apiAdminLogin();
// # Create a new user
cy.apiCreateUser().then(({user: newUser}) => {
// # Create a guest user
cy.apiCreateGuestUser({}).then(({guest}) => {
const id = getRandomId();
// # Create a custom user group
cy.apiCreateCustomUserGroup(`guestgroup${id}`, `guestgroup${id}`, [guest.id, newUser.id]).then(() => {
// # Create a new channel
cy.apiCreateChannel(testTeam.id, 'group-guest-channel', 'Channel').then(({channel}) => {
// # Visit a channel
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// # Open channel menu and click 'Add Members'
cy.uiOpenChannelMenu('Add Members');
// * Assert that modal appears
cy.get('#addUsersToChannelModal').should('be.visible');
// # Type 'group' into the input box
cy.get('#selectItems input').typeWithForce(`guestgroup${id}`);
// # Click the first row for a number of times
// cy.get('#multiSelectList').should('be.visible').first().click();
cy.get('#multiSelectList').should('exist').children().first().click();
// * Check you get a warning when adding a non team member
cy.findByTestId('teamWarningBanner').should('contain', '2 users were not selected because they are not a part of this team');
// * Check the correct username is appearing in the invite to team portion
cy.findByTestId('teamWarningBanner').should('contain', `@${newUser.username}`);
// * Check the guest username is in the warning message and won't be added to the team
cy.findByTestId('teamWarningBanner').should('contain', `@${guest.username} is a guest user`);
// # Click the button "Add" to add user to a channel
cy.uiGetButton('Cancel').click();
// # Wait for the modal to disappear
cy.get('#addUsersToChannelModal').should('not.exist');
});
});
});
});
});
it('User doesn\'t have permission to add user to team', () => {
cy.apiAdminLogin();
// # Create a new user
cy.apiCreateUser().then(({user: newUser}) => {
const id = getRandomId();
// # Create a custom user group
cy.apiCreateCustomUserGroup(`newgroup${id}`, `newgroup${id}`, [newUser.id]).then(() => {
// # Create a new channel
cy.apiCreateChannel(testTeam.id, 'new-group-channel', 'Channel').then(({channel}) => {
cy.apiLogin(firstUser);
// # Visit a channel
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// # Open channel menu and click 'Add Members'
cy.uiOpenChannelMenu('Add Members');
// * Assert that modal appears
cy.get('#addUsersToChannelModal').should('be.visible');
// # Type 'group' into the input box
cy.get('#selectItems input').typeWithForce(`newgroup${id}`);
// # Click the first row for a number of times
// cy.get('#multiSelectList').should('be.visible').first().click();
cy.get('#multiSelectList').should('exist').children().first().click();
// * Check you get a warning when adding a non team member
cy.findByTestId('teamWarningBanner').should('contain', '1 user was not selected because they are not a part of this team');
// * Check the correct username is appearing in the team invite banner
cy.findByTestId('teamWarningBanner').should('contain', `@${newUser.username}`);
});
});
});
});
});
function verifyMentionedUserAndProfilePopover(postId: string) {
@ -169,7 +366,7 @@ function verifyMentionedUserAndProfilePopover(postId: string) {
});
}
function addNumberOfUsersToChannel(num = 1) {
function addNumberOfUsersToChannel(num = 1, allowExisting = false) {
// # Open channel menu and click 'Add Members'
cy.uiOpenChannelMenu('Add Members');
cy.get('#addUsersToChannelModal').should('be.visible');
@ -177,8 +374,14 @@ function addNumberOfUsersToChannel(num = 1) {
// * Assert that modal appears
// # Click the first row for a number of times
Cypress._.times(num, () => {
cy.get('#selectItems input').typeWithForce('u');
cy.get('#multiSelectList').should('be.visible').first().click();
cy.get('#selectItems input').typeWithForce('user');
// cy.get('#multiSelectList').should('be.visible').first().click();
if (allowExisting) {
cy.get('#multiSelectList').should('exist').children().first().click();
} else {
cy.get('#multiSelectList').should('exist').children().not(':contains("Already in channel")').first().click();
}
});
// # Click the button "Add" to add user to a channel

View File

@ -25,10 +25,22 @@ describe('Collapsed Reply Threads', () => {
});
// # Log in as user that hasn't had CRT enabled before
cy.apiInitSetup({loginAfter: true, promoteNewUserAsAdmin: true, userPrefix: 'tipbutton'}).then(({team, channel}) => {
cy.apiInitSetup({loginAfter: true, promoteNewUserAsAdmin: true, userPrefix: 'tipbutton'}).then(({team, channel, user}) => {
testTeam = team;
testChannel = channel;
cy.apiSaveUserPreference([{
user_id: user.id,
category: 'crt_thread_pane_step',
name: user.id,
value: '0',
}, {
user_id: user.id,
category: 'crt_tutorial_triggered',
name: user.id,
value: '0',
}], user.id);
cy.apiCreateUser({prefix: 'other'}).then(({user: user1}) => {
otherUser = user1;

View File

@ -86,7 +86,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
});
});
it('MM-T1467 Accessibility Support in More Channels Dialog screen', () => {
it('MM-T1467 Accessibility Support in Browse Channels Dialog screen', () => {
function getChannelAriaLabel(channel) {
return channel.display_name.toLowerCase() + ', ' + channel.purpose.toLowerCase();
}
@ -117,8 +117,8 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
// # Hide already joined channels
cy.findByText('Hide Joined').click();
// # Focus on the Create Channel button and TAB three time
cy.get('#createNewChannelButton').focus().tab().tab().tab();
// # Focus on the Create Channel button and TAB four time
cy.get('#createNewChannelButton').focus().tab().tab().tab().tab();
// * Verify channel name is highlighted and reader reads the channel name and channel description
cy.get('#moreChannelsList').within(() => {
@ -155,11 +155,11 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
cy.findByRole('heading', {name: modalName});
// * Verify the accessibility support in search input
cy.findByRole('textbox', {name: 'Search for people'}).
cy.findByRole('textbox', {name: 'Search for people or groups'}).
should('have.attr', 'aria-autocomplete', 'list');
// # Search for a text and then check up and down arrow
cy.findByRole('textbox', {name: 'Search for people'}).
cy.findByRole('textbox', {name: 'Search for people or groups'}).
typeWithForce('u').
wait(TIMEOUTS.HALF_SEC).
typeWithForce('{downarrow}{downarrow}{downarrow}{uparrow}');
@ -184,7 +184,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
});
// # Search for an invalid text and check if reader can read no results
cy.findByRole('textbox', {name: 'Search for people'}).
cy.findByRole('textbox', {name: 'Search for people or groups'}).
typeWithForce('somethingwhichdoesnotexist').
wait(TIMEOUTS.HALF_SEC);

View File

@ -10,116 +10,6 @@
// Stage: @prod
// Group: @channels @cloud_only @cloud_trial
function simulateSubscription() {
cy.intercept('GET', '**/api/v4/cloud/subscription/invoices', {
statusCode: 200,
body: [
{
id: 'in_1Lz8b0I67GP2qpb43kcuMyFP',
number: '8D53267B-0006',
create_at: 1667263490000,
total: 65,
tax: 0,
status: 'open',
description: '',
period_start: 1664582400000,
period_end: 1667260800000,
subscription_id: 'sub_K0AxuWCDoDD9Qq',
line_items: [
{
price_id: 'price_1KLUYiI67GP2qpb48DXFukcJ',
total: 65,
quantity: 0.06451612903225806,
price_per_unit: 1000,
description: 'Cloud Professional',
},
],
current_product_name: 'Cloud Professional',
},
{
id: 'in_1LntnKI67GP2qpb4VObu3NgP',
number: '8D53267B-0005',
create_at: 1664584986000,
total: 733,
tax: 0,
status: 'failed',
description: '',
period_start: 1661990400000,
period_end: 1664582400000,
subscription_id: 'sub_K0AxuWCDoDD9Qq',
line_items: [
{
price_id: 'price_1KLUZ2I67GP2qpb45uTS89eb',
total: 733,
quantity: 0.7333333333333333,
price_per_unit: 999,
description: 'Cloud Professional',
},
],
current_product_name: 'Cloud Professional',
},
{
id: 'in_1LntnKI67GP2qpb4VObu3NgV',
number: '8D53267B-0005',
create_at: 1664584986000,
total: 733,
tax: 0,
status: 'paid',
description: '',
period_start: 1661990400000,
period_end: 1664582400000,
subscription_id: 'sub_K0AxuWCDoDD9Qq',
line_items: [
{
price_id: 'price_1KLUZ2I67GP2qpb45uTS89eb',
total: 733,
quantity: 0.7333333333333333,
price_per_unit: 999,
description: 'Cloud Professional',
},
],
current_product_name: 'Cloud Professional',
},
],
});
cy.intercept('GET', '**/api/v4/cloud/subscription', {
statusCode: 200,
body: {
id: 'sub_test1',
is_free_trial: 'true',
customer_id: '5zqhakmibpgyix9juiwwkpfnmr',
product_id: 'prod_K0AxuWCDoDD9Qq',
seats: 25,
status: 'active',
},
});
cy.intercept('GET', '**/api/v4/cloud/products**', {
statusCode: 200,
body:
[
{
id: 'prod_LSBESgGXq9KlLj',
sku: 'cloud-starter',
price_per_seat: 0,
name: 'Cloud Free',
},
{
id: 'prod_K0AxuWCDoDD9Qq',
sku: 'cloud-professional',
price_per_seat: 10,
name: 'Cloud Professional',
},
{
id: 'prod_Jh6tBLcgWWOOog',
sku: 'cloud-enterprise',
price_per_seat: 30,
name: 'Cloud Enterprise',
},
],
});
}
describe('System Console - Billing History', () => {
before(() => {
simulateSubscription();
@ -166,6 +56,116 @@ describe('System Console - Billing History', () => {
cy.get('@tableRows').eq(1).find('td').eq(4).find('a').should('have.attr', 'href').and('include', 'invoices/in_1LntnKI67GP2qpb4VObu3NgP/pdf');
cy.get('@tableRows').eq(2).find('td').eq(4).find('a').should('have.attr', 'href').and('include', 'invoices/in_1LntnKI67GP2qpb4VObu3NgV/pdf');
});
function simulateSubscription() {
cy.intercept('GET', '**/api/v4/cloud/subscription/invoices', {
statusCode: 200,
body: [
{
id: 'in_1Lz8b0I67GP2qpb43kcuMyFP',
number: '8D53267B-0006',
create_at: 1667263490000,
total: 65,
tax: 0,
status: 'open',
description: '',
period_start: 1664582400000,
period_end: 1667260800000,
subscription_id: 'sub_K0AxuWCDoDD9Qq',
line_items: [
{
price_id: 'price_1KLUYiI67GP2qpb48DXFukcJ',
total: 65,
quantity: 0.06451612903225806,
price_per_unit: 1000,
description: 'Cloud Professional',
},
],
current_product_name: 'Cloud Professional',
},
{
id: 'in_1LntnKI67GP2qpb4VObu3NgP',
number: '8D53267B-0005',
create_at: 1664584986000,
total: 733,
tax: 0,
status: 'failed',
description: '',
period_start: 1661990400000,
period_end: 1664582400000,
subscription_id: 'sub_K0AxuWCDoDD9Qq',
line_items: [
{
price_id: 'price_1KLUZ2I67GP2qpb45uTS89eb',
total: 733,
quantity: 0.7333333333333333,
price_per_unit: 999,
description: 'Cloud Professional',
},
],
current_product_name: 'Cloud Professional',
},
{
id: 'in_1LntnKI67GP2qpb4VObu3NgV',
number: '8D53267B-0005',
create_at: 1664584986000,
total: 733,
tax: 0,
status: 'paid',
description: '',
period_start: 1661990400000,
period_end: 1664582400000,
subscription_id: 'sub_K0AxuWCDoDD9Qq',
line_items: [
{
price_id: 'price_1KLUZ2I67GP2qpb45uTS89eb',
total: 733,
quantity: 0.7333333333333333,
price_per_unit: 999,
description: 'Cloud Professional',
},
],
current_product_name: 'Cloud Professional',
},
],
});
cy.intercept('GET', '**/api/v4/cloud/subscription', {
statusCode: 200,
body: {
id: 'sub_test1',
is_free_trial: 'true',
customer_id: '5zqhakmibpgyix9juiwwkpfnmr',
product_id: 'prod_K0AxuWCDoDD9Qq',
seats: 25,
status: 'active',
},
});
cy.intercept('GET', '**/api/v4/cloud/products**', {
statusCode: 200,
body:
[
{
id: 'prod_LSBESgGXq9KlLj',
sku: 'cloud-starter',
price_per_seat: 0,
name: 'Cloud Free',
},
{
id: 'prod_K0AxuWCDoDD9Qq',
sku: 'cloud-professional',
price_per_seat: 10,
name: 'Cloud Professional',
},
{
id: 'prod_Jh6tBLcgWWOOog',
sku: 'cloud-enterprise',
price_per_seat: 30,
name: 'Cloud Enterprise',
},
],
});
}
});
describe('System Console - Empty Billing Screen', () => {

View File

@ -10,45 +10,6 @@
// Stage: @prod
// Group: @channels @cloud_only @cloud_trial
function simulateSubscription() {
cy.intercept('GET', '**/api/v4/cloud/subscription', {
statusCode: 200,
body: {
id: 'sub_test1',
is_free_trial: 'true',
customer_id: '5zqhakmibpgyix9juiwwkpfnmr',
product_id: 'prod_Jh6tBLcgWWOOog',
seats: 25,
status: 'active',
},
});
cy.intercept('GET', '**/api/v4/cloud/products**', {
statusCode: 200,
body:
[
{
id: 'prod_LSBESgGXq9KlLj',
sku: 'cloud-starter',
price_per_seat: 0,
name: 'Cloud Free',
},
{
id: 'prod_K0AxuWCDoDD9Qq',
sku: 'cloud-professional',
price_per_seat: 10,
name: 'Cloud Professional',
},
{
id: 'prod_Jh6tBLcgWWOOog',
sku: 'cloud-enterprise',
price_per_seat: 30,
name: 'Cloud Enterprise',
},
],
});
}
describe('System Console - Payment Information section', () => {
before(() => {
// * Check if server has license for Cloud
@ -116,5 +77,43 @@ describe('System Console - Payment Information section', () => {
cy.get('span.savedFeedback__text').should('be.visible').should('have.text', 'Thanks for sharing feedback!');
cy.get('button#feedbackSubmitedDone').should('be.enabled').click();
});
});
function simulateSubscription() {
cy.intercept('GET', '**/api/v4/cloud/subscription', {
statusCode: 200,
body: {
id: 'sub_test1',
is_free_trial: 'true',
customer_id: '5zqhakmibpgyix9juiwwkpfnmr',
product_id: 'prod_Jh6tBLcgWWOOog',
seats: 25,
status: 'active',
},
});
cy.intercept('GET', '**/api/v4/cloud/products**', {
statusCode: 200,
body:
[
{
id: 'prod_LSBESgGXq9KlLj',
sku: 'cloud-starter',
price_per_seat: 0,
name: 'Cloud Free',
},
{
id: 'prod_K0AxuWCDoDD9Qq',
sku: 'cloud-professional',
price_per_seat: 10,
name: 'Cloud Professional',
},
{
id: 'prod_Jh6tBLcgWWOOog',
sku: 'cloud-enterprise',
price_per_seat: 30,
name: 'Cloud Enterprise',
},
],
});
}
});

View File

@ -10,37 +10,6 @@
// Stage: @prod
// Group: @channels @enterprise @not_cloud @system_console
// # Goes to the System Scheme page as System Admin
const goToSessionLengths = () => {
cy.apiAdminLogin();
cy.visit('/admin_console/environment/session_lengths');
};
// # Wait's until the Saving text becomes Save
const waitUntilConfigSave = () => {
cy.waitUntil(() => cy.get('#saveSetting').then((el) => {
return el[0].innerText === 'Save';
}));
};
// Clicks the save button in the system console page.
// waitUntilConfigSaved: If we need to wait for the save button to go from saving -> save.
// Usually we need to wait unless we are doing this in team override scheme
const saveConfig = (waitUntilConfigSaved = true, clickConfirmationButton = false) => {
// # Save if possible (if previous test ended abruptly all permissions may already be enabled)
cy.get('#saveSetting').then((btn) => {
if (btn.is(':enabled')) {
btn.click();
}
});
if (clickConfirmationButton) {
cy.get('#confirmModalButton').click();
}
if (waitUntilConfigSaved) {
waitUntilConfigSave();
}
};
describe('MM-T2574 Session Lengths', () => {
before(() => {
cy.shouldNotRunOnCloudEdition();
@ -125,4 +94,35 @@ describe('MM-T2574 Session Lengths', () => {
}
});
});
// # Goes to the System Scheme page as System Admin
const goToSessionLengths = () => {
cy.apiAdminLogin();
cy.visit('/admin_console/environment/session_lengths');
};
// # Wait's until the Saving text becomes Save
const waitUntilConfigSave = () => {
cy.waitUntil(() => cy.get('#saveSetting').then((el) => {
return el[0].innerText === 'Save';
}));
};
// Clicks the save button in the system console page.
// waitUntilConfigSaved: If we need to wait for the save button to go from saving -> save.
// Usually we need to wait unless we are doing this in team override scheme
const saveConfig = (waitUntilConfigSaved = true, clickConfirmationButton = false) => {
// # Save if possible (if previous test ended abruptly all permissions may already be enabled)
cy.get('#saveSetting').then((btn) => {
if (btn.is(':enabled')) {
btn.click();
}
});
if (clickConfirmationButton) {
cy.get('#confirmModalButton').click();
}
if (waitUntilConfigSaved) {
waitUntilConfigSave();
}
};
});

View File

@ -232,9 +232,9 @@ context('ldap', () => {
cy.visit(`/${testTeam.name}`);
cy.uiBrowseOrCreateChannel('Browse channels').click();
// * Search private channel name and make sure it isn't there in public channel directory
// * Search private channel name and make sure it is still visible
cy.get('#searchChannelsTextbox').type(testChannel.display_name);
cy.get('#moreChannelsList').should('include.text', 'No results for');
cy.get('#moreChannelsList').should('include.text', testChannel.display_name);
});
it('MM-T2629 - Private to public - More....', () => {

View File

@ -9,26 +9,6 @@
// Group: @channels @enterprise @not_cloud
function withTrialBefore(trialed: string) {
cy.intercept('GET', '**/api/v4/trial-license/prev', {
statusCode: 200,
body: {
IsLicensed: trialed,
IsTrial: trialed,
},
});
}
function withTrialLicense(trial: string) {
cy.intercept('GET', '**/api/v4/license/client?format=old', {
statusCode: 200,
body: {
IsLicensed: 'true',
IsTrial: trial,
},
});
}
describe('Self hosted Pricing modal', () => {
let urlL: string | undefined;
let createdUser: Cypress.UserProfile | undefined;
@ -191,4 +171,24 @@ describe('Self hosted Pricing modal', () => {
cy.findByText('https://mattermost.com/pl/pricing/#self-hosted').last().should('exist');
});
function withTrialBefore(trialed: string) {
cy.intercept('GET', '**/api/v4/trial-license/prev', {
statusCode: 200,
body: {
IsLicensed: trialed,
IsTrial: trialed,
},
});
}
function withTrialLicense(trial: string) {
cy.intercept('GET', '**/api/v4/license/client?format=old', {
statusCode: 200,
body: {
IsLicensed: 'true',
IsTrial: trial,
},
});
}
});

View File

@ -119,7 +119,7 @@ describe('Channel members test', () => {
cy.get('#addChannelMembers').click();
// # Enter user1 and user2 emails
cy.findByRole('textbox', {name: 'Search for people'}).typeWithForce(`${user1.email}{enter}${user2.email}{enter}`);
cy.findByRole('textbox', {name: 'Search for people or groups'}).typeWithForce(`${user1.email}{enter}${user2.email}{enter}`);
// # Confirm add the users
cy.get('#addUsersToChannelModal #saveItems').click();

View File

@ -148,7 +148,7 @@ describe('Compliance Export', () => {
cy.findByTestId('enableComplianceExportfalse').click();
// * Verify that exported button is disabled
cy.findByRole('button', {name: /run compliance export job now/i}).should('be.disabled');
cy.findByRole('button', {name: /run compliance export job now/i}).should('be.enabled');
});
it('MM-T1167 - Compliance Export job can be canceled', () => {

View File

@ -140,6 +140,7 @@ describe('Upload Files - Settings', () => {
},
}],
types: [],
getData: () => {},
}});
// * An error should be visible saying 'File attachments are disabled'
@ -165,6 +166,7 @@ describe('Upload Files - Settings', () => {
},
}],
types: [],
getData: () => {},
}});
// * An error should be visible saying 'File attachments are disabled'

View File

@ -41,6 +41,7 @@ describe('Paste Image', () => {
},
}],
types: [],
getData: () => {},
}});
cy.uiWaitForFileUploadPreview();

View File

@ -84,16 +84,18 @@ describe('Interactive Dialog', () => {
if (index === 0) {
expect(element.name).to.equal('someuserselector');
cy.wrap($elForm).find('.suggestion-list__item').first().should('be.visible');
cy.wrap($elForm).find('.form-control').type('{downarrow}'.repeat(10));
cy.wrap($elForm).find('.form-control').type('{uparrow}', {force: true});
cy.wrap($elForm).find('.form-control').type('{downarrow}'.repeat(10), {force: true});
cy.wrap($elForm).find('.suggestion-list__item').first().should('not.be.visible');
cy.wrap($elForm).find('.form-control').type('{uparrow}'.repeat(10));
cy.wrap($elForm).find('.form-control').type('{uparrow}'.repeat(10), {force: true});
cy.wrap($elForm).find('.suggestion-list__item').first().should('be.visible');
} else if (index === 1) {
expect(element.name).to.equal('somechannelselector');
cy.wrap($elForm).find('.suggestion-list__item').first().should('be.visible');
cy.wrap($elForm).find('.form-control').type('{downarrow}'.repeat(10));
cy.wrap($elForm).find('.form-control').type('{uparrow}', {force: true});
cy.wrap($elForm).find('.form-control').type('{downarrow}'.repeat(10), {force: true});
cy.wrap($elForm).find('.suggestion-list__item').first().should('not.be.visible');
cy.wrap($elForm).find('.form-control').type('{uparrow}'.repeat(10));
cy.wrap($elForm).find('.form-control').type('{uparrow}'.repeat(10), {force: true});
cy.wrap($elForm).find('.suggestion-list__item').first().should('be.visible');
}

View File

@ -29,12 +29,14 @@ describe('Keyboard Shortcuts', () => {
cy.get('body').cmdOrCtrlShortcut('K');
cy.get('#quickSwitchInput').type('T');
// * Confirm the town-square channel is selected in the suggestion list
cy.get('#suggestionList').findByTestId('town-square').should('be.visible').and('have.class', 'suggestion--selected');
// # Press down arrow
cy.wait(TIMEOUTS.HALF_SEC);
cy.get('body').type('{downarrow}');
cy.get('body').type('{downarrow}');
// * Confirm the offtopic channel is selected in the suggestion list
// * Confirm the off-topic channel is selected in the suggestion list
cy.get('#suggestionList').findByTestId('off-topic').should('be.visible').and('have.class', 'suggestion--selected');
// # Press ENTER

View File

@ -94,10 +94,8 @@ describe('Keyboard Shortcuts', () => {
});
cy.getLastPostId().then((postId) => {
// * verify Save not shown in webview
cy.findByText('Save').should('not.exist');
cy.viewport('iphone-6');
// * verify Saved not shown in webview
cy.findByText('Saved').should('not.exist');
// # Save Post
cy.uiPostDropdownMenuShortcut(postId, 'Save', 'S', 'RHS_ROOT');

View File

@ -42,6 +42,7 @@ describe('Messaging', () => {
cy.apiCreateGroupChannel([firstUser.id, secondUser.id, thirdUser.id]).then(({channel}) => {
// # Visit the newly created group message
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
cy.postMessage('hello');
// # Go to off-topic
cy.get('#sidebarItem_off-topic').click();

View File

@ -36,11 +36,11 @@ describe('Show GIF images properly', () => {
cy.url().should('include', offtopiclink);
// # Post tenor GIF
cy.postMessage('https://c.tenor.com/Ztva2YFROSkAAAAi/duck-searching.gif');
cy.postMessage('https://media.tenor.com/yCFHzEvKa9MAAAAi/hello.gif');
cy.getLastPostId().as('postId').then((postId) => {
// * Validate image size
cy.get(`#post_${postId}`).find('.attachment__image').should('have.css', 'width', '137px');
cy.get(`#post_${postId}`).find('.attachment__image').should('have.css', 'width', '189px');
});
// # Post giphy GIF

View File

@ -23,12 +23,12 @@ describe('Notifications', () => {
// # Open 'Settings' modal
cy.uiOpenSettingsModal().within(() => {
// # Open 'Words That Trigger Mentions' setting and uncheck all the checkboxes
cy.findByRole('heading', {name: 'Words That Trigger Mentions'}).should('be.visible').click();
// # Open 'Keywords that trigger Notifications' setting and uncheck all the checkboxes
cy.findByRole('heading', {name: 'Keywords that trigger Notifications'}).should('be.visible').click();
cy.findByRole('checkbox', {name: `Your case-sensitive first name "${otherUser.first_name}"`}).should('not.be.checked');
cy.findByRole('checkbox', {name: `Your non case-sensitive username "${otherUser.username}"`}).should('not.be.checked');
cy.findByRole('checkbox', {name: 'Channel-wide mentions "@channel", "@all", "@here"'}).click().should('not.be.checked');
cy.findByRole('checkbox', {name: 'Other non case-sensitive words, separated by commas:'}).should('not.be.checked');
cy.findByRole('checkbox', {name: 'Other non case-sensitive words, press Tab or use commas to separate keywords:'}).should('not.be.checked');
// # Save then close the modal
cy.uiSaveAndClose();

View File

@ -243,14 +243,16 @@ function setNotificationSettings(desiredSettings = {first: true, username: true,
// Navigate to settings modal
cy.uiOpenSettingsModal();
// Notifications header should be visible
cy.get('#notificationSettingsTitle').
scrollIntoView().
// # Click on notifications tab
cy.findByRoleExtended('tab', {name: 'Notifications'}).
should('be.visible').
and('contain', 'Notifications');
click();
// Notifications header should be visible
cy.findAllByText('Notifications').should('be.visible');
// Open up 'Words that trigger mentions' sub-section
cy.get('#keysTitle').
cy.findByText('Keywords that trigger Notifications').
scrollIntoView().
click();
@ -270,8 +272,8 @@ function setNotificationSettings(desiredSettings = {first: true, username: true,
// Set Custom field
if (desiredSettings.custom && desiredSettings.customText) {
cy.get('#notificationTriggerCustomText').
clear().
type(desiredSettings.customText);
type(desiredSettings.customText, {force: true}).
tab();
}
// Click “Save” and close modal

View File

@ -29,14 +29,16 @@ describe('Notifications', () => {
// # Open 'Settings' modal
cy.uiOpenSettingsModal().within(() => {
cy.get('#keysEdit').should('be.visible').click();
// # Open 'Keywords that trigger Notifications' setting
cy.findByRole('heading', {name: 'Keywords that trigger Notifications'}).should('be.visible').click();
// * As otherUser, ensure that 'Your non-case sensitive username' is not checked
cy.get('#notificationTriggerUsername').should('not.be.checked');
cy.findByRole('checkbox', {name: `Your non case-sensitive username "${otherUser.username}"`}).should('not.be.checked');
// # Close the modal
cy.get('#accountSettingsHeader').find('button').should('be.visible').click();
// # Save then close the modal
cy.uiSaveAndClose();
});
cy.apiLogout();
// # Login as sysadmin

View File

@ -17,6 +17,7 @@ describe('Plugin Marketplace', () => {
before(() => {
cy.shouldNotRunOnCloudEdition();
cy.shouldHaveFeatureFlag('StreamlinedMarketplace', 'false'); // https://mattermost.atlassian.net/browse/MM-54230
cy.shouldHavePluginUploadEnabled();
cy.apiInitSetup().then(({team}) => {
@ -72,6 +73,7 @@ describe('Plugin Marketplace', () => {
cy.get('#marketplaceTabs-pane-allListing').should('be.visible');
});
// https://mattermost.atlassian.net/browse/MM-54230
it('MM-T2001 autofocus on search plugin input box', () => {
cy.uiClose();
@ -82,6 +84,7 @@ describe('Plugin Marketplace', () => {
cy.findByPlaceholderText('Search Marketplace').should('be.focused');
});
// https://mattermost.atlassian.net/browse/MM-54230
it('render the list of all plugins by default', () => {
// * Verify all plugins tab should be active
cy.get('#marketplaceTabs-tab-allListing').should('be.visible').parent().should('have.class', 'active');
@ -92,6 +95,7 @@ describe('Plugin Marketplace', () => {
cy.get('#marketplaceTabs-pane-installed').should('not.exist');
});
// https://mattermost.atlassian.net/browse/MM-54230
// this test uses exist, not visible, due to issues with Cypress
it('render the list of installed plugins on demand', () => {
// # Click on installed plugins tab
@ -117,6 +121,7 @@ describe('Plugin Marketplace', () => {
cy.get('#modal_marketplace').should('not.exist');
});
// https://mattermost.atlassian.net/browse/MM-54230
it('should filter all on search', () => {
// # Load all plugins before searching
cy.get('.more-modal__row').should('have.length', 15);
@ -136,6 +141,7 @@ describe('Plugin Marketplace', () => {
should('have.length', 1);
});
// https://mattermost.atlassian.net/browse/MM-54230
it('should show an error bar on failing to filter', () => {
// # Enable Plugin Marketplace
cy.apiUpdateConfig({
@ -168,6 +174,7 @@ describe('Plugin Marketplace', () => {
cy.get('#marketplace-plugin-com\\.mattermost\\.webex').find('.btn.btn-outline', {timeout: TIMEOUTS.ONE_MIN}).scrollIntoView().should('be.visible').and('have.text', 'Configure');
});
// https://mattermost.atlassian.net/browse/MM-54230
it('should install a plugin from search results on demand', () => {
// # Uninstall any existing webex plugin
cy.apiRemovePluginById('com.mattermost.webex');
@ -226,6 +233,7 @@ describe('Plugin Marketplace', () => {
cy.get('#marketplace-plugin-github').should('be.visible');
});
// https://mattermost.atlassian.net/browse/MM-54230
it('MM-T1986 change tab to "All Plugins" when "Install Plugins" link is clicked', () => {
cy.get('#marketplaceTabs').scrollIntoView().should('be.visible').within(() => {
// # Switch tab to installed plugin

View File

@ -26,7 +26,7 @@ describe('channels > App Bar', {testIsolation: true}, () => {
describe('App Bar disabled', () => {
it('should not show the Playbook App Bar icon', () => {
cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: false}});
cy.apiUpdateConfig({ExperimentalSettings: {DisableAppBar: true}});
// # Login as testUser
cy.apiLogin(testUser);
@ -41,7 +41,7 @@ describe('channels > App Bar', {testIsolation: true}, () => {
describe('App Bar enabled', () => {
beforeEach(() => {
cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: true}});
cy.apiUpdateConfig({ExperimentalSettings: {DisableAppBar: false}});
// # Login as testUser
cy.apiLogin(testUser);

View File

@ -47,7 +47,7 @@ describe('channels > channel header', {testIsolation: true}, () => {
describe('App Bar enabled', () => {
it('webapp should hide the Playbook channel header button', () => {
cy.apiAdminLogin();
cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: true}});
cy.apiUpdateConfig({ExperimentalSettings: {DisableAppBar: false}});
// # Login as testUser
cy.apiLogin(testUser);
@ -65,7 +65,7 @@ describe('channels > channel header', {testIsolation: true}, () => {
describe('App Bar disabled', () => {
beforeEach(() => {
cy.apiAdminLogin();
cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: false}});
cy.apiUpdateConfig({ExperimentalSettings: {DisableAppBar: true}});
// # Login as testUser
cy.apiLogin(testUser);

View File

@ -164,7 +164,7 @@ describe('channels > rhs > checklist', {testIsolation: true}, () => {
});
// * Verify the expected error message.
cy.verifyEphemeralMessage('Failed to find slash command /invalid');
cy.verifyEphemeralMessage('Failed to execute slash command /invalid');
});
it('successfully runs a valid slash command', () => {

View File

@ -84,7 +84,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
cy.findByTestId('post_textbox').clear().type('/playbook check ');
// * Verify suggestions number: a single run with 4 tasks + 1 title
cy.get('.slash-command').should('have.length', 5);
cy.get('.slash-command__info').should('have.length', 5);
// # Clear input
cy.findByTestId('post_textbox').clear();
@ -295,7 +295,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
cy.findByTestId('post_textbox').clear().type('/playbook check ');
// * Verify suggestions number: 2 runs * 4 tasks + 1 title
cy.get('.slash-command').should('have.length', 9);
cy.get('.slash-command__info').should('have.length', 9);
// # Clear input
cy.findByTestId('post_textbox').clear();
@ -337,7 +337,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
cy.findByTestId('post_textbox').clear().type('/playbook checkadd ');
// * Verify suggestions number: 2 runs * 2 checklists + 1 title
cy.get('.slash-command').should('have.length', 5);
cy.get('.slash-command__info').should('have.length', 5);
// # Clear input
cy.findByTestId('post_textbox').clear();
@ -380,7 +380,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
cy.findByTestId('post_textbox').clear().type('/playbook checkremove ');
// * Verify suggestions number: 2 runs * 4 tasks + 1 title
cy.get('.slash-command').should('have.length', 9);
cy.get('.slash-command__info').should('have.length', 9);
// # Clear input
cy.findByTestId('post_textbox').clear();
@ -423,7 +423,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
cy.findByTestId('post_textbox').clear().type('/playbook owner ');
// * Verify suggestions number: 2 runs + 1 title
cy.get('.slash-command').should('have.length', 3);
cy.get('.slash-command__info').should('have.length', 3);
// # Clear input
cy.findByTestId('post_textbox').clear();
@ -465,7 +465,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
cy.findByTestId('post_textbox').clear().type('/playbook finish ');
// * Verify suggestions number: 2 runs + 1 title
cy.get('.slash-command').should('have.length', 3);
cy.get('.slash-command__info').should('have.length', 3);
// # Clear input
cy.findByTestId('post_textbox').clear();
@ -528,7 +528,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
cy.findByTestId('post_textbox').clear().type('/playbook update ');
// * Verify suggestions number: 2 runs + 1 title
cy.get('.slash-command').should('have.length', 3);
cy.get('.slash-command__info').should('have.length', 3);
// # Clear input
cy.findByTestId('post_textbox').clear();

View File

@ -43,7 +43,7 @@ declare namespace Cypress {
* @example
* cy.apiGetBots();
*/
apiGetBots(page: number, perPage: number, includeDeleted: boolean): Chainable<{bots: Bot[]}>;
apiGetBots(page?: number, perPage?: number, includeDeleted?: boolean): Chainable<{bots: Bot[]}>;
/**
* Disable bot.

View File

@ -1,39 +1,117 @@
{
"ServiceSettings": {
"SiteURL": "http://localhost:8065",
"WebsocketURL": "",
"LicenseFileLocation": "",
"ListenAddress": ":8065",
"ConnectionSecurity": "",
"TLSCertFile": "",
"TLSKeyFile": "",
"TLSMinVer": "1.2",
"TLSStrictTransport": false,
"TLSStrictTransportMaxAge": 63072000,
"TLSOverwriteCiphers": [],
"UseLetsEncrypt": false,
"LetsEncryptCertificateCacheFile": "./config/letsencrypt.cache",
"Forward80To443": false,
"TrustedProxyIPHeader": [],
"ReadTimeout": 300,
"WriteTimeout": 300,
"IdleTimeout": 300,
"MaximumLoginAttempts": 10,
"GoroutineHealthThreshold": -1,
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": true,
"EnableOutgoingWebhooks": true,
"EnableCommands": true,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
"GoogleDeveloperKey": "",
"EnableLinkPreviews": false,
"EnablePermalinkPreviews": true,
"RestrictLinkPreviews": "",
"EnableTesting": false,
"EnableDeveloper": false,
"DeveloperFlags": "",
"EnableClientPerformanceDebugging": false,
"EnableOpenTracing": false,
"EnableSecurityFixAlert": true,
"EnableInsecureOutgoingConnections": false,
"AllowedUntrustedInternalConnections": "localhost",
"EnableMultifactorAuthentication": false,
"EnforceMultifactorAuthentication": false,
"EnableUserAccessTokens": false,
"AllowCorsFrom": "",
"CorsExposedHeaders": "",
"CorsAllowCredentials": false,
"CorsDebug": false,
"AllowCookiesForSubdomains": false,
"ExtendSessionLengthWithActivity": true,
"SessionLengthWebInDays": 30,
"SessionLengthWebInHours": 720,
"SessionLengthMobileInDays": 30,
"SessionLengthMobileInHours": 720,
"SessionLengthSSOInDays": 30,
"SessionLengthSSOInHours": 720,
"SessionCacheInMinutes": 10,
"SessionIdleTimeoutInMinutes": 43200,
"WebsocketSecurePort": 443,
"WebsocketPort": 80,
"WebserverMode": "gzip",
"EnableGifPicker": false,
"GiphySdkKey": "s0glxvzVg9azvPipKxcPLpXV0q1x1fVP",
"EnableCustomEmoji": false,
"EnableEmojiPicker": true,
"EnableGifPicker": false,
"GfycatAPIKey": "2_KtH_W5",
"GfycatAPISecret": "3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof",
"PostEditTimeLimit": -1,
"TimeBetweenUserTypingUpdatesMilliseconds": 5000,
"EnablePostSearch": true,
"EnableFileSearch": true,
"MinimumHashtagLength": 3,
"EnableUserTypingMessages": true,
"EnableChannelViewedMessages": true,
"EnableUserStatuses": true,
"ExperimentalEnableAuthenticationTransfer": true,
"ClusterLogTimeoutMilliseconds": 2000,
"EnablePreviewFeatures": true,
"EnableTutorial": true,
"EnableOnboardingFlow": false,
"ExperimentalEnableDefaultChannelLeaveJoinMessages": true,
"ExperimentalGroupUnreadChannels": "disabled",
"EnableAPITeamDeletion": true,
"EnableAPITriggerAdminNotifications": false,
"EnableAPIUserDeletion": false,
"ExperimentalEnableHardenedMode": false,
"ExperimentalStrictCSRFEnforcement": false,
"EnableEmailInvitations": true,
"DisableBotsWhenOwnerIsDeactivated": true,
"EnableBotAccountCreation": true,
"EnableSVGs": true,
"EnableLatex": false,
"EnableLegacySidebar": false,
"EnableInlineLatex": true,
"PostPriority": true,
"AllowPersistentNotifications": true,
"AllowPersistentNotificationsForGuests": false,
"PersistentNotificationIntervalMinutes": 5,
"PersistentNotificationMaxCount": 6,
"PersistentNotificationMaxRecipients": 5,
"EnableAPIChannelDeletion": false,
"EnableLocalMode": false,
"LocalModeSocketLocation": "/var/tmp/mattermost_local.socket",
"EnableAWSMetering": false,
"SplitKey": "",
"FeatureFlagSyncIntervalSeconds": 30,
"DebugSplit": false,
"ThreadAutoFollow": true,
"CollapsedThreads": "disabled"
"CollapsedThreads": "disabled",
"ManagedResourcePaths": "",
"EnableCustomGroups": true,
"SelfHostedPurchase": true,
"AllowSyncedDrafts": true
},
"TeamSettings": {
"SiteName": "Mattermost",
"MaxUsersPerTeam": 2000,
"EnableJoinLeaveMessageByDefault": true,
"EnableUserCreation": true,
"EnableOpenServer": true,
"EnableUserDeactivation": false,
@ -43,6 +121,7 @@
"CustomBrandText": "",
"CustomDescriptionText": "",
"RestrictDirectMessage": "any",
"EnableLastActiveTime": true,
"UserStatusAwayTimeout": 300,
"MaxChannelsPerTeam": 2000,
"MaxNotificationsPerChannel": 1000,
@ -54,12 +133,19 @@
"ExperimentalPrimaryTeam": "",
"ExperimentalDefaultChannels": []
},
"ClientRequirements": {
"AndroidLatestVersion": "",
"AndroidMinVersion": "",
"IosLatestVersion": "",
"IosMinVersion": ""
},
"PasswordSettings": {
"MinimumLength": 5,
"Lowercase": false,
"Number": false,
"Uppercase": false,
"Symbol": false
"Symbol": false,
"EnableForgotLink": true
},
"EmailSettings": {
"EnableSignUpWithEmail": true,
@ -69,14 +155,27 @@
"UseChannelInEmailNotifications": false,
"RequireEmailVerification": false,
"FeedbackName": "",
"FeedbackEmail": "test@example.com",
"ReplyToAddress": "test@example.com",
"FeedbackOrganization": "",
"EnableSMTPAuth": false,
"SMTPUsername": "",
"SMTPPassword": "",
"SMTPServer": "localhost",
"SMTPPort": "10025",
"SMTPServerTimeout": 10,
"ConnectionSecurity": "",
"SendPushNotifications": true,
"PushNotificationServer": "https://push-test.mattermost.com",
"PushNotificationServerType": "custom",
"PushNotificationServerLocation": "us",
"PushNotificationContents": "generic",
"PushNotificationBuffer": 1000,
"EnableEmailBatching": false,
"EmailBatchingBufferSize": 256,
"EmailBatchingInterval": 30,
"EnablePreviewModeBanner": true,
"SkipServerCertificateVerification": false,
"EmailNotificationContentsType": "full",
"LoginButtonColor": "#0000",
"LoginButtonBorderColor": "#2389D7",
@ -87,6 +186,12 @@
"ShowFullName": true
},
"SupportSettings": {
"TermsOfServiceLink": "https://mattermost.com/pl/terms-of-use/",
"PrivacyPolicyLink": "https://mattermost.com/pl/privacy-policy/",
"AboutLink": "https://mattermost.com/pl/about-mattermost",
"HelpLink": "https://mattermost.com/pl/help/",
"ReportAProblemLink": "https://mattermost.com/pl/report-a-bug",
"ForgotPasswordLink": "",
"SupportEmail": "",
"CustomTermsOfServiceEnabled": false,
"CustomTermsOfServiceReAcceptancePeriod": 365,
@ -99,7 +204,10 @@
"BannerTextColor": "#333333",
"AllowBannerDismissal": true,
"AdminNoticesEnabled": false,
"UserNoticesEnabled": false
"UserNoticesEnabled": false,
"NoticesURL": "https://notices.mattermost.com/",
"NoticesFetchFrequency": 3600,
"NoticesSkipCache": false
},
"ThemeSettings": {
"EnableThemeSelection": true,
@ -114,7 +222,10 @@
"Scope": "",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserAPIEndpoint": ""
"UserAPIEndpoint": "",
"DiscoveryEndpoint": "",
"ButtonText": "",
"ButtonColor": ""
},
"GoogleSettings": {
"Enable": false,
@ -123,7 +234,10 @@
"Scope": "profile email",
"AuthEndpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"TokenEndpoint": "https://www.googleapis.com/oauth2/v4/token",
"UserAPIEndpoint": "https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses,nicknames,metadata"
"UserAPIEndpoint": "https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses,nicknames,metadata",
"DiscoveryEndpoint": "",
"ButtonText": "",
"ButtonColor": ""
},
"Office365Settings": {
"Enable": false,
@ -133,8 +247,21 @@
"AuthEndpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
"TokenEndpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
"UserAPIEndpoint": "https://graph.microsoft.com/v1.0/me",
"DiscoveryEndpoint": "",
"DirectoryId": ""
},
"OpenIdSettings": {
"Enable": false,
"Secret": "",
"Id": "",
"Scope": "profile openid email",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserAPIEndpoint": "",
"DiscoveryEndpoint": "",
"ButtonText": "",
"ButtonColor": "#145DBF"
},
"LdapSettings": {
"Enable": true,
"EnableSync": false,
@ -162,6 +289,8 @@
"PictureAttribute": "",
"SyncIntervalMinutes": 10000,
"SkipCertificateVerification": true,
"PublicCertificateFile": "",
"PrivateKeyFile": "",
"QueryTimeout": 60,
"MaxPageSize": 500,
"LoginFieldName": "",
@ -172,7 +301,9 @@
},
"ComplianceSettings": {
"Enable": false,
"EnableDaily": false
"Directory": "./data/",
"EnableDaily": false,
"BatchSize": 30000
},
"LocalizationSettings": {
"DefaultServerLocale": "en",
@ -183,12 +314,14 @@
"Enable": false,
"EnableSyncWithLdap": false,
"EnableSyncWithLdapIncludeAuth": false,
"IgnoreGuestsLdapSync": false,
"Verify": true,
"Encrypt": true,
"SignRequest": false,
"IdpURL": "",
"IdpDescriptorURL": "",
"IdpMetadataURL": "",
"ServiceProviderIdentifier": "",
"AssertionConsumerServiceURL": "",
"SignatureAlgorithm": "RSAwithSHA1",
"CanonicalAlgorithm": "Canonical1.0",
@ -213,18 +346,32 @@
"LoginButtonBorderColor": "#2389D7",
"LoginButtonTextColor": "#ffffff"
},
"NativeAppSettings": {
"AppCustomURLSchemes": ["mmauth://", "mmauthbeta://"],
"AppDownloadLink": "https://mattermost.com/pl/download-apps",
"AndroidAppDownloadLink": "https://mattermost.com/pl/android-app/",
"IosAppDownloadLink": "https://mattermost.com/pl/ios-app/"
},
"ClusterSettings": {
"Enable": false
},
"ExperimentalSettings": {
"RestrictSystemAdmin": true
},
"AnalyticsSettings": {
"MaxUsersForStatistics": 2500
},
"DataRetentionSettings": {
"EnableMessageDeletion": false,
"EnableFileDeletion": false,
"EnableBoardsDeletion": false,
"MessageRetentionDays": 365,
"FileRetentionDays": 365,
"DeletionJobStartTime": "02:00"
"BoardsRetentionDays": 365,
"DeletionJobStartTime": "02:00",
"BatchSize": 3000,
"TimeBetweenBatchesMilliseconds": 100,
"RetentionIdsBatchSize": 100
},
"MessageExportSettings": {
"EnableExport": false,
@ -232,37 +379,51 @@
"DailyRunTime": "01:00",
"ExportFromTimestamp": 0,
"BatchSize": 10000,
"DownloadExportResults": false,
"GlobalRelaySettings": {
"CustomerType": "A9",
"SMTPUsername": "",
"SMTPPassword": "",
"EmailAddress": ""
"EmailAddress": "",
"SMTPServerTimeout": 1800
}
},
"ProductSettings": {},
"PluginSettings": {
"Enable": true,
"EnableUploads": true,
"AllowInsecureDownloadURL": false,
"EnableHealthCheck": true,
"Directory": "./plugins",
"ClientDirectory": "./client/plugins",
"Plugins": {},
"PluginStates": {
"com.mattermost.calls": {
"Enable": false
},
"com.mattermost.nps": {
"Enable": false
},
"com.mattermost.plugin-incident-response": {
"Enable": false
},
"com.mattermost.plugin-incident-management": {
"Enable": false
},
"focalboard": {
"Enable": false
"playbooks": {
"Enable": true
}
}
},
"EnableMarketplace": true,
"EnableRemoteMarketplace": true,
"AutomaticPrepackagedPlugins": true,
"RequirePluginSignature": false,
"MarketplaceURL": "https://api.integrations.mattermost.com",
"SignaturePublicKeyFiles": [],
"ChimeraOAuthProxyURL": ""
},
"DisplaySettings": {
"CustomURLSchemes": [],
"MaxMarkdownNodes": 0,
"ExperimentalTimezone": false
},
"GuestAccountsSettings": {
"Enable": true,
"HideTags": false,
"AllowEmailAccounts": true,
"EnforceMultifactorAuthentication": false,
"RestrictCreationToDomains": ""
@ -272,5 +433,13 @@
"ImageProxyType": "local",
"RemoteImageProxyURL": "",
"RemoteImageProxyOptions": ""
},
"ImportSettings": {
"Directory": "./import",
"RetentionDays": 30
},
"ExportSettings": {
"Directory": "./export",
"RetentionDays": 30
}
}

View File

@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *****************************************************************************
// Groups
// https://api.mattermost.com/#tag/groups
// *****************************************************************************
import {ChainableT} from '../../types';
function apiCreateCustomUserGroup(displayName: string, name: string, userIds: string[]): ChainableT<Cypress.Response<any>> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/groups',
method: 'POST',
body: {
display_name: displayName,
name,
source: 'custom',
allow_reference: true,
user_ids: userIds,
},
}).then((response) => {
expect(response.status).to.equal(201);
return cy.wrap(response);
});
}
Cypress.Commands.add('apiCreateCustomUserGroup', apiCreateCustomUserGroup);
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Create custom user group
* @param {string} displayName - the display name of the group
* @param {string} name - the @ mentionable name of the group
* @param {string[]} userIds - users to add to the group
*/
apiCreateCustomUserGroup: typeof apiCreateCustomUserGroup;
}
}
}

View File

@ -8,6 +8,7 @@ import './cloud';
import './cluster';
import './common';
import './data_retention';
import './group';
import './keycloak';
import './ldap';
import './playbooks';

View File

@ -12,22 +12,28 @@
"TLSStrictTransportMaxAge": 63072000,
"TLSOverwriteCiphers": [],
"UseLetsEncrypt": false,
"LetsEncryptCertificateCacheFile": "./config/letsencrypt.cache",
"Forward80To443": false,
"TrustedProxyIPHeader": [],
"ReadTimeout": 300,
"WriteTimeout": 300,
"IdleTimeout": 300,
"MaximumLoginAttempts": 10,
"GoroutineHealthThreshold": -1,
"GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": true,
"EnableOutgoingWebhooks": true,
"EnableCommands": true,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
"GoogleDeveloperKey": "",
"EnableLinkPreviews": false,
"EnablePermalinkPreviews": true,
"RestrictLinkPreviews": "",
"EnableTesting": false,
"EnableDeveloper": false,
"DeveloperFlags": "",
"EnableClientPerformanceDebugging": false,
"EnableOpenTracing": false,
"EnableSecurityFixAlert": true,
"EnableInsecureOutgoingConnections": false,
@ -41,22 +47,25 @@
"CorsDebug": false,
"AllowCookiesForSubdomains": false,
"ExtendSessionLengthWithActivity": true,
"SessionLengthWebInDays": 30,
"SessionLengthWebInHours": 720,
"SessionLengthMobileInDays": 30,
"SessionLengthMobileInHours": 720,
"SessionLengthSSOInDays": 30,
"SessionLengthSSOInHours": 720,
"SessionCacheInMinutes": 10,
"SessionIdleTimeoutInMinutes": 43200,
"WebsocketSecurePort": 443,
"WebsocketPort": 80,
"WebserverMode": "gzip",
"EnableGifPicker": false,
"GiphySdkKey": "s0glxvzVg9azvPipKxcPLpXV0q1x1fVP",
"EnableCustomEmoji": false,
"EnableEmojiPicker": true,
"EnableGifPicker": false,
"GfycatAPIKey": "2_KtH_W5",
"GfycatAPISecret": "3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof",
"PostEditTimeLimit": -1,
"TimeBetweenUserTypingUpdatesMilliseconds": 5000,
"EnablePostSearch": true,
"EnableFileSearch": true,
"MinimumHashtagLength": 3,
"EnableUserTypingMessages": true,
"EnableChannelViewedMessages": true,
@ -69,6 +78,8 @@
"ExperimentalEnableDefaultChannelLeaveJoinMessages": true,
"ExperimentalGroupUnreadChannels": "disabled",
"EnableAPITeamDeletion": true,
"EnableAPITriggerAdminNotifications": false,
"EnableAPIUserDeletion": false,
"ExperimentalEnableHardenedMode": false,
"ExperimentalStrictCSRFEnforcement": false,
"EnableEmailInvitations": true,
@ -76,13 +87,31 @@
"EnableBotAccountCreation": true,
"EnableSVGs": true,
"EnableLatex": false,
"EnableLegacySidebar": false,
"EnableInlineLatex": true,
"PostPriority": true,
"AllowPersistentNotifications": true,
"AllowPersistentNotificationsForGuests": false,
"PersistentNotificationIntervalMinutes": 5,
"PersistentNotificationMaxCount": 6,
"PersistentNotificationMaxRecipients": 5,
"EnableAPIChannelDeletion": false,
"EnableLocalMode": false,
"LocalModeSocketLocation": "/var/tmp/mattermost_local.socket",
"EnableAWSMetering": false,
"SplitKey": "",
"FeatureFlagSyncIntervalSeconds": 30,
"DebugSplit": false,
"ThreadAutoFollow": true,
"CollapsedThreads": "disabled"
"CollapsedThreads": "disabled",
"ManagedResourcePaths": "",
"EnableCustomGroups": true,
"SelfHostedPurchase": true,
"AllowSyncedDrafts": true
},
"TeamSettings": {
"SiteName": "Mattermost",
"MaxUsersPerTeam": 2000,
"EnableJoinLeaveMessageByDefault": true,
"EnableUserCreation": true,
"EnableOpenServer": true,
"EnableUserDeactivation": false,
@ -92,6 +121,7 @@
"CustomBrandText": "",
"CustomDescriptionText": "",
"RestrictDirectMessage": "any",
"EnableLastActiveTime": true,
"UserStatusAwayTimeout": 300,
"MaxChannelsPerTeam": 2000,
"MaxNotificationsPerChannel": 1000,
@ -110,26 +140,37 @@
"IosMinVersion": ""
},
"SqlSettings": {
"DriverName": "postgres",
"DataSource": "postgres://mmuser:mostest@localhost/mattermost_test?sslmode=disable\u0026connect_timeout=10\u0026binary_parameters=yes",
"DataSourceReplicas": [],
"DataSourceSearchReplicas": [],
"MaxIdleConns": 20,
"ConnMaxLifetimeMilliseconds": 3600000,
"ConnMaxIdleTimeMilliseconds": 300000,
"MaxOpenConns": 300,
"Trace": false,
"AtRestEncryptKey": "",
"QueryTimeout": 30
"QueryTimeout": 30,
"DisableDatabaseSearch": false,
"MigrationsStatementTimeoutSeconds": 100000,
"ReplicaLagSettings": [],
"ReplicaMonitorIntervalSeconds": 5
},
"LogSettings": {
"EnableConsole": true,
"ConsoleLevel": "DEBUG",
"ConsoleJson": true,
"EnableColor": false,
"EnableFile": true,
"FileLevel": "INFO",
"FileJson": true,
"FileLocation": "",
"EnableWebhookDebugging": true,
"EnableDiagnostics": true,
"EnableSentry": false
"VerboseDiagnostics": false,
"EnableSentry": false,
"AdvancedLoggingJSON": {},
"AdvancedLoggingConfig": ""
},
"ExperimentalAuditSettings": {
"FileEnabled": false,
@ -138,16 +179,21 @@
"FileMaxAgeDays": 0,
"FileMaxBackups": 0,
"FileCompress": false,
"FileMaxQueueSize": 1000
"FileMaxQueueSize": 1000,
"AdvancedLoggingJSON": {},
"AdvancedLoggingConfig": ""
},
"NotificationLogSettings": {
"EnableConsole": true,
"ConsoleLevel": "DEBUG",
"ConsoleJson": true,
"EnableColor": false,
"EnableFile": true,
"FileLevel": "INFO",
"FileJson": true,
"FileLocation": ""
"FileLocation": "",
"AdvancedLoggingJSON": {},
"AdvancedLoggingConfig": ""
},
"PasswordSettings": {
"MinimumLength": 5,
@ -155,27 +201,48 @@
"Number": false,
"Uppercase": false,
"Symbol": false,
"Enable": false
"EnableForgotLink": true
},
"FileSettings": {
"EnableFileAttachments": true,
"EnableMobileUpload": true,
"EnableMobileDownload": true,
"MaxFileSize": 104857600,
"MaxImageResolution": 33177600,
"MaxImageDecoderConcurrency": -1,
"DriverName": "local",
"Directory": "./data/",
"EnablePublicLink": false,
"ExtractContent": true,
"ArchiveRecursion": false,
"PublicLinkSalt": "",
"InitialFont": "nunito-bold.ttf",
"AmazonS3AccessKeyId": "",
"AmazonS3SecretAccessKey": "",
"AmazonS3Bucket": "",
"AmazonS3PathPrefix": "",
"AmazonS3Region": "",
"AmazonS3Endpoint": "s3.amazonaws.com",
"AmazonS3SSL": true,
"AmazonS3SignV2": false,
"AmazonS3SSE": false,
"AmazonS3Trace": false
"AmazonS3Trace": false,
"AmazonS3RequestTimeoutMilliseconds": 30000,
"DedicatedExportStore": false,
"ExportDriverName": "local",
"ExportDirectory": "./data/",
"ExportAmazonS3AccessKeyId": "",
"ExportAmazonS3SecretAccessKey": "",
"ExportAmazonS3Bucket": "",
"ExportAmazonS3PathPrefix": "",
"ExportAmazonS3Region": "",
"ExportAmazonS3Endpoint": "s3.amazonaws.com",
"ExportAmazonS3SSL": true,
"ExportAmazonS3SignV2": false,
"ExportAmazonS3SSE": false,
"ExportAmazonS3Trace": false,
"ExportAmazonS3RequestTimeoutMilliseconds": 30000,
"ExportAmazonS3PresignExpiresSeconds": 21600
},
"EmailSettings": {
"EnableSignUpWithEmail": true,
@ -197,7 +264,10 @@
"ConnectionSecurity": "",
"SendPushNotifications": true,
"PushNotificationServer": "https://push-test.mattermost.com",
"PushNotificationServerType": "custom",
"PushNotificationServerLocation": "us",
"PushNotificationContents": "generic",
"PushNotificationBuffer": 1000,
"EnableEmailBatching": false,
"EmailBatchingBufferSize": 256,
"EmailBatchingInterval": 30,
@ -224,9 +294,10 @@
"SupportSettings": {
"TermsOfServiceLink": "https://mattermost.com/pl/terms-of-use/",
"PrivacyPolicyLink": "https://mattermost.com/pl/privacy-policy/",
"AboutLink": "https://docs.mattermost.com/pl/about-mattermost",
"AboutLink": "https://mattermost.com/pl/about-mattermost",
"HelpLink": "https://mattermost.com/pl/help/",
"ReportAProblemLink": "https://mattermost.com/pl/report-a-bug",
"ForgotPasswordLink": "",
"SupportEmail": "",
"CustomTermsOfServiceEnabled": false,
"CustomTermsOfServiceReAcceptancePeriod": 365,
@ -239,7 +310,10 @@
"BannerTextColor": "#333333",
"AllowBannerDismissal": true,
"AdminNoticesEnabled": false,
"UserNoticesEnabled": false
"UserNoticesEnabled": false,
"NoticesURL": "https://notices.mattermost.com/",
"NoticesFetchFrequency": 3600,
"NoticesSkipCache": false
},
"ThemeSettings": {
"EnableThemeSelection": true,
@ -254,7 +328,10 @@
"Scope": "",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserAPIEndpoint": ""
"UserAPIEndpoint": "",
"DiscoveryEndpoint": "",
"ButtonText": "",
"ButtonColor": ""
},
"GoogleSettings": {
"Enable": false,
@ -263,7 +340,10 @@
"Scope": "profile email",
"AuthEndpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"TokenEndpoint": "https://www.googleapis.com/oauth2/v4/token",
"UserAPIEndpoint": "https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses,nicknames,metadata"
"UserAPIEndpoint": "https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses,nicknames,metadata",
"DiscoveryEndpoint": "",
"ButtonText": "",
"ButtonColor": ""
},
"Office365Settings": {
"Enable": false,
@ -273,8 +353,21 @@
"AuthEndpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
"TokenEndpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
"UserAPIEndpoint": "https://graph.microsoft.com/v1.0/me",
"DiscoveryEndpoint": "",
"DirectoryId": ""
},
"OpenIdSettings": {
"Enable": false,
"Secret": "",
"Id": "",
"Scope": "profile openid email",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserAPIEndpoint": "",
"DiscoveryEndpoint": "",
"ButtonText": "",
"ButtonColor": "#145DBF"
},
"LdapSettings": {
"Enable": true,
"EnableSync": false,
@ -302,6 +395,8 @@
"PictureAttribute": "",
"SyncIntervalMinutes": 10000,
"SkipCertificateVerification": true,
"PublicCertificateFile": "",
"PrivateKeyFile": "",
"QueryTimeout": 60,
"MaxPageSize": 500,
"LoginFieldName": "",
@ -313,7 +408,8 @@
"ComplianceSettings": {
"Enable": false,
"Directory": "./data/",
"EnableDaily": false
"EnableDaily": false,
"BatchSize": 30000
},
"LocalizationSettings": {
"DefaultServerLocale": "en",
@ -324,12 +420,14 @@
"Enable": false,
"EnableSyncWithLdap": false,
"EnableSyncWithLdapIncludeAuth": false,
"IgnoreGuestsLdapSync": false,
"Verify": true,
"Encrypt": true,
"SignRequest": false,
"IdpURL": "",
"IdpDescriptorURL": "",
"IdpMetadataURL": "",
"ServiceProviderIdentifier": "",
"AssertionConsumerServiceURL": "",
"SignatureAlgorithm": "RSAwithSHA1",
"CanonicalAlgorithm": "Canonical1.0",
@ -355,10 +453,28 @@
"LoginButtonTextColor": "#ffffff"
},
"NativeAppSettings": {
"AppCustomURLSchemes": ["mmauth://", "mmauthbeta://"],
"AppDownloadLink": "https://mattermost.com/pl/download-apps",
"AndroidAppDownloadLink": "https://mattermost.com/pl/android-app/",
"IosAppDownloadLink": "https://mattermost.com/pl/ios-app/"
},
"ClusterSettings": {
"Enable": false,
"ClusterName": "",
"OverrideHostname": "",
"NetworkInterface": "",
"BindAddress": "",
"AdvertiseAddress": "",
"UseIPAddress": true,
"EnableGossipCompression": true,
"EnableExperimentalGossipEncryption": false,
"ReadOnlyConfig": true,
"GossipPort": 8074,
"StreamingPort": 8075,
"MaxIdleConns": 100,
"MaxIdleConnsPerHost": 128,
"IdleConnTimeoutMilliseconds": 90000
},
"MetricsSettings": {
"Enable": false,
"BlockProfileRate": 0,
@ -370,7 +486,11 @@
"LinkMetadataTimeoutMilliseconds": 5000,
"RestrictSystemAdmin": false,
"UseNewSAMLLibrary": false,
"DisableAppBar": false
"EnableSharedChannels": false,
"EnableRemoteClusterService": false,
"DisableAppBar": false,
"DisableRefetchingOnBrowserFocus": false,
"DelayChannelAutocomplete": false
},
"AnalyticsSettings": {
"MaxUsersForStatistics": 2500
@ -393,17 +513,33 @@
"PostsAggregatorJobStartTime": "03:00",
"IndexPrefix": "",
"LiveIndexingBatchSize": 1,
"BulkIndexingTimeWindowSeconds": 3600,
"BatchSize": 10000,
"RequestTimeoutSeconds": 30,
"SkipTLSVerification": false,
"Trace": ""
"CA": "",
"ClientCert": "",
"ClientKey": "",
"Trace": "",
"IgnoredPurgeIndexes": ""
},
"BleveSettings": {
"IndexDir": "",
"EnableIndexing": false,
"EnableSearching": false,
"EnableAutocomplete": false,
"BatchSize": 10000
},
"DataRetentionSettings": {
"EnableMessageDeletion": false,
"EnableFileDeletion": false,
"EnableBoardsDeletion": false,
"MessageRetentionDays": 365,
"FileRetentionDays": 365,
"DeletionJobStartTime": "02:00"
"BoardsRetentionDays": 365,
"DeletionJobStartTime": "02:00",
"BatchSize": 3000,
"TimeBetweenBatchesMilliseconds": 100,
"RetentionIdsBatchSize": 100
},
"MessageExportSettings": {
"EnableExport": false,
@ -411,17 +547,22 @@
"DailyRunTime": "01:00",
"ExportFromTimestamp": 0,
"BatchSize": 10000,
"DownloadExportResults": false,
"GlobalRelaySettings": {
"CustomerType": "A9",
"SMTPUsername": "",
"SMTPPassword": "",
"EmailAddress": ""
"EmailAddress": "",
"SMTPServerTimeout": 1800
}
},
"JobSettings": {
"RunJobs": true,
"RunScheduler": true
"RunScheduler": true,
"CleanupJobsThresholdDays": -1,
"CleanupConfigThresholdDays": -1
},
"ProductSettings": {},
"PluginSettings": {
"Enable": true,
"EnableUploads": true,
@ -431,17 +572,14 @@
"ClientDirectory": "./client/plugins",
"Plugins": {},
"PluginStates": {
"com.mattermost.calls": {
"Enable": false
},
"com.mattermost.nps": {
"Enable": false
},
"com.mattermost.plugin-incident-response": {
"Enable": false
},
"com.mattermost.plugin-incident-management": {
"Enable": false
},
"focalboard": {
"Enable": false
"playbooks": {
"Enable": true
}
},
"EnableMarketplace": true,
@ -449,14 +587,17 @@
"AutomaticPrepackagedPlugins": true,
"RequirePluginSignature": false,
"MarketplaceURL": "https://api.integrations.mattermost.com",
"SignaturePublicKeyFiles": []
"SignaturePublicKeyFiles": [],
"ChimeraOAuthProxyURL": ""
},
"DisplaySettings": {
"CustomURLSchemes": [],
"MaxMarkdownNodes": 0,
"ExperimentalTimezone": false
},
"GuestAccountsSettings": {
"Enable": true,
"HideTags": false,
"AllowEmailAccounts": true,
"EnforceMultifactorAuthentication": false,
"RestrictCreationToDomains": ""
@ -466,5 +607,13 @@
"ImageProxyType": "local",
"RemoteImageProxyURL": "",
"RemoteImageProxyOptions": ""
},
"ImportSettings": {
"Directory": "./import",
"RetentionDays": 30
},
"ExportSettings": {
"Directory": "./export",
"RetentionDays": 30
}
}

View File

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Client4 from 'mattermost-redux/client/client4';
import {Client4} from '@mattermost/client';
import clientRequest from '../plugins/client_request';

View File

@ -33,6 +33,7 @@ declare namespace Cypress {
type UserCustomStatus = import('@mattermost/types/users').UserCustomStatus;
type UserAccessToken = import('@mattermost/types/users').UserAccessToken;
type DeepPartial = import('@mattermost/types/utilities').DeepPartial;
type Group = import('@mattermost/types/groups').Group;
interface Chainable {
tab: (options?: {shift?: boolean}) => Chainable<JQuery>;
}

View File

@ -99,7 +99,7 @@ Cypress.Commands.add('createPlaybook', (teamName, playbookName) => {
// Select the playbook from the dropdown menu
Cypress.Commands.add('selectPlaybookFromDropdown', (playbookName) => {
cy.findByTestId('autoCompleteSelector').should('exist').within(() => {
cy.get('input').click().type(playbookName.toLowerCase());
cy.get('input').click().type(playbookName.toLowerCase(), {force: true});
cy.get('#suggestionList').contains(playbookName).click({force: true});
});
});

View File

@ -6,7 +6,7 @@
# Typically run the local server with:
cd server && make run
# Or build and distribute webapp including channels, boards and playbooks
# Or build and distribute webapp including channels and playbooks
# so that their product URLs do not rely on Webpack dev server.
# Especially important when running test inside the Playwright's docker container.
cd webapp && make dist
@ -45,7 +45,7 @@ npm run test
Change to root directory, run docker container
```
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.36.0-focal /bin/bash
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.38.0-jammy /bin/bash
```
#### 2. Inside the docker container

View File

@ -48,7 +48,7 @@ async function sysadminSetup(client: Client, user: UserProfile | null) {
await client.createTeam(createRandomTeam(defaultTeam.name, defaultTeam.displayName, 'O', false));
} else if (myDefaultTeam && testConfig.resetBeforeTest) {
await Promise.all(
myTeams.filter((team) => team.name !== defaultTeam.name).map((team) => client.deleteTeam(team.id))
myTeams.filter((team) => team.name !== defaultTeam.name).map((team) => client.deleteTeam(team.id)),
);
const myChannels = await client.getMyChannels(myDefaultTeam.id);
@ -61,7 +61,7 @@ async function sysadminSetup(client: Client, user: UserProfile | null) {
channel.name !== 'off-topic'
);
})
.map((channel) => client.deleteChannel(channel.id))
.map((channel) => client.deleteChannel(channel.id)),
);
}
@ -170,7 +170,7 @@ async function ensureServerDeployment(client: Client) {
sameClusterName,
sameClusterName
? ''
: `Should have cluster name set and as expected. Got "${ClusterName}" but expected "${haClusterName}"`
: `Should have cluster name set and as expected. Got "${ClusterName}" but expected "${haClusterName}"`,
).toBe(true);
const clusterInfo = await client.getClusterStatus();
@ -179,12 +179,12 @@ async function ensureServerDeployment(client: Client) {
sameCount,
sameCount
? ''
: `Should match number of nodes in a cluster as expected. Got "${clusterInfo?.length}" but expected "${haClusterNodeCount}"`
: `Should match number of nodes in a cluster as expected. Got "${clusterInfo?.length}" but expected "${haClusterNodeCount}"`,
).toBe(true);
clusterInfo.forEach((info) =>
// eslint-disable-next-line no-console
console.log(`hostname: ${info.hostname}, version: ${info.version}, config_hash: ${info.config_hash}`)
console.log(`hostname: ${info.hostname}, version: ${info.version}, config_hash: ${info.config_hash}`),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -4,34 +4,36 @@
"percy": "cross-env PERCY_TOKEN=$PERCY_TOKEN PW_PERCY_ENABLE=true percy exec -- playwright test --project=chrome --project=iphone --project=ipad",
"tsc": "tsc -b",
"lint": "eslint . --ext .js,.ts",
"prettier": "prettier --write .",
"prettier": "prettier . --check",
"prettier:fix": "prettier --write .",
"check": "npm run tsc && npm run lint && npm run prettier",
"codegen": "cross-env playwright codegen $PW_BASE_URL",
"playwright-ui": "playwright test --ui",
"test-slomo": "cross-env PW_SNAPSHOT_ENABLE=true PW_SLOWMO=1000 playwright test",
"show-report": "npx playwright show-report"
"show-report": "npx playwright show-report",
"postinstall": "npx playwright install"
},
"dependencies": {
"@axe-core/playwright": "4.7.3",
"@percy/cli": "1.26.2",
"@percy/cli": "1.27.1",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.36.1",
"@playwright/test": "1.38.0",
"async-wait-until": "2.0.12",
"axe-core": "4.7.2",
"axe-core": "4.8.1",
"chalk": "4.1.2",
"deepmerge": "4.3.1",
"dotenv": "16.3.1",
"form-data": "4.0.0",
"isomorphic-unfetch": "4.0.2",
"uuid": "9.0.0"
"uuid": "9.0.1"
},
"devDependencies": {
"@types/uuid": "9.0.2",
"@typescript-eslint/eslint-plugin": "6.1.0",
"@typescript-eslint/parser": "6.1.0",
"@types/uuid": "9.0.4",
"@typescript-eslint/eslint-plugin": "6.7.0",
"@typescript-eslint/parser": "6.7.0",
"cross-env": "7.0.3",
"eslint": "8.45.0",
"prettier": "2.8.7",
"typescript": "5.0.4"
"eslint": "8.49.0",
"prettier": "3.0.3",
"typescript": "5.2.2"
}
}

View File

@ -52,13 +52,6 @@ export default defineConfig({
},
},
projects: [
{
name: 'iphone',
use: {
browserName: 'chromium',
...devices['iPhone 13 Pro'],
},
},
{
name: 'ipad',
use: {

View File

@ -16,41 +16,38 @@
# 5. PW_ADMIN_EMAIL
# - Default to "sysadmin@sample.mattermost.com" if not set.
# 6. PW_BOARDS_PRODUCT_ENABLED
# - Default to "true" if not set. Used to correctly set server config.
# 7. PW_HA_CLUSTER_ENABLED
# 6. PW_HA_CLUSTER_ENABLED
# - Default to "false" if not set. Set to true if the test server is with HA enabled.
# 8. PW_HA_CLUSTER_NODE_COUNT
# 7. PW_HA_CLUSTER_NODE_COUNT
# - Default to "2" if not set.
# 9. PW_HA_CLUSTER_NAME
# 8. PW_HA_CLUSTER_NAME
# - Default to "mm_dev_cluster" if not set.
# 10. PW_RESET_BEFORE_TEST
# 9. PW_RESET_BEFORE_TEST
# - Default to "false" if not set. If true, the setup deletes all teams and channels other than the default team which is "ad-1".
# 11. CI
# 10. CI
# - Default to "false" if not set.
# 12. PW_HEADLESS
# 11. PW_HEADLESS
# - Default to "false" or headless mode if not set. Set to true to run test in headed mode.
# 13. PW_SLOWMO
# 12. PW_SLOWMO
# - Default to "0" if not set which means normal test speed run. Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
# 14. PW_WORKERS
# 13. PW_WORKERS
# - Default to "1" if not set. The maximum number of concurrent worker processes to use for parallelizing tests.
# 15. PW_SNAPSHOT_ENABLE
# 14. PW_SNAPSHOT_ENABLE
# - Default to "false" if not set. Set to true to enable snapshot testing.
# Note that, snapshot testing should be done in Playwright docker image only.
# This is to ensure that, there's a common base platform for all contributors
# regardless of each local development platform.
# 16. PW_PERCY_ENABLE
# 15. PW_PERCY_ENABLE
# - Default to "false" if not set. Use to save and compare results via https://percy.io/.
# 17. PERCY_TOKEN
# 16. PERCY_TOKEN
# - A token required by https://percy.io/.

View File

@ -2,6 +2,5 @@
// See LICENSE.txt for license information.
export const appsPluginId = 'com.mattermost.apps';
export const boardsPluginId = 'focalboard';
export const callsPluginId = 'com.mattermost.calls';
export const playbooksPluginId = 'playbooks';

View File

@ -3,11 +3,10 @@
import os from 'node:os';
import {expect, test} from '@playwright/test';
import {expect} from '@playwright/test';
import {callsPluginId} from './constant';
import {getAdminClient} from './server/init';
import {isSmallScreen} from './util';
export async function shouldHaveCallsEnabled(enabled = true) {
const {adminClient} = await getAdminClient();
@ -26,14 +25,10 @@ export async function shouldHaveFeatureFlag(name: string, value: string | boolea
const matched = config.FeatureFlags[name] === value;
expect(
matched,
matched ? '' : `FeatureFlags["${name}'] expect "${value}" but actual "${config.FeatureFlags[name]}"`
matched ? '' : `FeatureFlags["${name}'] expect "${value}" but actual "${config.FeatureFlags[name]}"`,
).toBeTruthy();
}
export function shouldSkipInSmallScreen() {
test.skip(({viewport}) => isSmallScreen(viewport), 'Not applicable to mobile device');
}
export async function shouldRunInLinux() {
const platform = os.platform();
await expect(platform, 'Run in Linux or Playwright docker image only').toBe('linux');

View File

@ -35,9 +35,6 @@ const onPremServerConfig = (): Partial<TestAdminConfig> => {
Enable: testConfig.haClusterEnabled,
ClusterName: testConfig.haClusterName,
},
ExperimentalSettings: {
DisableAppBar: false,
},
PasswordSettings: {
MinimumLength: 5,
Lowercase: false,
@ -48,9 +45,15 @@ const onPremServerConfig = (): Partial<TestAdminConfig> => {
},
PluginSettings: {
EnableUploads: true,
Plugins: {
PluginStates: {
'com.mattermost.calls': {
defaultenabled: true,
Enable: false,
},
'com.mattermost.nps': {
Enable: false,
},
playbooks: {
Enable: true,
},
},
},
@ -65,7 +68,7 @@ const onPremServerConfig = (): Partial<TestAdminConfig> => {
};
// Should be based only from the generated default config from ./server via "make config-reset"
// Based on v7.10 server
// Based on v9.1 server
const defaultServerConfig: AdminConfig = {
ServiceSettings: {
SiteURL: '',
@ -127,8 +130,7 @@ const defaultServerConfig: AdminConfig = {
WebsocketPort: 80,
WebserverMode: 'gzip',
EnableGifPicker: true,
GfycatAPIKey: '2_KtH_W5',
GfycatAPISecret: '3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof',
GiphySdkKey: 's0glxvzVg9azvPipKxcPLpXV0q1x1fVP',
EnableCustomEmoji: true,
EnableEmojiPicker: true,
PostEditTimeLimit: -1,
@ -158,6 +160,11 @@ const defaultServerConfig: AdminConfig = {
EnableLatex: false,
EnableInlineLatex: true,
PostPriority: true,
AllowPersistentNotifications: true,
AllowPersistentNotificationsForGuests: false,
PersistentNotificationIntervalMinutes: 5,
PersistentNotificationMaxCount: 6,
PersistentNotificationMaxRecipients: 5,
EnableAPIChannelDeletion: false,
EnableLocalMode: false,
LocalModeSocketLocation: '/var/tmp/mattermost_local.socket',
@ -171,15 +178,11 @@ const defaultServerConfig: AdminConfig = {
EnableCustomGroups: true,
SelfHostedPurchase: true,
AllowSyncedDrafts: true,
AllowPersistentNotifications: true,
PersistentNotificationMaxCount: 6,
PersistentNotificationMaxRecipients: 5,
PersistentNotificationIntervalMinutes: 5,
AllowPersistentNotificationsForGuests: false,
},
TeamSettings: {
SiteName: 'Mattermost',
MaxUsersPerTeam: 50,
EnableJoinLeaveMessageByDefault: true,
EnableUserCreation: true,
EnableOpenServer: false,
EnableUserDeactivation: false,
@ -223,6 +226,7 @@ const defaultServerConfig: AdminConfig = {
DisableDatabaseSearch: false,
MigrationsStatementTimeoutSeconds: 100000,
ReplicaLagSettings: [],
ReplicaMonitorIntervalSeconds: 5,
},
LogSettings: {
EnableConsole: true,
@ -237,6 +241,7 @@ const defaultServerConfig: AdminConfig = {
EnableDiagnostics: true,
VerboseDiagnostics: false,
EnableSentry: true,
AdvancedLoggingJSON: {},
AdvancedLoggingConfig: '',
},
ExperimentalAuditSettings: {
@ -247,6 +252,7 @@ const defaultServerConfig: AdminConfig = {
FileMaxBackups: 0,
FileCompress: false,
FileMaxQueueSize: 1000,
AdvancedLoggingJSON: {},
AdvancedLoggingConfig: '',
},
NotificationLogSettings: {
@ -258,6 +264,7 @@ const defaultServerConfig: AdminConfig = {
FileLevel: 'INFO',
FileJson: true,
FileLocation: '',
AdvancedLoggingJSON: {},
AdvancedLoggingConfig: '',
},
PasswordSettings: {
@ -293,6 +300,21 @@ const defaultServerConfig: AdminConfig = {
AmazonS3SSE: false,
AmazonS3Trace: false,
AmazonS3RequestTimeoutMilliseconds: 30000,
DedicatedExportStore: false,
ExportDriverName: 'local',
ExportDirectory: './data/',
ExportAmazonS3AccessKeyId: '',
ExportAmazonS3SecretAccessKey: '',
ExportAmazonS3Bucket: '',
ExportAmazonS3PathPrefix: '',
ExportAmazonS3Region: '',
ExportAmazonS3Endpoint: 's3.amazonaws.com',
ExportAmazonS3SSL: true,
ExportAmazonS3SignV2: false,
ExportAmazonS3SSE: false,
ExportAmazonS3Trace: false,
ExportAmazonS3RequestTimeoutMilliseconds: 30000,
ExportAmazonS3PresignExpiresSeconds: 21600,
},
EmailSettings: {
EnableSignUpWithEmail: true,
@ -344,7 +366,7 @@ const defaultServerConfig: AdminConfig = {
SupportSettings: {
TermsOfServiceLink: 'https://mattermost.com/pl/terms-of-use/',
PrivacyPolicyLink: 'https://mattermost.com/pl/privacy-policy/',
AboutLink: 'https://docs.mattermost.com/pl/about-mattermost',
AboutLink: 'https://mattermost.com/pl/about-mattermost',
HelpLink: 'https://mattermost.com/pl/help/',
ReportAProblemLink: 'https://mattermost.com/pl/report-a-bug',
ForgotPasswordLink: '',
@ -539,7 +561,7 @@ const defaultServerConfig: AdminConfig = {
UseNewSAMLLibrary: false,
EnableSharedChannels: false,
EnableRemoteClusterService: false,
DisableAppBar: true,
DisableAppBar: false,
DisableRefetchingOnBrowserFocus: false,
DelayChannelAutocomplete: false,
},
@ -589,6 +611,8 @@ const defaultServerConfig: AdminConfig = {
BoardsRetentionDays: 365,
DeletionJobStartTime: '02:00',
BatchSize: 3000,
TimeBetweenBatchesMilliseconds: 100,
RetentionIdsBatchSize: 100,
},
MessageExportSettings: {
EnableExport: false,
@ -627,6 +651,9 @@ const defaultServerConfig: AdminConfig = {
'com.mattermost.nps': {
Enable: true,
},
playbooks: {
Enable: true,
},
},
EnableMarketplace: true,
EnableRemoteMarketplace: true,
@ -638,6 +665,7 @@ const defaultServerConfig: AdminConfig = {
},
DisplaySettings: {
CustomURLSchemes: [],
MaxMarkdownNodes: 0,
ExperimentalTimezone: true,
},
GuestAccountsSettings: {
@ -656,32 +684,24 @@ const defaultServerConfig: AdminConfig = {
CloudSettings: {
CWSURL: 'https://customers.mattermost.com',
CWSAPIURL: 'https://portal.internal.prod.cloud.mattermost.com',
CWSMock: false,
},
FeatureFlags: {
TestFeature: 'off',
TestBoolFeature: false,
EnableRemoteClusterService: false,
AppsEnabled: true,
PluginPlaybooks: '',
PluginApps: '',
PluginFocalboard: '',
PluginCalls: '',
PermalinkPreviews: true,
PermalinkPreviews: false,
CallsEnabled: true,
BoardsFeatureFlags: '',
BoardsDataRetention: false,
NormalizeLdapDNs: false,
GraphQL: false,
CommandPalette: false,
SendWelcomePost: true,
PostPriority: true,
PostPriority: false,
WysiwygEditor: false,
PeopleProduct: false,
ReduceOnBoardingTaskList: false,
ThreadsEverywhere: false,
OnboardingTourTips: true,
DeprecateCloudFree: false,
CloudReverseTrial: false,
EnableExportDirectDownload: false,
StreamlinedMarketplace: true,
},
ImportSettings: {
Directory: './import',

View File

@ -24,7 +24,7 @@ export async function initSetup({
const {adminClient, adminUser} = await getAdminClient();
if (!adminClient) {
throw new Error(
"Failed to setup admin: Check that you're able to access the server using the same admin credential."
"Failed to setup admin: Check that you're able to access the server using the same admin credential.",
);
}
@ -83,8 +83,8 @@ export async function initSetup({
// eslint-disable-next-line no-console
console.log(
chalk.green(
`This failed due to the experimental fetch support in Node.js starting v18.0.0.\nYou may set environment variable: "export NODE_OPTIONS='--no-experimental-fetch'", then try again.'`
)
`This failed due to the experimental fetch support in Node.js starting v18.0.0.\nYou may set environment variable: "export NODE_OPTIONS='--no-experimental-fetch'", then try again.'`,
),
);
}
expect(err, 'Should not throw an error').toBeFalsy();

View File

@ -16,6 +16,6 @@ export async function hideDynamicChannelsContent(page: Page) {
export async function waitForAnimationEnd(locator: Locator) {
return locator.evaluate((element) =>
Promise.all(element.getAnimations({subtree: true}).map((animation) => animation.finished))
Promise.all(element.getAnimations({subtree: true}).map((animation) => animation.finished)),
);
}

View File

@ -1,11 +1,10 @@
import {test as base, Browser, Page, ViewportSize} from '@playwright/test';
import {test as base, Browser, Page} from '@playwright/test';
import {AxeResults} from 'axe-core';
import AxeBuilder from '@axe-core/playwright';
import {TestBrowser} from './browser_context';
import {shouldHaveCallsEnabled, shouldHaveFeatureFlag, shouldSkipInSmallScreen, shouldRunInLinux} from './flag';
import {shouldHaveCallsEnabled, shouldHaveFeatureFlag, shouldRunInLinux} from './flag';
import {initSetup, getAdminClient} from './server';
import {isSmallScreen} from './util';
import {hideDynamicChannelsContent, waitForAnimationEnd, waitUntil} from './test_action';
import {pages} from './ui/pages';
import {matchSnapshot} from './visual';
@ -29,8 +28,8 @@ export const test = base.extend<ExtendedFixtures>({
const ab = new AxeBuilderExtended();
await use(ab);
},
pw: async ({browser, viewport}, use) => {
const pw = new PlaywrightExtended(browser, viewport);
pw: async ({browser}, use) => {
const pw = new PlaywrightExtended(browser);
await use(pw);
await pw.testBrowser.close();
},
@ -47,7 +46,6 @@ class PlaywrightExtended {
// ./flag
readonly shouldHaveCallsEnabled;
readonly shouldHaveFeatureFlag;
readonly shouldSkipInSmallScreen;
readonly shouldRunInLinux;
// ./server
@ -62,20 +60,16 @@ class PlaywrightExtended {
// ./ui/pages
readonly pages;
// ./util
readonly isSmallScreen;
// ./visual
readonly matchSnapshot;
constructor(browser: Browser, viewport: ViewportSize | null) {
constructor(browser: Browser) {
// ./browser_context
this.testBrowser = new TestBrowser(browser);
// ./flag
this.shouldHaveCallsEnabled = shouldHaveCallsEnabled;
this.shouldHaveFeatureFlag = shouldHaveFeatureFlag;
this.shouldSkipInSmallScreen = shouldSkipInSmallScreen;
this.shouldRunInLinux = shouldRunInLinux;
// ./server
@ -90,9 +84,6 @@ class PlaywrightExtended {
// ./ui/pages
this.pages = pages;
// ./util
this.isSmallScreen = () => isSmallScreen(viewport);
// ./visual
this.matchSnapshot = matchSnapshot;
}

View File

@ -1,27 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test';
export default class BoardsCreateModal {
readonly container: Locator;
readonly productSwitchMenu;
constructor(container: Locator) {
this.container = container;
this.productSwitchMenu = container.getByRole('button', {name: 'Product switch menu'});
}
async switchProduct(name: string) {
await this.productSwitchMenu.click();
await this.container.getByRole('link', {name: `${name}`}).click();
}
async toBeVisible(name: string) {
await expect(this.container.getByRole('heading', {name})).toBeVisible();
}
}
export {BoardsCreateModal};

View File

@ -1,28 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Locator} from '@playwright/test';
export default class BoardsSidebar {
readonly container: Locator;
readonly plusButton;
readonly createNewBoardMenuItem;
readonly createNewCategoryMenuItem;
readonly titles;
constructor(container: Locator) {
this.container = container;
this.plusButton = container.locator('.add-board-icon');
this.createNewBoardMenuItem = container.getByRole('button', {name: 'Create new board'});
this.createNewCategoryMenuItem = container.getByRole('button', {name: 'Create New Category'});
this.titles = container.locator('.SidebarBoardItem > .octo-sidebar-title');
}
async waitForTitle(name: string) {
await this.container.getByRole('button', {name: `${name}`}).waitFor({state: 'visible'});
}
}
export {BoardsSidebar};

View File

@ -0,0 +1,93 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test';
import {components} from '@e2e-support/ui/components';
import {waitUntil} from '@e2e-support/test_action';
import {duration} from '@e2e-support/util';
export default class ChannelsCenterView {
readonly container: Locator;
readonly header;
readonly headerMobile;
readonly postCreate;
constructor(container: Locator) {
this.container = container;
this.header = new components.ChannelsHeader(this.container.locator('.channel-header'));
this.headerMobile = new components.ChannelsHeaderMobile(this.container.locator('.navbar'));
this.postCreate = new components.ChannelsPostCreate(container.getByTestId('post-create'));
}
async toBeVisible() {
await expect(this.container).toBeVisible();
await this.postCreate.toBeVisible();
}
/**
* Return the first post in the Center
*/
async getFirstPost() {
const firstPost = this.container.getByTestId('postView').first();
await firstPost.waitFor();
return new components.ChannelsPost(firstPost);
}
/**
* Return the last post in the Center
*/
async getLastPost() {
const lastPost = this.container.getByTestId('postView').last();
await lastPost.waitFor();
return new components.ChannelsPost(lastPost);
}
/**
* Return the Nth post in the Center from the top
* @param index
* @returns
*/
async getNthPost(index: number) {
const nthPost = this.container.getByTestId('postView').nth(index);
await nthPost.waitFor();
return new components.ChannelsPost(nthPost);
}
/**
* Returns the Center post by post's id
* @param postId Just the ID without the prefix
*/
async getPostById(id: string) {
const postById = this.container.locator(`[id="post_${id}"]`);
await postById.waitFor();
return new components.ChannelsPost(postById);
}
async waitUntilLastPostContains(text: string, timeout = duration.ten_sec) {
await waitUntil(
async () => {
const post = await this.getLastPost();
const content = await post.container.textContent();
return content?.includes(text);
},
{timeout},
);
}
async waitUntilPostWithIdContains(id: string, text: string, timeout = duration.ten_sec) {
await waitUntil(
async () => {
const post = await this.getPostById(id);
const content = await post.container.textContent();
return content?.includes(text);
},
{timeout},
);
}
}
export {ChannelsCenterView};

View File

@ -0,0 +1,61 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test';
export default class EmojiGifPicker {
readonly container: Locator;
readonly gifTab: Locator;
readonly gifSearchInput: Locator;
readonly gifPickerItems: Locator;
constructor(container: Locator) {
this.container = container;
this.gifTab = container.getByText('GIFs');
this.gifSearchInput = container.getByPlaceholder('Search GIPHY');
this.gifPickerItems = container.locator('.gif-picker__items');
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
async openGifTab() {
await expect(this.gifTab).toBeVisible();
await this.gifTab.click({force: true});
await expect(this.gifSearchInput).toBeVisible();
await expect(this.gifPickerItems).toBeVisible();
}
async searchGif(name: string) {
await this.gifSearchInput.fill(name);
await expect(this.gifSearchInput).toHaveValue(name);
}
async getNthGif(n: number) {
await expect(this.gifPickerItems).toBeVisible();
await this.gifPickerItems.locator('img').nth(n).waitFor();
const nthGif = this.gifPickerItems.locator('img').nth(n);
await expect(nthGif).toBeVisible();
const nthGifSrc = await nthGif.getAttribute('src');
const nthGifAlt = await nthGif.getAttribute('alt');
if (!nthGifSrc || !nthGifAlt) {
throw new Error('Gif src or alt is empty');
}
return {
src: nthGifSrc,
alt: nthGifAlt,
img: nthGif,
};
}
}
export {EmojiGifPicker};

View File

@ -10,13 +10,13 @@ export default class ChannelsHeaderMobile {
this.container = container;
}
async toggleSidebar() {
await this.container.getByRole('button', {name: 'Toggle sidebar Menu Icon'}).click();
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
async toggleSidebar() {
await this.container.getByRole('button', {name: 'Toggle sidebar Menu Icon'}).click();
}
}
export {ChannelsHeaderMobile};

View File

@ -7,30 +7,76 @@ export default class ChannelsPostCreate {
readonly container: Locator;
readonly input;
readonly attachmentButton;
readonly emojiButton;
readonly sendMessageButton;
constructor(container: Locator) {
constructor(container: Locator, isRHS = false) {
this.container = container;
this.input = container.getByTestId('post_textbox');
if (!isRHS) {
this.input = container.getByTestId('post_textbox');
} else {
this.input = container.getByTestId('reply_textbox');
}
this.attachmentButton = container.getByLabel('attachment');
this.emojiButton = container.getByLabel('select an emoji');
this.sendMessageButton = container.getByTestId('SendMessageButton');
}
async postMessage(message: string) {
async toBeVisible() {
await expect(this.container).toBeVisible();
await this.input.waitFor();
await expect(this.input).toBeVisible();
}
/**
* It just writes the message in the input and doesn't send it
* @param message : Message to be written in the input
*/
async writeMessage(message: string) {
await this.input.waitFor();
await expect(this.input).toBeVisible();
await this.input.fill(message);
}
/**
* Returns the value of the message input
*/
async getInputValue() {
await expect(this.input).toBeVisible();
return await this.input.inputValue();
}
/**
* Sends the message already written in the input
*/
async sendMessage() {
await expect(this.input).toBeVisible();
const messageInputValue = await this.getInputValue();
expect(messageInputValue).not.toBe('');
await expect(this.sendMessageButton).toBeVisible();
await expect(this.sendMessageButton).toBeEnabled();
await this.sendMessageButton.click();
}
async toBeVisible() {
await expect(this.container).toBeVisible();
await expect(this.input).toBeVisible();
/**
* Composes and sends a message
*/
async postMessage(message: string) {
await this.writeMessage(message);
await this.sendMessage();
}
async openEmojiPicker() {
await expect(this.emojiButton).toBeVisible();
await this.emojiButton.click();
}
}

View File

@ -6,22 +6,43 @@ import {expect, Locator} from '@playwright/test';
export default class PostDotMenu {
readonly container: Locator;
readonly replyMenuItem;
readonly forwardMenuItem;
readonly followMessageMenuItem;
readonly markAsUnreadMenuItem;
readonly remindMenuItem;
readonly saveMenuItem;
readonly removeFromSavedMenuItem;
readonly pinToChannelMenuItem;
readonly unpinFromChannelMenuItem;
readonly copyLinkMenuItem;
readonly editMenuItem;
readonly copyTextMenuItem;
readonly deleteMenuItem;
constructor(container: Locator) {
this.container = container;
this.deleteMenuItem = this.container.getByText('Delete', {exact: true});
const getMenuItem = (hasText: string) => container.getByRole('menuitem').filter({hasText});
this.replyMenuItem = getMenuItem('Reply');
this.forwardMenuItem = getMenuItem('Forward');
this.followMessageMenuItem = getMenuItem('Follow message');
this.markAsUnreadMenuItem = getMenuItem('Mark as Unread');
this.remindMenuItem = getMenuItem('Remind');
this.saveMenuItem = getMenuItem('Save');
this.removeFromSavedMenuItem = getMenuItem('Remove from Saved');
this.pinToChannelMenuItem = getMenuItem('Pin to Channel');
this.unpinFromChannelMenuItem = getMenuItem('Unpin from Channel');
this.copyLinkMenuItem = getMenuItem('Copy Link');
this.editMenuItem = getMenuItem('Edit');
this.copyTextMenuItem = getMenuItem('Copy Text');
this.deleteMenuItem = getMenuItem('Delete');
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
async delete() {
await this.deleteMenuItem.waitFor();
await this.deleteMenuItem.click();
}
}
export {PostDotMenu};

View File

@ -6,12 +6,24 @@ import {expect, Locator} from '@playwright/test';
export default class PostMenu {
readonly container: Locator;
readonly plusOneEmojiButton;
readonly grinningEmojiButton;
readonly whiteCheckMarkEmojiButton;
readonly addReactionButton;
readonly saveButton;
readonly replyButton;
readonly actionsButton;
readonly dotMenuButton;
constructor(container: Locator) {
this.container = container;
this.plusOneEmojiButton = container.getByRole('button', {name: '+1 emoji'});
this.grinningEmojiButton = container.getByRole('button', {name: 'grinning emoji'});
this.whiteCheckMarkEmojiButton = container.getByRole('button', {name: 'white check mark emoji'});
this.addReactionButton = container.getByRole('button', {name: 'add reaction'});
this.saveButton = container.getByRole('button', {name: 'save'});
this.actionsButton = container.getByRole('button', {name: 'actions'});
this.replyButton = container.getByRole('button', {name: 'reply'});
this.dotMenuButton = container.getByRole('button', {name: 'more'});
}

View File

@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test';
export default class PostReminderMenu {
readonly container: Locator;
readonly thirtyMinsMenuItem;
readonly oneHourMenuItem;
readonly twoHoursMenuItem;
readonly tomorrowMenuItem;
readonly customMenuItem;
constructor(container: Locator) {
this.container = container;
const getMenuItem = (hasText: string) => container.getByRole('menuitem').filter({hasText});
this.thirtyMinsMenuItem = getMenuItem('30 mins');
this.oneHourMenuItem = getMenuItem('1 hour');
this.twoHoursMenuItem = getMenuItem('2 hours');
this.tomorrowMenuItem = getMenuItem('Tomorrow');
this.customMenuItem = getMenuItem('Custom');
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
}
export {PostReminderMenu};

View File

@ -8,16 +8,13 @@ import {components} from '@e2e-support/ui/components';
export default class ChannelsSidebarRight {
readonly container: Locator;
readonly input;
readonly sendMessageButton;
readonly postCreate;
readonly closeButton;
constructor(container: Locator) {
this.container = container;
this.input = container.getByTestId('reply_textbox');
this.sendMessageButton = container.getByTestId('SendMessageButton');
this.postCreate = new components.ChannelsPostCreate(container.getByTestId('comment-create'), true);
this.closeButton = container.locator('#rhsCloseButton');
}
@ -25,29 +22,23 @@ export default class ChannelsSidebarRight {
await expect(this.container).toBeVisible();
}
async postMessage(message: string) {
await this.input.fill(message);
}
async sendMessage() {
await this.sendMessageButton.click();
}
/**
* Returns the value of the textbox in RHS
*/
async getInputValue() {
return await this.input.inputValue();
}
/**
* Returns the RHS post by post id
* @param postId Just the ID without the prefix
*/
async getRHSPostById(postId: string) {
const rhsPostId = `rhsPost_${postId}`;
const postLocator = this.container.locator(`#${rhsPostId}`);
return new components.ChannelsPost(postLocator);
async getPostById(postId: string) {
const post = this.container.locator(`[id="rhsPost_${postId}"]`);
await post.waitFor();
return new components.ChannelsPost(post);
}
/**
* Return the last post in the RHS
*/
async getLastPost() {
const post = this.container.getByTestId('rhsPostView').last();
await post.waitFor();
return new components.ChannelsPost(post);
}
/**

View File

@ -1,56 +1,60 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BoardsSidebar} from './boards/sidebar';
import {ChannelsHeader} from './channels/header';
import {ChannelsHeaderMobile} from './channels/header_mobile';
import {ChannelsAppBar} from './channels/app_bar';
import {ChannelsPostCreate} from './channels/post_create';
import {ChannelsPost} from './channels/post';
import {ChannelsCenterView} from './channels/center_view';
import {ChannelsSidebarLeft} from './channels/sidebar_left';
import {ChannelsSidebarRight} from './channels/sidebar_right';
import {DeletePostModal} from './channels/delete_post_modal';
import {FindChannelsModal} from './channels/find_channels_modal';
import {Footer} from './footer';
import {GlobalHeader} from './global_header';
import {MainHeader} from './main_header';
import {PostDotMenu} from './channels/post_dot_menu';
import {DeletePostModal} from './channels/delete_post_modal';
import {PostReminderMenu} from './channels/post_reminder_menu';
import {PostMenu} from './channels/post_menu';
import {ThreadFooter} from './channels/thread_footer';
import {EmojiGifPicker} from './channels/emoji_gif_picker';
const components = {
BoardsSidebar,
GlobalHeader,
ChannelsCenterView,
ChannelsSidebarLeft,
ChannelsSidebarRight,
ChannelsAppBar,
ChannelsHeader,
ChannelsHeaderMobile,
ChannelsPostCreate,
ChannelsPost,
ChannelsSidebarLeft,
ChannelsSidebarRight,
FindChannelsModal,
Footer,
GlobalHeader,
MainHeader,
PostDotMenu,
DeletePostModal,
PostDotMenu,
PostMenu,
ThreadFooter,
Footer,
MainHeader,
PostReminderMenu,
EmojiGifPicker,
};
export {
components,
BoardsSidebar,
GlobalHeader,
ChannelsCenterView,
ChannelsSidebarLeft,
ChannelsSidebarRight,
ChannelsAppBar,
ChannelsHeader,
ChannelsHeaderMobile,
ChannelsPostCreate,
ChannelsPost,
ChannelsSidebarLeft,
ChannelsSidebarRight,
FindChannelsModal,
GlobalHeader,
PostDotMenu,
DeletePostModal,
PostDotMenu,
PostMenu,
ThreadFooter,
};

View File

@ -1,47 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Page} from '@playwright/test';
import {components} from '@e2e-support/ui/components';
export default class BoardsCreatePage {
readonly boards = 'Boards';
readonly page: Page;
readonly globalHeader;
readonly createBoardHeading;
readonly createEmptyBoardButton;
readonly useTemplateButton;
constructor(page: Page) {
this.page = page;
this.globalHeader = new components.GlobalHeader(this.page.locator('#global-header'));
this.createBoardHeading = page.getByRole('heading', {name: 'Create a board'});
this.createEmptyBoardButton = page.getByRole('button', {name: ' Create an empty board'});
this.useTemplateButton = page.getByRole('button', {name: 'Use this template'});
}
async goto(teamId = '') {
let boardsUrl = '/boards';
if (teamId) {
boardsUrl += `/team/${teamId}`;
}
await this.page.goto(boardsUrl);
}
async toBeVisible() {
await this.globalHeader.toBeVisible(this.boards);
await expect(this.createEmptyBoardButton).toBeVisible();
await expect(this.useTemplateButton).toBeVisible();
await expect(this.createBoardHeading).toBeVisible();
}
async createEmptyBoard() {
await this.createEmptyBoardButton.click();
}
}
export {BoardsCreatePage};

View File

@ -1,60 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Page} from '@playwright/test';
import {components} from '@e2e-support/ui/components';
export default class BoardsViewPage {
readonly boards = 'Boards';
readonly page: Page;
readonly sidebar;
readonly globalHeader;
readonly topHead;
readonly editableTitle;
readonly shareButton;
constructor(page: Page) {
this.page = page;
this.sidebar = new components.BoardsSidebar(page.locator('.octo-sidebar'));
this.globalHeader = new components.GlobalHeader(this.page.locator('#global-header'));
this.topHead = page.locator('.top-head');
this.editableTitle = this.topHead.getByPlaceholder('Untitled board');
this.shareButton = page.getByRole('button', {name: '󰍁 Share'});
}
async goto(teamId = '', boardId = '', viewId = '', cardId = '') {
let boardsUrl = '/boards';
if (teamId) {
boardsUrl += `/team/${teamId}`;
if (boardId) {
boardsUrl += `/${boardId}`;
if (viewId) {
boardsUrl += `/${viewId}`;
if (cardId) {
boardsUrl += `/${cardId}`;
}
}
}
}
await this.page.goto(boardsUrl);
}
async toBeVisible() {
await this.page.waitForLoadState('networkidle');
await this.globalHeader.toBeVisible(this.boards);
await expect(this.shareButton).toBeVisible();
await expect(this.topHead).toBeVisible();
}
async shouldHaveUntitledBoard() {
await this.editableTitle.isVisible();
expect(await this.editableTitle.getAttribute('value')).toBe('');
await expect(this.page.getByTitle('(Untitled Board)')).toBeVisible();
}
}
export {BoardsViewPage};

View File

@ -3,36 +3,51 @@
import {Page} from '@playwright/test';
import {waitUntil} from '@e2e-support/test_action';
import {components} from '@e2e-support/ui/components';
import {duration, isSmallScreen} from '@e2e-support/util';
export default class ChannelsPage {
readonly channels = 'Channels';
readonly page: Page;
readonly postCreate;
readonly findChannelsModal;
readonly globalHeader;
readonly header;
readonly headerMobile;
readonly appBar;
readonly centerView;
readonly sidebarLeft;
readonly sidebarRight;
readonly postDotMenu;
readonly appBar;
readonly findChannelsModal;
readonly deletePostModal;
readonly postDotMenu;
readonly postReminderMenu;
readonly emojiGifPickerPopup;
constructor(page: Page) {
this.page = page;
this.postCreate = new components.ChannelsPostCreate(page.locator('#post-create'));
this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'}));
// The main areas of the app
this.globalHeader = new components.GlobalHeader(page.locator('#global-header'));
this.header = new components.ChannelsHeader(page.locator('.channel-header'));
this.headerMobile = new components.ChannelsHeaderMobile(page.locator('.navbar'));
this.appBar = new components.ChannelsAppBar(page.locator('.app-bar'));
this.centerView = new components.ChannelsCenterView(page.getByTestId('channel_view'));
this.sidebarLeft = new components.ChannelsSidebarLeft(page.locator('#SidebarContainer'));
this.sidebarRight = new components.ChannelsSidebarRight(page.locator('#sidebar-right'));
this.postDotMenu = new components.PostDotMenu(page.getByRole('menu', {name: 'Post extra options'}));
this.appBar = new components.ChannelsAppBar(page.locator('.app-bar'));
// Modals
this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'}));
this.deletePostModal = new components.DeletePostModal(page.locator('#deletePostModal'));
// Menus
this.postDotMenu = new components.PostDotMenu(page.getByRole('menu', {name: 'Post extra options'}));
this.postReminderMenu = new components.PostReminderMenu(page.getByRole('menu', {name: 'Set a reminder for:'}));
// Popovers
this.emojiGifPickerPopup = new components.EmojiGifPicker(page.locator('#emojiGifPicker'));
}
async toBeVisible() {
await this.centerView.toBeVisible();
}
async goto(teamName = '', channelName = '') {
@ -46,75 +61,6 @@ export default class ChannelsPage {
await this.page.goto(channelsUrl);
}
async toBeVisible() {
if (!isSmallScreen(this.page.viewportSize())) {
await this.globalHeader.toBeVisible(this.channels);
}
await this.postCreate.toBeVisible();
}
async postMessage(message: string) {
await this.postCreate.input.waitFor();
await this.postCreate.postMessage(message);
}
async sendMessage() {
await this.postCreate.sendMessage();
}
async getFirstPost() {
await this.page.getByTestId('postView').first().waitFor();
const post = await this.page.getByTestId('postView').first();
return new components.ChannelsPost(post);
}
async getLastPost() {
await this.page.getByTestId('postView').last().waitFor();
const post = await this.page.getByTestId('postView').last();
return new components.ChannelsPost(post);
}
async getNthPost(index: number) {
await this.page.getByTestId('postView').nth(index).waitFor();
const post = await this.page.getByTestId('postView').nth(index);
return new components.ChannelsPost(post);
}
async getPostById(id: string) {
await this.page.locator(`[id="post_${id}"]`).waitFor();
const post = await this.page.locator(`[id="post_${id}"]`);
return new components.ChannelsPost(post);
}
async getRHSPostById(id: string) {
await this.page.locator(`[id="rhsPost_${id}"]`).waitFor();
const post = await this.page.locator(`[id="rhsPost_${id}"]`);
return new components.ChannelsPost(post);
}
async waitUntilLastPostContains(text: string, timeout = duration.ten_sec) {
await waitUntil(
async () => {
const post = await this.getLastPost();
const content = await post.container.textContent();
return content?.includes(text);
},
{timeout}
);
}
async waitUntilPostWithIdContains(id: string, text: string, timeout = duration.ten_sec) {
await waitUntil(
async () => {
const post = await this.getPostById(id);
const content = await post.container.textContent();
return content?.includes(text);
},
{timeout}
);
}
}
export {ChannelsPage};

View File

@ -1,8 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BoardsCreatePage} from './boards_create';
import {BoardsViewPage} from './boards_view';
import {ChannelsPage} from './channels';
import {LandingLoginPage} from './landing_login';
import {LoginPage} from './login';
@ -10,8 +8,6 @@ import {ResetPasswordPage} from './reset_password';
import {SignupPage} from './signup';
const pages = {
BoardsCreatePage,
BoardsViewPage,
ChannelsPage,
LandingLoginPage,
LoginPage,
@ -19,4 +15,4 @@ const pages = {
SignupPage,
};
export {pages, BoardsCreatePage, BoardsViewPage, ChannelsPage, LandingLoginPage, LoginPage, SignupPage};
export {pages, ChannelsPage, LandingLoginPage, LoginPage, SignupPage};

Some files were not shown because too many files have changed in this diff Show More