CLD-7841 Optimize E2E testing costs (#27883)

* Utilize free runners for E2E tests, report back to PR on run completion
* Increase test stability on smaller instances
* Merge worker reports
* merge start+prepare steps
* upgrade keycloak
* increase test timeouts
* Add video recording to report artifacts

Co-authored-by: Saturnino Abril <5334504+saturninoabril@users.noreply.github.com>

---------

Co-authored-by: Saturnino Abril <5334504+saturninoabril@users.noreply.github.com>
Этот коммит содержится в:
Mario Vitale
2024-08-14 21:39:08 +02:00
коммит произвёл GitHub
родитель 5f19d8513b
Коммит 948f8941f0
37 изменённых файлов: 261 добавлений и 151 удалений

56
.github/workflows/e2e-fulltests-ci.yml поставляемый
Просмотреть файл

@@ -62,12 +62,15 @@ jobs:
REPORT_TYPE: "${{ steps.generate.outputs.REPORT_TYPE }}" REPORT_TYPE: "${{ steps.generate.outputs.REPORT_TYPE }}"
ROLLING_RELEASE_commit_sha: "${{ steps.generate.outputs.ROLLING_RELEASE_commit_sha }}" ROLLING_RELEASE_commit_sha: "${{ steps.generate.outputs.ROLLING_RELEASE_commit_sha }}"
ROLLING_RELEASE_SERVER_IMAGE: "${{ steps.generate.outputs.ROLLING_RELEASE_SERVER_IMAGE }}" ROLLING_RELEASE_SERVER_IMAGE: "${{ steps.generate.outputs.ROLLING_RELEASE_SERVER_IMAGE }}"
WORKFLOW_RUN_URL: "${{steps.generate.outputs.WORKFLOW_RUN_URL}}"
CYCLE_URL: "${{steps.generate.outputs.CYCLE_URL}}"
env: env:
GH_TOKEN: "${{ github.token }}" GH_TOKEN: "${{ github.token }}"
REF: "${{ inputs.ref || github.sha }}" REF: "${{ inputs.ref || github.sha }}"
PR_NUMBER: "${{ inputs.PR_NUMBER || '' }}" PR_NUMBER: "${{ inputs.PR_NUMBER || '' }}"
REPORT_TYPE: "${{ inputs.REPORT_TYPE }}" REPORT_TYPE: "${{ inputs.REPORT_TYPE }}"
ROLLING_RELEASE_FROM_TAG: "${{ inputs.ROLLING_RELEASE_FROM_TAG }}" ROLLING_RELEASE_FROM_TAG: "${{ inputs.ROLLING_RELEASE_FROM_TAG }}"
AUTOMATION_DASHBOARD_URL: "${{ secrets.MM_E2E_AUTOMATION_DASHBOARD_URL }}"
# We could exclude the @smoke group for PRs, but then we wouldn't have it in the report # We could exclude the @smoke group for PRs, but then we wouldn't have it in the report
TEST_FILTER_PR: >- TEST_FILTER_PR: >-
--stage="@prod" --stage="@prod"
@@ -161,9 +164,13 @@ jobs:
ROLLING_RELEASE_SERVER_IMAGE="mattermost/mattermost-enterprise-edition:${ROLLING_RELEASE_SERVER_IMAGE_TAG}" ROLLING_RELEASE_SERVER_IMAGE="mattermost/mattermost-enterprise-edition:${ROLLING_RELEASE_SERVER_IMAGE_TAG}"
BUILD_ID_SUFFIX="rolling${ROLLING_RELEASE_FROM_TAG/-/_}-$BUILD_ID_SUFFIX" BUILD_ID_SUFFIX="rolling${ROLLING_RELEASE_FROM_TAG/-/_}-$BUILD_ID_SUFFIX"
BUILD_ID_SUFFIX_IN_STATUS_CHECK=true BUILD_ID_SUFFIX_IN_STATUS_CHECK=true
WORKERS_NUMBER=10 # Rolling release tests are particularly impacted by increased parallelism. It's more efficient to run on fewer workers
### Run sanity assertions after variable generations ### Run sanity assertions after variable generations
git show-ref --verify "refs/tags/${ROLLING_RELEASE_FROM_TAG}" # 'inputs.ROLLING_RELEASE_FROM_TAG' must be a tag, for release report types git show-ref --verify "refs/tags/${ROLLING_RELEASE_FROM_TAG}" # 'inputs.ROLLING_RELEASE_FROM_TAG' must be a tag, for release report types
fi fi
# BUILD_ID format: $pipelineID-$imageTag-$testType-$serverType-$serverEdition
# Reference on BUILD_ID parsing: https://github.com/saturninoabril/automation-dashboard/blob/175891781bf1072c162c58c6ec0abfc5bcb3520e/lib/common_utils.ts#L3-L23
BUILD_ID="${{ github.run_id }}_${{ github.run_attempt }}-${SERVER_IMAGE_TAG}-${BUILD_ID_SUFFIX}"
echo "commit_sha=${COMMIT_SHA}" >> $GITHUB_OUTPUT echo "commit_sha=${COMMIT_SHA}" >> $GITHUB_OUTPUT
echo "BRANCH=${BRANCH}" >> $GITHUB_OUTPUT echo "BRANCH=${BRANCH}" >> $GITHUB_OUTPUT
echo "SERVER_IMAGE=${SERVER_IMAGE_ORG}/mattermost-enterprise-edition:${SERVER_IMAGE_TAG}" >> $GITHUB_OUTPUT echo "SERVER_IMAGE=${SERVER_IMAGE_ORG}/mattermost-enterprise-edition:${SERVER_IMAGE_TAG}" >> $GITHUB_OUTPUT
@@ -177,18 +184,22 @@ jobs:
echo "REPORT_TYPE=${COMPUTED_REPORT_TYPE}" >> $GITHUB_OUTPUT echo "REPORT_TYPE=${COMPUTED_REPORT_TYPE}" >> $GITHUB_OUTPUT
echo "ROLLING_RELEASE_commit_sha=${ROLLING_RELEASE_COMMIT_SHA}" >> $GITHUB_OUTPUT echo "ROLLING_RELEASE_commit_sha=${ROLLING_RELEASE_COMMIT_SHA}" >> $GITHUB_OUTPUT
echo "ROLLING_RELEASE_SERVER_IMAGE=${ROLLING_RELEASE_SERVER_IMAGE}" >> $GITHUB_OUTPUT echo "ROLLING_RELEASE_SERVER_IMAGE=${ROLLING_RELEASE_SERVER_IMAGE}" >> $GITHUB_OUTPUT
# BUILD_ID format: $pipelineID-$imageTag-$testType-$serverType-$serverEdition echo "BUILD_ID=${BUILD_ID}" >> $GITHUB_OUTPUT
# Reference on BUILD_ID parsing: https://github.com/saturninoabril/automation-dashboard/blob/175891781bf1072c162c58c6ec0abfc5bcb3520e/lib/common_utils.ts#L3-L23 # User notification variables
echo "BUILD_ID=${{ github.run_id }}_${{ github.run_attempt }}-${SERVER_IMAGE_TAG}-${BUILD_ID_SUFFIX}" >> $GITHUB_OUTPUT echo "WORKFLOW_RUN_URL=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{github.run_id}}" >> $GITHUB_OUTPUT
echo "CYCLE_URL=${AUTOMATION_DASHBOARD_URL%%/api}/cycle/${BUILD_ID}" >> $GITHUB_OUTPUT
- name: ci/notify-user - name: ci/notify-user
env:
COMMIT_SHA: "${{steps.generate.outputs.commit_sha}}"
STATUS_CHECK_CONTEXT: "${{steps.generate.outputs.status_check_context}}"
WORKFLOW_RUN_URL: "${{steps.generate.outputs.WORKFLOW_RUN_URL}}"
CYCLE_URL: "${{steps.generate.outputs.CYCLE_URL}}"
run: | run: |
if [ -n "$PR_NUMBER" ]; then if [ -n "$PR_NUMBER" ]; then
WORKFLOW_RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
CYCLE_URL="https://automation-dashboard.vercel.app/cycle/${{ github.run_id }}"
gh issue -R "${{ github.repository }}" comment "$PR_NUMBER" --body-file - <<EOF gh issue -R "${{ github.repository }}" comment "$PR_NUMBER" --body-file - <<EOF
E2E test run is starting for commit \`${{ steps.generate.outputs.commit_sha }}\`${MM_ENV:+, with \`MM_ENV=$MM_ENV\`}. E2E test run is starting for commit \`${COMMIT_SHA}\`${MM_ENV:+, with \`MM_ENV=$MM_ENV\`}.
You can check its progress by either: You can check its progress by either:
- Looking at the corresponding commit status, which will be available in a few moments: \`${{ steps.generate.outputs.status_check_context }}\`. - Looking at the corresponding commit status, which will be available in a few moments: \`${STATUS_CHECK_CONTEXT}\`.
- Looking at the [E2E test's Workflow Run]($WORKFLOW_RUN_URL), with Run ID \`${{ github.run_id }}\`. - Looking at the [E2E test's Workflow Run]($WORKFLOW_RUN_URL), with Run ID \`${{ github.run_id }}\`.
- Looking at the [E2E test's Cycle on the Automation Dashboard]($CYCLE_URL). - Looking at the [E2E test's Cycle on the Automation Dashboard]($CYCLE_URL).
EOF EOF
@@ -229,3 +240,34 @@ jobs:
REPORT_TM4J_TEST_CYCLE_LINK_PREFIX: "${{ secrets.MM_E2E_TEST_CYCLE_LINK_PREFIX }}" REPORT_TM4J_TEST_CYCLE_LINK_PREFIX: "${{ secrets.MM_E2E_TEST_CYCLE_LINK_PREFIX }}"
CWS_URL: "${{ needs.generate-test-variables.outputs.SERVER == 'cloud' && secrets.MM_E2E_CWS_URL || '' }}" CWS_URL: "${{ needs.generate-test-variables.outputs.SERVER == 'cloud' && secrets.MM_E2E_CWS_URL || '' }}"
CWS_EXTRA_HTTP_HEADERS: "${{ needs.generate-test-variables.outputs.SERVER == 'cloud' && secrets.MM_E2E_CWS_EXTRA_HTTP_HEADERS || '' }}" CWS_EXTRA_HTTP_HEADERS: "${{ needs.generate-test-variables.outputs.SERVER == 'cloud' && secrets.MM_E2E_CWS_EXTRA_HTTP_HEADERS || '' }}"
notify-user:
runs-on: ubuntu-latest
if: always()
needs:
- generate-test-variables
- e2e-fulltest
permissions:
issues: write
pull-requests: write
defaults:
run:
shell: bash
env:
GH_TOKEN: "${{ github.token }}"
PR_NUMBER: "${{ inputs.PR_NUMBER || '' }}"
MM_ENV: "${{ inputs.MM_ENV || '' }}"
COMMIT_SHA: "${{ needs.generate-test-variables.outputs.commit_sha }}"
STATUS_CHECK_CONTEXT: "${{ needs.generate-test-variables.outputs.status_check_context }}"
WORKFLOW_RUN_URL: "${{ needs.generate-test-variables.outputs.WORKFLOW_RUN_URL }}"
CYCLE_URL: "${{ needs.generate-test-variables.outputs.CYCLE_URL }}"
steps:
- name: ci/notify-user-test-completion
run: |
if [ -n "$PR_NUMBER" ]; then
gh issue -R "${{ github.repository }}" comment "$PR_NUMBER" --body-file - <<EOF
E2E test has completed for commit \`${COMMIT_SHA}\`${MM_ENV:+, with \`MM_ENV=$MM_ENV\`}.
You can check its result on the [Automation Dashboard]($CYCLE_URL), or by checking out the status check named \`${STATUS_CHECK_CONTEXT}\` in this PR.
The run summary artifact is available in the corresponding [Workflow Run]($WORKFLOW_RUN_URL).
EOF
fi

16
.github/workflows/e2e-tests-ci-template.yml поставляемый
Просмотреть файл

@@ -240,11 +240,11 @@ jobs:
# - For MacOS: works on developer machines, but uses too many resources to be able to run on Github Actions # - 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 # - 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, windows-2022, macos-12-xl]
os: [ubuntu-latest-8-cores] os: [ubuntu-latest]
worker_index: ${{ fromJSON(needs.generate-build-variables.outputs.workers) }} # https://docs.github.com/en/actions/learn-github-actions/expressions#example-returning-a-json-object worker_index: ${{ fromJSON(needs.generate-build-variables.outputs.workers) }} # https://docs.github.com/en/actions/learn-github-actions/expressions#example-returning-a-json-object
runs-on: "${{ matrix.os }}" runs-on: "${{ matrix.os }}"
timeout-minutes: 90 timeout-minutes: 120
needs: needs:
- cypress-check - cypress-check
- playwright-check - playwright-check
@@ -265,7 +265,7 @@ jobs:
MM_ENV: "${{ inputs.MM_ENV }}" MM_ENV: "${{ inputs.MM_ENV }}"
BRANCH: "${{ inputs.BRANCH }}" BRANCH: "${{ inputs.BRANCH }}"
BUILD_ID: "${{ inputs.BUILD_ID }}" BUILD_ID: "${{ inputs.BUILD_ID }}"
CI_BASE_URL: "http://localhost:8065/?worker_index=${{ matrix.worker_index }}" CI_BASE_URL: "${{ matrix.os }}-${{ matrix.worker_index }}"
CYPRESS_pushNotificationServer: "${{ secrets.PUSH_NOTIFICATION_SERVER }}" CYPRESS_pushNotificationServer: "${{ secrets.PUSH_NOTIFICATION_SERVER }}"
CWS_URL: "${{ secrets.CWS_URL }}" CWS_URL: "${{ secrets.CWS_URL }}"
CWS_EXTRA_HTTP_HEADERS: "${{ secrets.CWS_EXTRA_HTTP_HEADERS }}" CWS_EXTRA_HTTP_HEADERS: "${{ secrets.CWS_EXTRA_HTTP_HEADERS }}"
@@ -326,6 +326,7 @@ jobs:
path: | path: |
e2e-tests/cypress/logs/ e2e-tests/cypress/logs/
e2e-tests/cypress/results/ e2e-tests/cypress/results/
retention-days: 1
report: report:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -349,6 +350,13 @@ jobs:
with: with:
path: e2e-tests/cypress/ path: e2e-tests/cypress/
merge-multiple: true merge-multiple: true
- name: ci/upload-report-global
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: e2e-test-results
path: |
e2e-tests/cypress/logs/
e2e-tests/cypress/results/
- name: ci/report-calculate-failures - name: ci/report-calculate-failures
id: calculate-failures id: calculate-failures
run: | run: |

Просмотреть файл

@@ -93,6 +93,7 @@ export ENABLED_DOCKER_SERVICES_DEFAULT="postgres inbucket"
export TEST_FILTER_DEFAULT='--stage=@prod --group=@smoke' export TEST_FILTER_DEFAULT='--stage=@prod --group=@smoke'
export BRANCH_DEFAULT=$(git branch --show-current || echo -n "unknown") export BRANCH_DEFAULT=$(git branch --show-current || echo -n "unknown")
export BUILD_ID_DEFAULT=$(date +%s) export BUILD_ID_DEFAULT=$(date +%s)
export CI_BASE_URL_DEFAULT="localhost"
# OS specific defaults overrides # OS specific defaults overrides
case $MME2E_OSTYPE in case $MME2E_OSTYPE in
darwin ) darwin )
@@ -111,3 +112,4 @@ export BRANCH_UNSANITIZED=${BRANCH:-$BRANCH_DEFAULT}
export BRANCH=${BRANCH_UNSANITIZED::50} # The automation dashboard only accepts branch names up to 50 characters export BRANCH=${BRANCH_UNSANITIZED::50} # The automation dashboard only accepts branch names up to 50 characters
export BUILD_ID_UNSANITIZED=${BUILD_ID:-$BUILD_ID_DEFAULT} export BUILD_ID_UNSANITIZED=${BUILD_ID:-$BUILD_ID_DEFAULT}
export BUILD_ID=${BUILD_ID_UNSANITIZED::64} # The automation dashboard only accepts build IDs up to 64 characters export BUILD_ID=${BUILD_ID_UNSANITIZED::64} # The automation dashboard only accepts build IDs up to 64 characters
export CI_BASE_URL=${CI_BASE_URL:-$CI_BASE_URL_DEFAULT}

Просмотреть файл

@@ -276,7 +276,6 @@ generate_env_files() {
# Generating TEST-specific env files # Generating TEST-specific env files
# Some are defaulted in .e2erc due to being needed to other scripts as well # Some are defaulted in .e2erc due to being needed to other scripts as well
export CI_BASE_URL="${CI_BASE_URL:-http://localhost:8065}"
export REPO=mattermost # Static, but declared here for making generate_test_cycle.js easier to run export REPO=mattermost # Static, but declared here for making generate_test_cycle.js easier to run
export HEADLESS=true # Static, but declared here for making generate_test_cycle.js easier to run export HEADLESS=true # Static, but declared here for making generate_test_cycle.js easier to run
case "$TEST" in case "$TEST" in

Просмотреть файл

@@ -29,15 +29,16 @@ EOF
# Run cypress test # Run cypress test
# No need to collect its exit status: if it's nonzero, this script will terminate since we use '-e' # No need to collect its exit status: if it's nonzero, this script will terminate since we use '-e'
LOGFILE_SUFFIX="${CI_BASE_URL//\//_}" # Remove slashes from CI_BASE_URL to produce a usable filename
# shellcheck disable=SC2016 # shellcheck disable=SC2016
if ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash -c '[ -n "${AUTOMATION_DASHBOARD_URL}" ]'; then if ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress bash -c '[ -n "${AUTOMATION_DASHBOARD_URL}" ]'; then
mme2e_log "AUTOMATION_DASHBOARD_URL is set. Using run_test_cycle.js for the cypress run" mme2e_log "AUTOMATION_DASHBOARD_URL is set. Using run_test_cycle.js for the cypress run"
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress node run_test_cycle.js | tee ../cypress/logs/cypress.log ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress node run_test_cycle.js | tee ../cypress/logs/${LOGFILE_SUFFIX}_cypress.log
else else
mme2e_log "AUTOMATION_DASHBOARD_URL is unset. Using run_tests.js for the cypress run" mme2e_log "AUTOMATION_DASHBOARD_URL is unset. Using run_tests.js for the cypress run"
# shellcheck disable=SC2086 # shellcheck disable=SC2086
${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress node run_tests.js $TEST_FILTER | tee ../cypress/logs/cypress.log ${MME2E_DC_SERVER} exec -T -u "$MME2E_UID" -- cypress node run_tests.js $TEST_FILTER | tee ../cypress/logs/${LOGFILE_SUFFIX}_cypress.log
fi fi
# Collect server logs # Collect server logs
${MME2E_DC_SERVER} logs --no-log-prefix -- server >../cypress/logs/mattermost.log 2>&1 ${MME2E_DC_SERVER} logs --no-log-prefix -- server >../cypress/logs/${LOGFILE_SUFFIX}_mattermost.log 2>&1

Просмотреть файл

@@ -2,18 +2,17 @@ SHELL := /bin/bash
.PHONY: all run stop clean .PHONY: all run stop clean
all: run all: run
run: generate-server start-server prepare-server run-test run: generate-server start-server run-test
stop: stop-server stop-dashboard clean stop: stop-server stop-dashboard clean
clean: clean:
rm -fv .ci/server.yml rm -fv .ci/server.yml
rm -fv .ci/.env.{server,dashboard,cypress,playwright} rm -fv .ci/.env.{server,dashboard,cypress,playwright}
.PHONY: generate-server start-server prepare-server run-test stop-server restart-server .PHONY: generate-server start-server run-test stop-server restart-server
generate-server: generate-server:
bash ./.ci/server.generate.sh bash ./.ci/server.generate.sh
start-server: generate-server start-server: generate-server
bash ./.ci/server.start.sh bash ./.ci/server.start.sh
prepare-server:
bash ./.ci/server.prepare.sh bash ./.ci/server.prepare.sh
run-test: run-test:
bash ./.ci/server.run_test.sh bash ./.ci/server.run_test.sh

Просмотреть файл

@@ -55,7 +55,7 @@ Notes:
* 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. * If you need to introduce variables that you want to control from `.ci/env`: you need to update the scripts under the `.ci/` dir and configure them to write the new variables' values over to the appropriate `.env.*` file. In particular, avoid defining variables that depend on other variables within the docker-compose override files: this is to ensure uniformity in their availability and simplifies the question of what container has access to which variable considerably.
* Exceptions are of course accepted wherever it makes sense (e.g. if you need to group variables based on some common functionality) * Exceptions are of course accepted wherever it makes sense (e.g. if you need to group variables based on some common functionality)
- The `publish-report` Make target is meant for internal usage. Usage and variables are documented in the respective scripts. - The `publish-report` Make target is meant for internal usage. Usage and variables are documented in the respective scripts.
- `make start-server` won't cleanup containers that don't change across runs. This means that you can use it to emulate a Mattermost server upgrade while retaining your database data by simply changing the `SERVER_IMAGE` variable on your machine, and then re-runing `make start-server` (and `make prepare-server` if you intend to run cypress again, since its container is also recreated). But this also means that if you want to run a clean local environment, you may have to manually run `make stop` to cleanup any running containers and their volumes, which include e.g. the database. - `make start-server` won't cleanup containers that don't change across runs. This means that you can use it to emulate a Mattermost server upgrade while retaining your database data by simply changing the `SERVER_IMAGE` variable on your machine, and then re-running `make start-server`. But this also means that if you want to run a clean local environment, you may have to manually run `make stop` to cleanup any running containers and their volumes, which include e.g. the database.
##### For code changes: ##### For code changes:
* `make fmt-ci` to format and check yaml files and shell scripts. * `make fmt-ci` to format and check yaml files and shell scripts.

Просмотреть файл

@@ -5,13 +5,15 @@ import {defineConfig} from 'cypress';
export default defineConfig({ export default defineConfig({
chromeWebSecurity: false, chromeWebSecurity: false,
defaultCommandTimeout: 20000, defaultCommandTimeout: 30000,
downloadsFolder: 'tests/downloads', downloadsFolder: 'tests/downloads',
fixturesFolder: 'tests/fixtures', fixturesFolder: 'tests/fixtures',
numTestsKeptInMemory: 0, numTestsKeptInMemory: 0,
screenshotsFolder: 'tests/screenshots', screenshotsFolder: 'tests/screenshots',
taskTimeout: 20000, taskTimeout: 20000,
video: false, video: true,
videoCompression: true,
videosFolder: 'tests/videos',
viewportWidth: 1300, viewportWidth: 1300,
env: { env: {
adminEmail: 'sysadmin@sample.mattermost.com', adminEmail: 'sysadmin@sample.mattermost.com',

70
e2e-tests/cypress/package-lock.json сгенерированный
Просмотреть файл

@@ -38,7 +38,7 @@
"chalk": "4.1.2", "chalk": "4.1.2",
"client-oauth2": "github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49", "client-oauth2": "github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.7.3", "cypress": "13.13.2",
"cypress-file-upload": "5.0.8", "cypress-file-upload": "5.0.8",
"cypress-multi-reporters": "1.6.4", "cypress-multi-reporters": "1.6.4",
"cypress-plugin-tab": "1.0.5", "cypress-plugin-tab": "1.0.5",
@@ -4518,13 +4518,13 @@
"dev": true "dev": true
}, },
"node_modules/cypress": { "node_modules/cypress": {
"version": "13.7.3", "version": "13.13.2",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.7.3.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.2.tgz",
"integrity": "sha512-uoecY6FTCAuIEqLUYkTrxamDBjMHTYak/1O7jtgwboHiTnS1NaMOoR08KcTrbRZFCBvYOiS4tEkQRmsV+xcrag==", "integrity": "sha512-PvJQU33933NvS1StfzEb8/mu2kMy4dABwCF+yd5Bi7Qly1HOVf+Bufrygee/tlmty/6j5lX+KIi8j9Q3JUMbhA==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@cypress/request": "^3.0.0", "@cypress/request": "^3.0.1",
"@cypress/xvfb": "^1.2.4", "@cypress/xvfb": "^1.2.4",
"@types/sinonjs__fake-timers": "8.1.1", "@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2", "@types/sizzle": "^2.3.2",
@@ -4563,7 +4563,7 @@
"request-progress": "^3.0.0", "request-progress": "^3.0.0",
"semver": "^7.5.3", "semver": "^7.5.3",
"supports-color": "^8.1.1", "supports-color": "^8.1.1",
"tmp": "~0.2.1", "tmp": "~0.2.3",
"untildify": "^4.0.0", "untildify": "^4.0.0",
"yauzl": "^2.10.0" "yauzl": "^2.10.0"
}, },
@@ -10429,30 +10429,12 @@
"dev": true "dev": true
}, },
"node_modules/tmp": { "node_modules/tmp": {
"version": "0.2.1", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
"dev": true, "dev": true,
"dependencies": {
"rimraf": "^3.0.0"
},
"engines": { "engines": {
"node": ">=8.17.0" "node": ">=14.14"
}
},
"node_modules/tmp/node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/to-fast-properties": { "node_modules/to-fast-properties": {
@@ -14844,12 +14826,12 @@
"dev": true "dev": true
}, },
"cypress": { "cypress": {
"version": "13.7.3", "version": "13.13.2",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.7.3.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.2.tgz",
"integrity": "sha512-uoecY6FTCAuIEqLUYkTrxamDBjMHTYak/1O7jtgwboHiTnS1NaMOoR08KcTrbRZFCBvYOiS4tEkQRmsV+xcrag==", "integrity": "sha512-PvJQU33933NvS1StfzEb8/mu2kMy4dABwCF+yd5Bi7Qly1HOVf+Bufrygee/tlmty/6j5lX+KIi8j9Q3JUMbhA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@cypress/request": "^3.0.0", "@cypress/request": "^3.0.1",
"@cypress/xvfb": "^1.2.4", "@cypress/xvfb": "^1.2.4",
"@types/sinonjs__fake-timers": "8.1.1", "@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2", "@types/sizzle": "^2.3.2",
@@ -14888,7 +14870,7 @@
"request-progress": "^3.0.0", "request-progress": "^3.0.0",
"semver": "^7.5.3", "semver": "^7.5.3",
"supports-color": "^8.1.1", "supports-color": "^8.1.1",
"tmp": "~0.2.1", "tmp": "~0.2.3",
"untildify": "^4.0.0", "untildify": "^4.0.0",
"yauzl": "^2.10.0" "yauzl": "^2.10.0"
}, },
@@ -19310,24 +19292,10 @@
"dev": true "dev": true
}, },
"tmp": { "tmp": {
"version": "0.2.1", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
"dev": true, "dev": true
"requires": {
"rimraf": "^3.0.0"
},
"dependencies": {
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
}
}
}, },
"to-fast-properties": { "to-fast-properties": {
"version": "2.0.0", "version": "2.0.0",

Просмотреть файл

@@ -33,7 +33,7 @@
"chalk": "4.1.2", "chalk": "4.1.2",
"client-oauth2": "github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49", "client-oauth2": "github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.7.3", "cypress": "13.13.2",
"cypress-file-upload": "5.0.8", "cypress-file-upload": "5.0.8",
"cypress-multi-reporters": "1.6.4", "cypress-multi-reporters": "1.6.4",
"cypress-plugin-tab": "1.0.5", "cypress-plugin-tab": "1.0.5",

Просмотреть файл

@@ -63,6 +63,7 @@ async function runCypressTest(specExecution) {
spec: specExecution.file, spec: specExecution.file,
config: { config: {
screenshotsFolder: `${MOCHAWESOME_REPORT_DIR}/screenshots`, screenshotsFolder: `${MOCHAWESOME_REPORT_DIR}/screenshots`,
videosFolder: `${MOCHAWESOME_REPORT_DIR}/videos`,
trashAssetsBeforeRuns: false, trashAssetsBeforeRuns: false,
}, },
reporter: 'cypress-multi-reporters', reporter: 'cypress-multi-reporters',

Просмотреть файл

@@ -109,6 +109,7 @@ async function runTests() {
spec: testFile, spec: testFile,
config: { config: {
screenshotsFolder: `${MOCHAWESOME_REPORT_DIR}/screenshots`, screenshotsFolder: `${MOCHAWESOME_REPORT_DIR}/screenshots`,
videosFolder: `${MOCHAWESOME_REPORT_DIR}/videos`,
trashAssetsBeforeRuns: false, trashAssetsBeforeRuns: false,
}, },
env: { env: {

Просмотреть файл

@@ -188,6 +188,10 @@ describe('Verify Accessibility Support in Post', () => {
cy.get(`#CENTER_flagIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'save message'); cy.get(`#CENTER_flagIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'save message');
cy.focused().tab(); cy.focused().tab();
// * Verify focus is on the actions button
cy.get(`#CENTER_actions_button_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'actions');
cy.focused().tab();
// * Verify focus is on the comment button // * Verify focus is on the comment button
cy.get(`#CENTER_commentIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'reply'); cy.get(`#CENTER_commentIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'reply');
cy.focused().tab(); cy.focused().tab();
@@ -234,6 +238,10 @@ describe('Verify Accessibility Support in Post', () => {
cy.get(`#RHS_COMMENT_button_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'more'); cy.get(`#RHS_COMMENT_button_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'more');
cy.focused().tab({shift: true}); cy.focused().tab({shift: true});
// * Verify focus is on the actions button
cy.get(`#RHS_COMMENT_actions_button_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'actions');
cy.focused().tab({shift: true});
// * Verify focus is on the save icon // * Verify focus is on the save icon
cy.get(`#RHS_COMMENT_flagIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'save message'); cy.get(`#RHS_COMMENT_flagIcon_${postId}`).should('be.focused').and('have.attr', 'aria-label', 'save message');
cy.focused().tab({shift: true}); cy.focused().tab({shift: true});
@@ -333,8 +341,9 @@ function verifyPostLabel(elementId, username, labelSuffix) {
// * Verify reader reads out the post correctly // * Verify reader reads out the post correctly
cy.get('@lastPost').then((el) => { cy.get('@lastPost').then((el) => {
// # Get the post time // # Get the post time
cy.wrap(el).find('time.post__time').invoke('text').then((time) => { cy.wrap(el).find('time.post__time').invoke('attr', 'datetime').then((time) => {
const expectedLabel = `At ${time} ${Cypress.dayjs().format('dddd, MMMM D')}, ${username} ${labelSuffix}`; const parsedTime = Cypress.dayjs(time);
const expectedLabel = `At ${parsedTime.format('h:mm A dddd, MMMM D')}, ${username} ${labelSuffix}`;
cy.wrap(el).should('have.attr', 'aria-label', expectedLabel); cy.wrap(el).should('have.attr', 'aria-label', expectedLabel);
}); });
}); });

Просмотреть файл

@@ -26,7 +26,9 @@ describe('Archived channels', () => {
it('MM-T1721 Archive channel posts menu should have copy link and reply options', () => { it('MM-T1721 Archive channel posts menu should have copy link and reply options', () => {
// # Click to add a channel description // # Click to add a channel description
cy.findByRoleExtended('button', {name: 'Set header'}).should('be.visible').click(); // Note that it is invisible until the mouse hovers it, which is unfeasible in Cypress
// https://docs.cypress.io/api/commands/hover
cy.findByText('Add a channel header').click({force: true});
// # Add channel header for system message // # Add channel header for system message
const header = 'this is a header!'; const header = 'this is a header!';

Просмотреть файл

@@ -35,6 +35,9 @@ describe('Bot display name', () => {
}); });
cy.visit(`/${team.name}/channels/off-topic`); cy.visit(`/${team.name}/channels/off-topic`);
cy.wait('@resources'); cy.wait('@resources');
// # Wait for the page to fully load before continuing
cy.get('#sidebar-header-container').should('be.visible').and('have.text', team.display_name);
}); });
}); });
}); });

Просмотреть файл

@@ -31,6 +31,8 @@ describe('MM-T4063 Custom status expiry', () => {
const waitingTime = 30; //minutes const waitingTime = 30; //minutes
let expiresAt = dayjs(); let expiresAt = dayjs();
let expiresAtAcceptableValues = [''];
let expiresAtRegexp: RegExp;
const expiryTimeFormat = 'h:mm A'; const expiryTimeFormat = 'h:mm A';
it('MM-T4063_1 should open status dropdown', () => { it('MM-T4063_1 should open status dropdown', () => {
// # Click on the sidebar header to open status dropdown // # Click on the sidebar header to open status dropdown
@@ -77,7 +79,12 @@ describe('MM-T4063 Custom status expiry', () => {
cy.get('#custom_status_modal').should('not.exist'); cy.get('#custom_status_modal').should('not.exist');
// # Setting the time at which the custom status should be expired // # Setting the time at which the custom status should be expired
// # Note that we need to be flexible around accepted values, as this calculation and the server-side one may differ slightly
expiresAt = dayjs().add(waitingTime, 'minute'); expiresAt = dayjs().add(waitingTime, 'minute');
expiresAtAcceptableValues = [-1, 0, 1].map((el) =>
expiresAt.add(el, 'minute').format(expiryTimeFormat),
);
expiresAtRegexp = new RegExp(`(${expiresAtAcceptableValues.join('|')})`);
// * Status should be set and the emoji should be visible in the sidebar header // * Status should be set and the emoji should be visible in the sidebar header
cy.uiGetProfileHeader(). cy.uiGetProfileHeader().
@@ -97,7 +104,7 @@ describe('MM-T4063 Custom status expiry', () => {
cy.get('.status-dropdown-menu .custom_status__row span.emoticon').invoke('attr', 'data-emoticon').should('contain', customStatus.emoji); cy.get('.status-dropdown-menu .custom_status__row span.emoticon').invoke('attr', 'data-emoticon').should('contain', customStatus.emoji);
// * Correct clear time should be displayed in the status dropdown // * Correct clear time should be displayed in the status dropdown
cy.get('.status-dropdown-menu .custom_status__expiry time').should('have.text', expiresAt.format(expiryTimeFormat)); cy.get('.status-dropdown-menu .custom_status__expiry time').invoke('text').should('match', expiresAtRegexp);
}); });
it('MM-T4063_6 custom status should be cleared after duration of set custom status', () => { it('MM-T4063_6 custom status should be cleared after duration of set custom status', () => {
@@ -116,7 +123,7 @@ describe('MM-T4063 Custom status expiry', () => {
// * Should show expiry time of status when current status is selected // * Should show expiry time of status when current status is selected
cy.get('#custom_status_modal .statusSuggestion__content').contains('span', customStatus.text).click(); cy.get('#custom_status_modal .statusSuggestion__content').contains('span', customStatus.text).click();
cy.get('#custom_status_modal .expiry-value').should('have.text', expiresAt.format(expiryTimeFormat)); cy.get('#custom_status_modal .expiry-value').invoke('text').should('match', expiresAtRegexp);
// # Close custom status modal // # Close custom status modal
cy.get('#custom_status_modal .modal-header .close').click(); cy.get('#custom_status_modal .modal-header .close').click();

Просмотреть файл

@@ -142,7 +142,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
}); });
} }
// # Visit the test channel // # Visit the test channel, and wait for the page to fully load
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`); cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
// # Open Add Members Dialog // # Open Add Members Dialog
@@ -160,6 +160,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
// # Search for a text and then check up and down arrow // # Search for a text and then check up and down arrow
cy.findByRole('textbox', {name: 'Search for people or groups'}). cy.findByRole('textbox', {name: 'Search for people or groups'}).
wait(TIMEOUTS.HALF_SEC).
typeWithForce('u'). typeWithForce('u').
wait(TIMEOUTS.HALF_SEC). wait(TIMEOUTS.HALF_SEC).
typeWithForce('{downarrow}{downarrow}{downarrow}{uparrow}'); typeWithForce('{downarrow}{downarrow}{downarrow}{uparrow}');

Просмотреть файл

@@ -10,6 +10,8 @@
// Stage: @prod // Stage: @prod
// Group: @channels @files_and_attachments // Group: @channels @files_and_attachments
import * as TIMEOUTS from '../../../fixtures/timeouts';
describe('YouTube Video', () => { describe('YouTube Video', () => {
before(() => { before(() => {
// # Enable Link Previews // # Enable Link Previews
@@ -27,10 +29,12 @@ describe('YouTube Video', () => {
it('MM-T2258 YouTube Video play, collapse', () => { it('MM-T2258 YouTube Video play, collapse', () => {
// # Post message // # Post message
cy.postMessage('https://www.youtube.com/watch?v=gLNmtUEvI5A'); const youtubeUrl = 'https://www.youtube.com/watch?v=gLNmtUEvI5A';
cy.postMessage(youtubeUrl);
cy.uiWaitUntilMessagePostedIncludes(youtubeUrl);
cy.getLastPost().within(() => { cy.getLastPost().within(() => {
// # Click play button // # Click play button
cy.get('.play-button').click(); cy.get('.play-button', {timeout: TIMEOUTS.TEN_SEC}).click();
// * Video should be loaded in the iframe // * Video should be loaded in the iframe
cy.get('.video-div > iframe').should('exist'); cy.get('.video-div > iframe').should('exist');

Просмотреть файл

@@ -87,8 +87,7 @@ describe('Integrations', () => {
// # Type "/echo message 3" // # Type "/echo message 3"
cy.uiGetPostTextBox().clear().type(`/echo ${message} 3{enter}`); cy.uiGetPostTextBox().clear().type(`/echo ${message} 3{enter}`);
// * Verify that post is not shown after 1 second // * Verify that post is not shown after pressing enter
cy.wait(TIMEOUTS.ONE_SEC);
cy.getLastPost().within(() => { cy.getLastPost().within(() => {
cy.findByText(message).should('not.exist'); cy.findByText(message).should('not.exist');
}); });

Просмотреть файл

@@ -84,7 +84,8 @@ describe('Integrations', () => {
loginAndVisitChannel(testUser, testChannelUrl); loginAndVisitChannel(testUser, testChannelUrl);
// # In a GM use the /invite command to invite a user to a channel you have permission to add them to but place extra white space before the username // # In a GM use the /invite command to invite a user to a channel you have permission to add them to but place extra white space before the username
cy.postMessage(`/groupmsg @${member1.username} @${member2.username} `); cy.postMessage(`/groupmsg @${member1.username},@${member2.username} `);
cy.uiGetChannelHeaderButton().contains(member1.username).contains(member2.username);
cy.postMessage(`/invite @${userToInviteGM.username} ~${testChannel.name} `); cy.postMessage(`/invite @${userToInviteGM.username} ~${testChannel.name} `);
// * User added to channel as expected // * User added to channel as expected
@@ -133,7 +134,9 @@ describe('Integrations', () => {
loginAndVisitChannel(testUser, testChannelUrl); loginAndVisitChannel(testUser, testChannelUrl);
// # In a GM Use the /invite command to invite a channel to another channel (e.g., /invite @[channel name]) // # In a GM Use the /invite command to invite a channel to another channel (e.g., /invite @[channel name])
cy.postMessage(`/groupmsg @${member1.username} @${member2.username} `); cy.postMessage(`/groupmsg @${member1.username},@${member2.username} `);
cy.uiGetChannelHeaderButton().contains(member1.username).contains(member2.username);
cy.reload(); // Required for Cypress to write in the right channel
cy.postMessage(`/invite @${testChannel.name} `); cy.postMessage(`/invite @${testChannel.name} `);
// * Error appears: "We couldn't find the user. They may have been deactivated by the System Administrator." // * Error appears: "We couldn't find the user. They may have been deactivated by the System Administrator."
@@ -159,7 +162,9 @@ describe('Integrations', () => {
loginAndVisitChannel(testUser, testChannelUrl); loginAndVisitChannel(testUser, testChannelUrl);
// # In a GM use the /invite command to invite someone to a channel they're already a member of // # In a GM use the /invite command to invite someone to a channel they're already a member of
cy.postMessage(`/groupmsg @${member1.username} @${member2.username} `); cy.postMessage(`/groupmsg @${member1.username},@${member2.username} `);
cy.uiGetChannelHeaderButton().contains(member1.username).contains(member2.username);
cy.reload(); // Required for Cypress to write in the right channel
cy.postMessage(`/invite @${userToInvite.username} ~${testChannel.name} `); cy.postMessage(`/invite @${userToInvite.username} ~${testChannel.name} `);
// * Error appears: "[username] is already in the channel" // * Error appears: "[username] is already in the channel"

Просмотреть файл

@@ -67,6 +67,7 @@ describe('Integrations', () => {
click(); click();
// * Verify that only "Hello World" is posted in off-topic channel // * Verify that only "Hello World" is posted in off-topic channel
cy.uiWaitUntilMessagePostedIncludes('Hello World');
cy.getLastPostId().then((postId) => { cy.getLastPostId().then((postId) => {
cy.get(`#postMessageText_${postId}`).should('be.visible').and('have.text', 'Hello World'); cy.get(`#postMessageText_${postId}`).should('be.visible').and('have.text', 'Hello World');
}); });
@@ -103,6 +104,7 @@ describe('Integrations', () => {
click(); click();
// * Verify that both messages are posted in off-topic channel // * Verify that both messages are posted in off-topic channel
cy.uiWaitUntilMessagePostedIncludes('Hello World');
cy.getLastPostId().then((postId) => { cy.getLastPostId().then((postId) => {
cy.get(`#postMessageText_${postId}`).should('be.visible').and('have.text', 'Hello World'); cy.get(`#postMessageText_${postId}`).should('be.visible').and('have.text', 'Hello World');
}); });

Просмотреть файл

@@ -11,6 +11,7 @@
// Group: @channels @mark_as_unread // Group: @channels @mark_as_unread
import {markAsUnreadFromPost, switchToChannel} from './helpers'; import {markAsUnreadFromPost, switchToChannel} from './helpers';
import * as TIMEOUTS from '../../../fixtures/timeouts';
describe('Verify unread toast appears after repeated manual marking post as unread', () => { describe('Verify unread toast appears after repeated manual marking post as unread', () => {
let firstPost; let firstPost;
@@ -33,7 +34,10 @@ describe('Verify unread toast appears after repeated manual marking post as unre
cy.visit(`/${team.name}/channels/${testChannel.name}`); cy.visit(`/${team.name}/channels/${testChannel.name}`);
switchToChannel(offTopicChannel); switchToChannel(offTopicChannel);
cy.postMessageAs({ // Ensure that the Off-Topic channel has loaded successfully, before posting messages in the other channel
cy.get('#channelHeaderTitle').should('be.visible').and('contain.text', offTopicChannel.display_name);
cy.wait(TIMEOUTS.ONE_SEC).postMessageAs({
sender: otherUser, sender: otherUser,
message: 'First message', message: 'First message',
channelId: testChannel.id, channelId: testChannel.id,

Просмотреть файл

@@ -21,6 +21,9 @@ describe('Message Reply', () => {
cy.apiInitSetup({loginAfter: true}).then(({team, channel}) => { cy.apiInitSetup({loginAfter: true}).then(({team, channel}) => {
newChannel = channel; newChannel = channel;
cy.visit(`/${team.name}/channels/${channel.name}`); cy.visit(`/${team.name}/channels/${channel.name}`);
// # Wait for the page to fully load before continuing
cy.get('#sidebar-header-container').should('be.visible').and('have.text', team.display_name);
}); });
}); });
@@ -43,9 +46,11 @@ describe('Message Reply', () => {
cy.clickPostCommentIcon(postId); cy.clickPostCommentIcon(postId);
// # Reply with the attachment // # Reply with the attachment
cy.postMessageReplyInRHS('A reply to an older post with attachment'); const replyText = 'A reply to an older post with attachment';
cy.postMessageReplyInRHS(replyText);
// # Get the latest reply post // # Get the latest reply post
cy.uiWaitUntilMessagePostedIncludes(replyText);
cy.getLastPostId().then((replyId) => { cy.getLastPostId().then((replyId) => {
// * Verify that the reply is in the channel view with matching text // * Verify that the reply is in the channel view with matching text
cy.get(`#post_${replyId}`).within(() => { cy.get(`#post_${replyId}`).within(() => {

Просмотреть файл

@@ -41,6 +41,10 @@ describe('Notifications', () => {
// # Login as receiver and visit off-topic channel // # Login as receiver and visit off-topic channel
cy.apiLogin(receiver); cy.apiLogin(receiver);
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`); cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
// # Wait for the page to fully load before continuing
cy.get('#channelHeaderDropdownButton').should('be.visible').and('have.text', testChannel.display_name);
cy.get(`#sidebarItem_${otherChannel.name}`).click(); cy.get(`#sidebarItem_${otherChannel.name}`).click();
cy.get('#sidebarItem_off-topic').click(); cy.get('#sidebarItem_off-topic').click();
}); });
@@ -284,4 +288,5 @@ function setNotificationSettings(desiredSettings = {first: true, username: true,
// # Navigate to a channel we are NOT going to post to // # Navigate to a channel we are NOT going to post to
cy.get(`#sidebarItem_${channelName}`).scrollIntoView().click({force: true}); cy.get(`#sidebarItem_${channelName}`).scrollIntoView().click({force: true});
cy.get('#loadingSpinner').should('not.exist');
} }

Просмотреть файл

@@ -11,6 +11,7 @@
// Group: @channels @not_cloud @system_console // Group: @channels @not_cloud @system_console
import {fileSizeToString} from '../../../utils'; import {fileSizeToString} from '../../../utils';
import * as TIMEOUTS from '../../../fixtures/timeouts';
describe('Unsaved Changes', () => { describe('Unsaved Changes', () => {
let defaultMaxFileSize; let defaultMaxFileSize;
@@ -26,7 +27,7 @@ describe('Unsaved Changes', () => {
it('MM-T955 Warning leaving changed page without saving: Leave page, discard changes', () => { it('MM-T955 Warning leaving changed page without saving: Leave page, discard changes', () => {
// # Make a change on any page. // # Make a change on any page.
cy.visit('/admin_console/environment/file_storage'); cy.visit('/admin_console/environment/file_storage');
cy.findByTestId('FileSettings.MaxFileSizenumber').clear().type('150'); cy.findByTestId('FileSettings.MaxFileSizenumber', {timeout: TIMEOUTS.FIVE_SEC}).clear().type('150');
// # Click a navigation item in left nav. // # Click a navigation item in left nav.
cy.findByText('Database').click(); cy.findByText('Database').click();
@@ -38,7 +39,7 @@ describe('Unsaved Changes', () => {
// * Opens other page, changes discarded. // * Opens other page, changes discarded.
cy.url().should('include', '/environment/database'); cy.url().should('include', '/environment/database');
cy.visit('/admin_console/environment/file_storage'); cy.visit('/admin_console/environment/file_storage');
cy.findByTestId('FileSettings.MaxFileSizenumber').should('have.value', fileSizeToString(defaultMaxFileSize).replace('MB', '')); cy.findByTestId('FileSettings.MaxFileSizenumber', {timeout: TIMEOUTS.FIVE_SEC}).should('have.value', fileSizeToString(defaultMaxFileSize).replace('MB', ''));
}); });
it('MM-T956 Warning leaving changed page without saving: Cancel leaving page', () => { it('MM-T956 Warning leaving changed page without saving: Cancel leaving page', () => {
@@ -46,7 +47,7 @@ describe('Unsaved Changes', () => {
// # Make a change on any page. // # Make a change on any page.
cy.visit('/admin_console/environment/file_storage'); cy.visit('/admin_console/environment/file_storage');
cy.findByTestId('FileSettings.MaxFileSizenumber').clear().type(draftValue); cy.findByTestId('FileSettings.MaxFileSizenumber', {timeout: TIMEOUTS.FIVE_SEC}).clear().type(draftValue);
// # Click a navigation item in left nav. // # Click a navigation item in left nav.
cy.findByText('Database').click(); cy.findByText('Database').click();
@@ -57,7 +58,7 @@ describe('Unsaved Changes', () => {
// * Stays on current page, changes kept. // * Stays on current page, changes kept.
cy.url().should('include', '/environment/file_storage'); cy.url().should('include', '/environment/file_storage');
cy.findByTestId('FileSettings.MaxFileSizenumber').should('have.value', draftValue); cy.findByTestId('FileSettings.MaxFileSizenumber', {timeout: TIMEOUTS.FIVE_SEC}).should('have.value', draftValue);
}); });
}); });

Просмотреть файл

@@ -68,6 +68,7 @@ describe('System Console > User Management > Users', () => {
it('MM-T932 Users - Change a user\'s password', () => { it('MM-T932 Users - Change a user\'s password', () => {
// # Search for the user. // # Search for the user.
cy.findByPlaceholderText('Search users').type(testUser.email).wait(TIMEOUTS.HALF_SEC); cy.findByPlaceholderText('Search users').type(testUser.email).wait(TIMEOUTS.HALF_SEC);
cy.get('#systemUsersTable-cell-0_emailColumn').should('contain', testUser.email);
// # Open the actions menu. // # Open the actions menu.
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC); cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);

Просмотреть файл

@@ -156,6 +156,7 @@ describe('User Management', () => {
// # Search for the user. // # Search for the user.
cy.get('#input_searchTerm').clear().type(user.email, {delay: TIMEOUTS.ONE_HUNDRED_MILLIS}).wait(TIMEOUTS.HALF_SEC); cy.get('#input_searchTerm').clear().type(user.email, {delay: TIMEOUTS.ONE_HUNDRED_MILLIS}).wait(TIMEOUTS.HALF_SEC);
cy.get('#systemUsersTable-cell-0_emailColumn').should('contain', user.email);
cy.get('#actionMenuButton-systemUsersTable-0').click().wait(TIMEOUTS.HALF_SEC); cy.get('#actionMenuButton-systemUsersTable-0').click().wait(TIMEOUTS.HALF_SEC);

Просмотреть файл

@@ -185,6 +185,7 @@ describe('User Management', () => {
// # Search for the user. // # Search for the user.
cy.get('#input_searchTerm').clear().type(gitlabUser.email).wait(TIMEOUTS.HALF_SEC); cy.get('#input_searchTerm').clear().type(gitlabUser.email).wait(TIMEOUTS.HALF_SEC);
cy.get('#systemUsersTable-cell-0_emailColumn').should('contain', gitlabUser.email);
// # Open actions menu. // # Open actions menu.
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC); cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
@@ -255,6 +256,7 @@ describe('User Management', () => {
// # Search for the user. // # Search for the user.
cy.get('#input_searchTerm').clear().type(oldEmail).wait(TIMEOUTS.HALF_SEC); cy.get('#input_searchTerm').clear().type(oldEmail).wait(TIMEOUTS.HALF_SEC);
cy.get('#systemUsersTable-cell-0_emailColumn').should('contain', oldEmail);
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC); cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
cy.findByText('Update email').click().wait(TIMEOUTS.HALF_SEC); cy.findByText('Update email').click().wait(TIMEOUTS.HALF_SEC);

Просмотреть файл

@@ -38,6 +38,7 @@ describe('unread_with_bottom_start_toast', () => {
cy.apiCreateChannel(testTeam.id, 'channel-a', 'ChannelA').then(({channel}) => { cy.apiCreateChannel(testTeam.id, 'channel-a', 'ChannelA').then(({channel}) => {
cy.apiAddUserToChannel(channel.id, otherUser.id).then(() => { cy.apiAddUserToChannel(channel.id, otherUser.id).then(() => {
cy.visit(`/${testTeam.name}/channels/${channel.name}`); cy.visit(`/${testTeam.name}/channels/${channel.name}`);
cy.get('#loadingSpinner').should('not.exist'); // The channel should fully load once, before it can mark messages as "new"
cy.uiClickSidebarItem('off-topic'); cy.uiClickSidebarItem('off-topic');
cy.postMessage('hi'); cy.postMessage('hi');
@@ -73,6 +74,7 @@ describe('unread_with_bottom_start_toast', () => {
cy.apiCreateChannel(testTeam.id, 'channel-b', 'ChannelB').then(({channel}) => { cy.apiCreateChannel(testTeam.id, 'channel-b', 'ChannelB').then(({channel}) => {
cy.apiAddUserToChannel(channel.id, otherUser.id).then(() => { cy.apiAddUserToChannel(channel.id, otherUser.id).then(() => {
cy.visit(`/${testTeam.name}/channels/${channel.name}`); cy.visit(`/${testTeam.name}/channels/${channel.name}`);
cy.get('#loadingSpinner').should('not.exist'); // The channel should fully load once, before it can mark messages as "new"
cy.uiClickSidebarItem('off-topic'); cy.uiClickSidebarItem('off-topic');
cy.postMessage('hi'); cy.postMessage('hi');
@@ -114,6 +116,10 @@ describe('unread_with_bottom_start_toast', () => {
cy.postMessageAs({sender: otherUser, message: `test message ${index}`, channelId: channel.id}).wait(50); cy.postMessageAs({sender: otherUser, message: `test message ${index}`, channelId: channel.id}).wait(50);
} }
// # Switch channel and back
cy.uiClickSidebarItem('off-topic');
cy.uiClickSidebarItem(channel.name);
cy.wait(TIMEOUTS.ONE_SEC); cy.wait(TIMEOUTS.ONE_SEC);
// # Scroll to the top to find the oldest message // # Scroll to the top to find the oldest message
@@ -123,6 +129,7 @@ describe('unread_with_bottom_start_toast', () => {
// # Mark post as unread // # Mark post as unread
cy.uiClickPostDropdownMenu(postId, 'Mark as Unread'); cy.uiClickPostDropdownMenu(postId, 'Mark as Unread');
}); });
cy.get('div.toast').should('be.visible').contains('30 new messages'); // The toast message should appear right away
// # Visit off-topic channel and switch back to test channel // # Visit off-topic channel and switch back to test channel
cy.uiClickSidebarItem('off-topic'); cy.uiClickSidebarItem('off-topic');

Просмотреть файл

@@ -73,7 +73,7 @@ describe('channels > rhs > status update', {testIsolation: true}, () => {
describe('post update dialog', () => { describe('post update dialog', () => {
it('renders description correctly', () => { it('renders description correctly', () => {
// # Run the `/playbook update` slash command. // # Run the `/playbook update` slash command.
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// # Get dialog modal. // # Get dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {
@@ -84,7 +84,7 @@ describe('channels > rhs > status update', {testIsolation: true}, () => {
it('description link navigates to run overview', () => { it('description link navigates to run overview', () => {
// # Run the `/playbook update` slash command. // # Run the `/playbook update` slash command.
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// # Get dialog modal. // # Get dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {
@@ -101,7 +101,7 @@ describe('channels > rhs > status update', {testIsolation: true}, () => {
it('prevents posting an update message with only whitespace', () => { it('prevents posting an update message with only whitespace', () => {
// # Run the `/playbook update` slash command. // # Run the `/playbook update` slash command.
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// # Get dialog modal. // # Get dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {
@@ -157,7 +157,7 @@ describe('channels > rhs > status update', {testIsolation: true}, () => {
cy.visit(`/${testTeam.name}/channels/${channelName}`); cy.visit(`/${testTeam.name}/channels/${channelName}`);
// # Run the `/playbook update` slash command. // # Run the `/playbook update` slash command.
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// # Get dialog modal. // # Get dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {
@@ -183,7 +183,7 @@ describe('channels > rhs > status update', {testIsolation: true}, () => {
const reminderTime = '1 day'; const reminderTime = '1 day';
// # Run the `/playbook update` slash command. // # Run the `/playbook update` slash command.
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// # Get the dialog modal. // # Get the dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {
@@ -249,7 +249,7 @@ describe('channels > rhs > status update', {testIsolation: true}, () => {
describe('prevents user from losing changes', () => { describe('prevents user from losing changes', () => {
it('cancel, go back and save', () => { it('cancel, go back and save', () => {
// # Run the `/playbook update` slash command. // # Run the `/playbook update` slash command.
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// # Get dialog modal. // # Get dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {
@@ -279,7 +279,7 @@ describe('channels > rhs > status update', {testIsolation: true}, () => {
it('click overview link, go back and save', () => { it('click overview link, go back and save', () => {
// # Run the `/playbook update` slash command. // # Run the `/playbook update` slash command.
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// # Get dialog modal. // # Get dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {
@@ -310,7 +310,7 @@ describe('channels > rhs > status update', {testIsolation: true}, () => {
it('cancel and discard explicitly', () => { it('cancel and discard explicitly', () => {
// # Run the `/playbook update` slash command. // # Run the `/playbook update` slash command.
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// # Get dialog modal. // # Get dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {
@@ -333,7 +333,7 @@ describe('channels > rhs > status update', {testIsolation: true}, () => {
it('click overview link and discard explicitly', () => { it('click overview link and discard explicitly', () => {
// # Run the `/playbook update` slash command. // # Run the `/playbook update` slash command.
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// # Get dialog modal. // # Get dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {
@@ -364,7 +364,7 @@ describe('channels > rhs > status update', {testIsolation: true}, () => {
describe('shows the last update in update message', () => { describe('shows the last update in update message', () => {
it('shows the default when we have not made an update before', () => { it('shows the default when we have not made an update before', () => {
// # Run the `/playbook update` slash command. // # Run the `/playbook update` slash command.
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// # Get the dialog modal. // # Get the dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {
@@ -383,7 +383,7 @@ describe('channels > rhs > status update', {testIsolation: true}, () => {
cy.updateStatus(firstMessage); cy.updateStatus(firstMessage);
// # Run the `/playbook update` slash command. // # Run the `/playbook update` slash command.
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// # Get the dialog modal. // # Get the dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {
@@ -399,7 +399,7 @@ describe('channels > rhs > status update', {testIsolation: true}, () => {
describe('the default reminder', () => { describe('the default reminder', () => {
it('shows the configured default when we have not made a previous update', () => { it('shows the configured default when we have not made a previous update', () => {
// # Run the `/playbook update` slash command. // # Run the `/playbook update` slash command.
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// # Get the dialog modal. // # Get the dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {
@@ -418,7 +418,7 @@ describe('channels > rhs > status update', {testIsolation: true}, () => {
cy.updateStatus(firstMessage, '15 minutes'); cy.updateStatus(firstMessage, '15 minutes');
// # Run the `/playbook update` slash command. // # Run the `/playbook update` slash command.
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// # Get the dialog modal. // # Get the dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {
@@ -437,7 +437,7 @@ describe('channels > rhs > status update', {testIsolation: true}, () => {
cy.updateStatus(firstMessage, '90 minutes'); cy.updateStatus(firstMessage, '90 minutes');
// # Run the `/playbook update` slash command. // # Run the `/playbook update` slash command.
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// # Get the dialog modal. // # Get the dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {
@@ -456,7 +456,7 @@ describe('channels > rhs > status update', {testIsolation: true}, () => {
cy.updateStatus(firstMessage, '7 days'); cy.updateStatus(firstMessage, '7 days');
// # Run the `/playbook update` slash command. // # Run the `/playbook update` slash command.
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// # Get the dialog modal. // # Get the dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {

Просмотреть файл

@@ -90,7 +90,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
cy.findByTestId('post_textbox').clear(); cy.findByTestId('post_textbox').clear();
// # Run a slash command with correct parameters // # Run a slash command with correct parameters
cy.uiPostMessageQuickly('/playbook check 1 1'); cy.uiPostMessageQuickly('/playbook check 1 1 ');
// * Verify the task is checked // * Verify the task is checked
cy.get('[data-rbd-droppable-id="1"]').find('.checkbox').eq(1).should('be.checked'); cy.get('[data-rbd-droppable-id="1"]').find('.checkbox').eq(1).should('be.checked');
@@ -98,7 +98,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
it('check add', () => { it('check add', () => {
// # Run a slash command with correct parameters // # Run a slash command with correct parameters
cy.uiPostMessageQuickly('/playbook checkadd 1 new-task'); cy.uiPostMessageQuickly('/playbook checkadd 1 new-task ');
// * Verify the task was added // * Verify the task was added
cy.get('[data-rbd-droppable-id="1"]').contains('new-task'); cy.get('[data-rbd-droppable-id="1"]').contains('new-task');
@@ -106,7 +106,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
it('check remove', () => { it('check remove', () => {
// # Run a slash command with correct parameters // # Run a slash command with correct parameters
cy.uiPostMessageQuickly('/playbook checkremove 1 1'); cy.uiPostMessageQuickly('/playbook checkremove 1 1 ');
// * Verify the task was added // * Verify the task was added
cy.get('[data-rbd-droppable-id="1"]').contains('Step 2').should('not.exist'); cy.get('[data-rbd-droppable-id="1"]').contains('Step 2').should('not.exist');
@@ -114,7 +114,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
it('owner', () => { it('owner', () => {
// # Run a slash command // # Run a slash command
cy.uiPostMessageQuickly('/playbook owner'); cy.uiPostMessageQuickly('/playbook owner ');
// * Verify the message. // * Verify the message.
cy.verifyEphemeralMessage(`@${testUser.username} is the current owner for this playbook run.`); cy.verifyEphemeralMessage(`@${testUser.username} is the current owner for this playbook run.`);
@@ -123,13 +123,13 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
cy.uiPostMessageQuickly(`/playbook owner @${testUser2.username}`); cy.uiPostMessageQuickly(`/playbook owner @${testUser2.username}`);
// * Verify that the owner was set. // * Verify that the owner was set.
cy.uiPostMessageQuickly('/playbook owner'); cy.uiPostMessageQuickly('/playbook owner ');
cy.verifyEphemeralMessage(`@${testUser2.username} is the current owner for this playbook run.`); cy.verifyEphemeralMessage(`@${testUser2.username} is the current owner for this playbook run.`);
}); });
it('timeline', () => { it('timeline', () => {
// # Run a slash command on a run with view access // # Run a slash command on a run with view access
cy.uiPostMessageQuickly('/playbook timeline'); cy.uiPostMessageQuickly('/playbook timeline ');
// * Verify the message. // * Verify the message.
cy.verifyEphemeralMessage(`Timeline for ${playbookRunName}`); cy.verifyEphemeralMessage(`Timeline for ${playbookRunName}`);
@@ -137,7 +137,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
it('finish', () => { it('finish', () => {
// # Run a slash command with correct parameters // # Run a slash command with correct parameters
cy.uiPostMessageQuickly('/playbook finish'); cy.uiPostMessageQuickly('/playbook finish ');
// * Verify confirm modal is visible. // * Verify confirm modal is visible.
cy.get('#interactiveDialogModalLabel').should('exist'); cy.get('#interactiveDialogModalLabel').should('exist');
@@ -274,19 +274,19 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
it('check', () => { it('check', () => {
// # Run a slash command with not enough parameters // # Run a slash command with not enough parameters
cy.uiPostMessageQuickly('/playbook check 1 1'); cy.uiPostMessageQuickly('/playbook check 1 1 ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Command expects three arguments: the run number, the checklist number and the item number.'); cy.verifyEphemeralMessage('Command expects three arguments: the run number, the checklist number and the item number.');
// # Run a slash command wrong run number // # Run a slash command wrong run number
cy.uiPostMessageQuickly('/playbook check 2 1 1'); cy.uiPostMessageQuickly('/playbook check 2 1 1 ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Invalid run number'); cy.verifyEphemeralMessage('Invalid run number');
// # Run a slash command on a run with view access // # Run a slash command on a run with view access
cy.uiPostMessageQuickly('/playbook check 0 1 1'); cy.uiPostMessageQuickly('/playbook check 0 1 1 ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Become a participant to interact with this run'); cy.verifyEphemeralMessage('Become a participant to interact with this run');
@@ -301,7 +301,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
cy.findByTestId('post_textbox').clear(); cy.findByTestId('post_textbox').clear();
// # Run a slash command with correct parameters // # Run a slash command with correct parameters
cy.uiPostMessageQuickly('/playbook check 1 1 1'); cy.uiPostMessageQuickly('/playbook check 1 1 1 ');
cy.get('#rhsContainer').within(() => { cy.get('#rhsContainer').within(() => {
// * Verify number of runs // * Verify number of runs
cy.get('[data-testid="run-list-card"]').should('have.length', 2); cy.get('[data-testid="run-list-card"]').should('have.length', 2);
@@ -316,19 +316,19 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
it('check add', () => { it('check add', () => {
// # Run a slash command with not enough parameters // # Run a slash command with not enough parameters
cy.uiPostMessageQuickly('/playbook checkadd 1'); cy.uiPostMessageQuickly('/playbook checkadd 1 ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Command expects two arguments: the run number and the checklist number.'); cy.verifyEphemeralMessage('Command expects two arguments: the run number and the checklist number.');
// # Run a slash command wrong run number // # Run a slash command wrong run number
cy.uiPostMessageQuickly('/playbook checkadd 2 1 1'); cy.uiPostMessageQuickly('/playbook checkadd 2 1 1 ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Invalid run number'); cy.verifyEphemeralMessage('Invalid run number');
// # Run a slash command on a run with view access // # Run a slash command on a run with view access
cy.uiPostMessageQuickly('/playbook checkadd 0 1 new-task'); cy.uiPostMessageQuickly('/playbook checkadd 0 1 new-task ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Become a participant to interact with this run'); cy.verifyEphemeralMessage('Become a participant to interact with this run');
@@ -343,7 +343,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
cy.findByTestId('post_textbox').clear(); cy.findByTestId('post_textbox').clear();
// # Run a slash command with correct parameters // # Run a slash command with correct parameters
cy.uiPostMessageQuickly('/playbook checkadd 1 1 new-task'); cy.uiPostMessageQuickly('/playbook checkadd 1 1 new-task ');
cy.get('#rhsContainer').within(() => { cy.get('#rhsContainer').within(() => {
// * Verify number of runs // * Verify number of runs
@@ -359,19 +359,19 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
it('check remove', () => { it('check remove', () => {
// # Run a slash command with not enough parameters // # Run a slash command with not enough parameters
cy.uiPostMessageQuickly('/playbook checkremove 1 1'); cy.uiPostMessageQuickly('/playbook checkremove 1 1 ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Command expects three arguments: the run number, the checklist number and the item number.'); cy.verifyEphemeralMessage('Command expects three arguments: the run number, the checklist number and the item number.');
// # Run a slash command wrong run number // # Run a slash command wrong run number
cy.uiPostMessageQuickly('/playbook checkremove 2 0 1'); cy.uiPostMessageQuickly('/playbook checkremove 2 0 1 ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Invalid run number'); cy.verifyEphemeralMessage('Invalid run number');
// # Run a slash command on a run with view access // # Run a slash command on a run with view access
cy.uiPostMessageQuickly('/playbook checkremove 0 1 0'); cy.uiPostMessageQuickly('/playbook checkremove 0 1 0 ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Become a participant to interact with this run'); cy.verifyEphemeralMessage('Become a participant to interact with this run');
@@ -386,7 +386,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
cy.findByTestId('post_textbox').clear(); cy.findByTestId('post_textbox').clear();
// # Run a slash command with correct parameters // # Run a slash command with correct parameters
cy.uiPostMessageQuickly('/playbook checkremove 1 1 1'); cy.uiPostMessageQuickly('/playbook checkremove 1 1 1 ');
cy.get('#rhsContainer').within(() => { cy.get('#rhsContainer').within(() => {
// * Verify number of runs // * Verify number of runs
@@ -402,19 +402,19 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
it('owner', () => { it('owner', () => {
// # Run a slash command with not enough parameters // # Run a slash command with not enough parameters
cy.uiPostMessageQuickly('/playbook owner'); cy.uiPostMessageQuickly('/playbook owner ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('/playbook owner expects at most one argument.'); cy.verifyEphemeralMessage('/playbook owner expects at most one argument.');
// # Run a slash command wrong run number // # Run a slash command wrong run number
cy.uiPostMessageQuickly('/playbook owner 2'); cy.uiPostMessageQuickly('/playbook owner 2 ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Invalid run number'); cy.verifyEphemeralMessage('Invalid run number');
// # Run a slash command on a run with view access // # Run a slash command on a run with view access
cy.uiPostMessageQuickly('/playbook owner 0'); cy.uiPostMessageQuickly('/playbook owner 0 ');
// * Verify the message. // * Verify the message.
cy.verifyEphemeralMessage(`@${testUser.username} is the current owner for this playbook run.`); cy.verifyEphemeralMessage(`@${testUser.username} is the current owner for this playbook run.`);
@@ -429,34 +429,34 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
cy.findByTestId('post_textbox').clear(); cy.findByTestId('post_textbox').clear();
// # Run a slash command on a run with view access // # Run a slash command on a run with view access
cy.uiPostMessageQuickly(`/playbook owner 0 @${testUser2.username}`); cy.uiPostMessageQuickly(`/playbook owner 0 @${testUser2.username} `);
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Become a participant to interact with this run'); cy.verifyEphemeralMessage('Become a participant to interact with this run');
// # Run a slash command on a run with write access // # Run a slash command on a run with write access
cy.uiPostMessageQuickly(`/playbook owner 1 @${testUser2.username}`); cy.uiPostMessageQuickly(`/playbook owner 1 @${testUser2.username} `);
// * Verify that the owner was set. // * Verify that the owner was set.
cy.uiPostMessageQuickly('/playbook owner 1'); cy.uiPostMessageQuickly('/playbook owner 1 ');
cy.verifyEphemeralMessage(`@${testUser2.username} is the current owner for this playbook run.`); cy.verifyEphemeralMessage(`@${testUser2.username} is the current owner for this playbook run.`);
}); });
it('finish', () => { it('finish', () => {
// # Run a slash command with not enough parameters // # Run a slash command with not enough parameters
cy.uiPostMessageQuickly('/playbook finish'); cy.uiPostMessageQuickly('/playbook finish ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Command expects one argument: the run number.'); cy.verifyEphemeralMessage('Command expects one argument: the run number.');
// # Run a slash command wrong run number // # Run a slash command wrong run number
cy.uiPostMessageQuickly('/playbook finish 2'); cy.uiPostMessageQuickly('/playbook finish 2 ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Invalid run number'); cy.verifyEphemeralMessage('Invalid run number');
// # Run a slash command on a run with view access // # Run a slash command on a run with view access
cy.uiPostMessageQuickly('/playbook finish 0'); cy.uiPostMessageQuickly('/playbook finish 0 ');
// * Verify the message. // * Verify the message.
cy.verifyEphemeralMessage(`userID ${testUser2.id} is not an admin or channel member`); cy.verifyEphemeralMessage(`userID ${testUser2.id} is not an admin or channel member`);
@@ -479,7 +479,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
}); });
// # Run a slash command with correct parameters // # Run a slash command with correct parameters
cy.uiPostMessageQuickly('/playbook finish 1'); cy.uiPostMessageQuickly('/playbook finish 1 ');
// * Verify confirm modal is visible. // * Verify confirm modal is visible.
cy.get('#interactiveDialogModalLabel').should('exist'); cy.get('#interactiveDialogModalLabel').should('exist');
@@ -493,19 +493,19 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
it('timeline', () => { it('timeline', () => {
// # Run a slash command with not enough parameters // # Run a slash command with not enough parameters
cy.uiPostMessageQuickly('/playbook timeline'); cy.uiPostMessageQuickly('/playbook timeline ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Command expects one argument: the run number.'); cy.verifyEphemeralMessage('Command expects one argument: the run number.');
// # Run a slash command wrong run number // # Run a slash command wrong run number
cy.uiPostMessageQuickly('/playbook timeline 2'); cy.uiPostMessageQuickly('/playbook timeline 2 ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Invalid run number'); cy.verifyEphemeralMessage('Invalid run number');
// # Run a slash command on a run with view access // # Run a slash command on a run with view access
cy.uiPostMessageQuickly('/playbook timeline 0'); cy.uiPostMessageQuickly('/playbook timeline 0 ');
// * Verify the message. // * Verify the message.
cy.verifyEphemeralMessage(`Timeline for ${playbookRuns[1].name}`); cy.verifyEphemeralMessage(`Timeline for ${playbookRuns[1].name}`);
@@ -513,13 +513,13 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
it('update', () => { it('update', () => {
// # Run a slash command with not enough parameters // # Run a slash command with not enough parameters
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Command expects one argument: the run number.'); cy.verifyEphemeralMessage('Command expects one argument: the run number.');
// # Run a slash command wrong run number // # Run a slash command wrong run number
cy.uiPostMessageQuickly('/playbook update 2'); cy.uiPostMessageQuickly('/playbook update 2 ');
// * Verify the expected error message. // * Verify the expected error message.
cy.verifyEphemeralMessage('Invalid run number'); cy.verifyEphemeralMessage('Invalid run number');
@@ -534,7 +534,7 @@ describe('channels > slash command > owner', {testIsolation: true}, () => {
cy.findByTestId('post_textbox').clear(); cy.findByTestId('post_textbox').clear();
// # Run a slash command with correct parameters // # Run a slash command with correct parameters
cy.uiPostMessageQuickly('/playbook update 1'); cy.uiPostMessageQuickly('/playbook update 1 ');
// # Get dialog modal. // # Get dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {

Просмотреть файл

@@ -85,6 +85,7 @@ describe('channels > slash command > test', {testIsolation: true}, () => {
EnableTesting: true, EnableTesting: true,
}, },
}); });
cy.apiReloadConfig();
}); });
beforeEach(() => { beforeEach(() => {
@@ -132,6 +133,7 @@ describe('channels > slash command > test', {testIsolation: true}, () => {
EnableTesting: false, EnableTesting: false,
}, },
}); });
cy.apiReloadConfig();
}); });
beforeEach(() => { beforeEach(() => {
@@ -178,6 +180,7 @@ describe('channels > slash command > test', {testIsolation: true}, () => {
EnableTesting: true, EnableTesting: true,
}, },
}); });
cy.apiReloadConfig();
}); });
beforeEach(() => { beforeEach(() => {

Просмотреть файл

@@ -175,6 +175,7 @@ describe('playbooks > list', {testIsolation: true}, () => {
// # Drop loaded fixture onto playbook list // # Drop loaded fixture onto playbook list
cy.findByTestId('playbook-list-scroll-container').selectFile(validPlaybookExport, { cy.findByTestId('playbook-list-scroll-container').selectFile(validPlaybookExport, {
action: 'drag-drop', action: 'drag-drop',
force: true,
}); });
// * Verify that a new playbook was created. // * Verify that a new playbook was created.

Просмотреть файл

@@ -3,6 +3,8 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
const fs = require('fs');
const clientRequest = require('./client_request'); const clientRequest = require('./client_request');
const { const {
dbGetActiveUserSessions, dbGetActiveUserSessions,
@@ -72,5 +74,19 @@ module.exports = (on, config) => {
return launchOptions; return launchOptions;
}); });
// https://docs.cypress.io/guides/guides/screenshots-and-videos#Delete-videos-for-specs-without-failing-or-retried-tests
on('after:spec', (spec, results) => {
if (results && results.video) {
// Do we have failures for any retry attempts?
const failures = results.tests.some((test) =>
test.attempts.some((attempt) => attempt.state === 'failed'),
);
if (!failures) {
// delete the video if the spec passed and no tests retried
fs.unlinkSync(results.video);
}
}
});
return config; return config;
}; };

Просмотреть файл

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import * as TIMEOUTS from '../../fixtures/timeouts'; import * as TIMEOUTS from '../../fixtures/timeouts';
const playbookRunStartCommand = '/playbook run'; const playbookRunStartCommand = '/playbook run ';
Cypress.Commands.add('startPlaybookRun', (playbookName, playbookRunName) => { Cypress.Commands.add('startPlaybookRun', (playbookName, playbookRunName) => {
cy.get('#interactiveDialogModal').should('exist').within(() => { cy.get('#interactiveDialogModal').should('exist').within(() => {
@@ -167,7 +167,7 @@ Cypress.Commands.add('selectReminderTime', (timeText) => {
*/ */
Cypress.Commands.add('updateStatus', (message, reminderQuery) => { Cypress.Commands.add('updateStatus', (message, reminderQuery) => {
// # Run the slash command to update status. // # Run the slash command to update status.
cy.uiPostMessageQuickly('/playbook update'); cy.uiPostMessageQuickly('/playbook update ');
// # Get the interactive dialog modal. // # Get the interactive dialog modal.
cy.getStatusUpdateDialog().within(() => { cy.getStatusUpdateDialog().within(() => {

5
server/build/docker/keycloak/kc-healthcheck.sh Исполняемый файл
Просмотреть файл

@@ -0,0 +1,5 @@
#!/bin/bash
set -eu -o pipefail
/opt/keycloak/bin/kcadm.sh config credentials -x --server http://localhost:8080 --realm master --user "$KEYCLOAK_ADMIN" --password "$KEYCLOAK_ADMIN_PASSWORD"
/opt/keycloak/bin/kcadm.sh get realms/mattermost >/dev/null

Просмотреть файл

@@ -61,18 +61,22 @@ services:
timeout: 15s timeout: 15s
retries: 12 retries: 12
keycloak: keycloak:
image: mattermostdevelopment/mirrored-keycloak:10.0.2 image: quay.io/keycloak/keycloak:23.0.7
restart: always restart: always
entrypoint: /opt/keycloak/bin/kc.sh start --import-realm
environment: environment:
KEYCLOAK_USER: mmuser KEYCLOAK_ADMIN: admin
KEYCLOAK_PASSWORD: mostest KEYCLOAK_ADMIN_PASSWORD: admin
DB_VENDOR: h2 KC_HOSTNAME_STRICT: 'false'
KEYCLOAK_IMPORT: /setup/realm.json KC_HOSTNAME_STRICT_HTTPS: 'false'
KC_HTTP_ENABLED: 'true'
JAVA_OPTS: "-Xms64m -Xmx2G -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true" JAVA_OPTS: "-Xms64m -Xmx2G -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true"
volumes: volumes:
- "./docker/keycloak:/setup" - "./docker/keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json"
- "./docker/keycloak/kc-healthcheck.sh:/usr/local/bin/kc-healthcheck.sh"
healthcheck: healthcheck:
test: [ "CMD", "bash", "-o", "pipefail", "-c", "curl --silent localhost:9990/health | grep -q '\"status\":\"UP\"'" ] # We can't use a simple 'curl --silent localhost:9990/health | grep -q \"status\":\"UP\"' because there's no curl in the image: https://www.keycloak.org/server/health#_using_the_health_checks
test: [ "CMD", "/usr/local/bin/kc-healthcheck.sh" ]
interval: 10s interval: 10s
timeout: 15s timeout: 15s
retries: 12 retries: 12