Migrate QMS tests (#6063)

This commit is contained in:
Alexey Zinoviev 2024-07-13 19:23:11 +04:00 committed by GitHub
parent ce8683e8e0
commit d0785ffdd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
93 changed files with 3461 additions and 0 deletions

View File

@ -33,6 +33,7 @@ env:
server-plugins
templates
tests
qms-tests
rush.json
.prettierrc
tools
@ -282,6 +283,99 @@ jobs:
# with:
# name: db-snapshot
# path: ./tests/db_dump
uitest-qms:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
filter: tree:0
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Cache node modules
uses: actions/cache@v4
env:
cache-name: cache-node-platform
with:
path: |
common/temp
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Checking for mis-matching dependencies...
run: node common/scripts/install-run-rush.js check
- name: Installing...
run: node common/scripts/install-run-rush.js install
- name: Docker Build
run: node common/scripts/install-run-rush.js docker:build -p 20
env:
DOCKER_CLI_HINTS: false
- name: Prepare server
run: |
cd ./qms-tests
./prepare.sh
- name: Install Playwright
run: |
cd ./qms-tests/sanity
node ../../common/scripts/install-run-rushx.js ci
- name: Run UI tests
run: |
cd ./qms-tests/sanity
node ../../common/scripts/install-run-rushx.js uitest
- name: "Store docker logs"
if: always()
run: |
cd ./qms-tests/sanity
mkdir logs
docker logs $(docker ps | grep transactor | cut -f 1 -d ' ') > logs/transactor.log
docker logs $(docker ps | grep account | cut -f 1 -d ' ') > logs/account.log
docker logs $(docker ps | grep front | cut -f 1 -d ' ') > logs/front.log
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-results-qms
path: ./qms-tests/sanity/playwright-report/
# - name: Get Allure history
# uses: actions/checkout@v4
# if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
# continue-on-error: true
# with:
# ref: gh-pages
# path: gh-pages
# - name: Generates Allure Report
# uses: simple-elf/allure-report-action@master
# if: always()
# id: allure-report
# with:
# allure_results: ./qms-tests/sanity/allure-results/
# gh_pages: gh-pages
# allure_report: allure-report
# allure_history: allure-history
# - name: Upload allure test results
# if: always()
# uses: actions/upload-artifact@v4
# with:
# name: allure-report-qms
# path: ./allure-report/
# - name: Deploy report to Github Pages
# if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
# uses: peaceiris/actions-gh-pages@v4
# with:
# PERSONAL_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# PUBLISH_BRANCH: gh-pages
# PUBLISH_DIR: allure-history
- name: Upload Logs
if: always()
uses: actions/upload-artifact@v4
with:
name: docker-logs-qms
path: ./qms-tests/sanity/logs
uitest-uweb:
runs-on: ubuntu-latest
timeout-minutes: 60

View File

@ -539,6 +539,9 @@ dependencies:
'@rush-temp/products-resources':
specifier: file:./projects/products-resources.tgz
version: file:projects/products-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2)
'@rush-temp/qms-tests-sanity':
specifier: file:./projects/qms-tests-sanity.tgz
version: file:projects/qms-tests-sanity.tgz
'@rush-temp/query':
specifier: file:./projects/query.tgz
version: file:projects/query.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2)
@ -23274,6 +23277,30 @@ packages:
- ts-node
dev: false
file:projects/qms-tests-sanity.tgz:
resolution: {integrity: sha512-ntTsOcgNEYuQVnuTv8IDdeGHDmCqEJpcQijo2H/OJJlOfHBj52ijRQCFB8Jnhv6L2RLEV9euSHAk+VGxvWsqyw==, tarball: file:projects/qms-tests-sanity.tgz}
name: '@rush-temp/qms-tests-sanity'
version: 0.0.0
dependencies:
'@playwright/test': 1.41.2
'@types/jest': 29.5.12
'@types/node': 20.11.19
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
allure-playwright: 2.12.2
cross-env: 7.0.3
dotenv: 16.0.3
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
prettier: 3.2.5
typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: false
file:projects/query.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
resolution: {integrity: sha512-XIZxHSUO5njnO8+G1dIkH+RFiZ3F3xsCoSCSySE1Gy6QEGebjZUu5uT0YTFZxYe9bo3jceL7gMHPaFs3fWgMxA==, tarball: file:projects/query.tgz}
id: file:projects/query.tgz

View File

@ -206,6 +206,7 @@
placeholder={documents.string.DocumentCodePlaceholder}
disabled={loadingCodes}
bind:value={docObject.code}
id="doc-code"
kind="large-style"
/>
{#if codeNotUnique}

1
qms-tests/.env Normal file
View File

@ -0,0 +1 @@
STORAGE_CONFIG="minio|minio?accessKey=minioadmin&secretKey=minioadmin"

View File

@ -0,0 +1,88 @@
{
"localhost:8083": {
"key": "tracex",
"title": "TraceX",
"protocol": "http",
"languages": "en",
"defaultLanguage": "en",
"defaultApplication": "documents",
"defaultSpace": "documents:space:QualityDocuments",
"defaultSpecial": "",
"lastNameFirst": "true",
"initWorkspace": "init-ws-qms",
"links": [
{
"rel": "shortcut icon",
"type": "image/x-icon",
"href": "/tracex/favicon.ico"
},
{
"rel": "icon",
"type": "image/svg+xml",
"sizes": "any",
"href": "/tracex/favicon.svg"
},
{
"rel": "icon",
"type": "image/png",
"sizes": "16x16",
"href": "/tracex/favicon_16.png"
},
{
"rel": "icon",
"type": "image/png",
"sizes": "32x32",
"href": "/tracex/favicon_32.png"
},
{
"rel": "icon",
"type": "image/png",
"sizes": "192x192",
"href": "/tracex/favicon_192.png"
}
]
},
"localhost:8080": {
"key": "tracex-dev",
"title": "TraceX",
"protocol": "http",
"languages": "en",
"defaultLanguage": "en",
"defaultApplication": "documents",
"defaultSpace": "documents:space:QualityDocuments",
"defaultSpecial": "",
"lastNameFirst": "true",
"initWorkspace": "init-ws-qms",
"links": [
{
"rel": "shortcut icon",
"type": "image/x-icon",
"href": "/tracex/favicon.ico"
},
{
"rel": "icon",
"type": "image/svg+xml",
"sizes": "any",
"href": "/tracex/favicon.svg"
},
{
"rel": "icon",
"type": "image/png",
"sizes": "16x16",
"href": "/tracex/favicon_16.png"
},
{
"rel": "icon",
"type": "image/png",
"sizes": "32x32",
"href": "/tracex/favicon_32.png"
},
{
"rel": "icon",
"type": "image/png",
"sizes": "192x192",
"href": "/tracex/favicon_192.png"
}
]
}
}

View File

@ -0,0 +1,153 @@
version: "3"
services:
mongodb:
image: 'mongo:7-jammy'
command: mongod --port 27018
environment:
- PUID=1000
- PGID=1000
ports:
- 27018:27018
restart: unless-stopped
minio:
image: 'minio/minio'
command: server /data --address ":9000"
expose:
- 9000
ports:
- 9002:9000
elastic:
image: 'elasticsearch:7.14.2'
command: |
/bin/sh -c "./bin/elasticsearch-plugin list | grep -q ingest-attachment || yes | ./bin/elasticsearch-plugin install --silent ingest-attachment;
/usr/local/bin/docker-entrypoint.sh eswrapper"
expose:
- 9200
ports:
- 9201:9200
environment:
- ELASTICSEARCH_PORT_NUMBER=9200
- BITNAMI_DEBUG=true
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms1024m -Xmx1024m
- http.cors.enabled=true
- http.cors.allow-origin=http://localhost:8082
healthcheck:
interval: 20s
retries: 10
test: curl -s http://localhost:9200/_cluster/health | grep -vq '"status":"red"'
account:
image: hardcoreeng/account
pull_policy: never
links:
- mongodb
- minio
ports:
- 3003:3003
volumes:
- ./branding-test.json:/var/cfg/branding-test.json
environment:
- ACCOUNT_PORT=3003
- SERVER_SECRET=secret
- MONGO_URL=mongodb://mongodb:27018
- TRANSACTOR_URL=ws://transactor:3334
- ENDPOINT_URL=ws://localhost:3334
- STORAGE_CONFIG=${STORAGE_CONFIG}
- MODEL_ENABLED=*
- BRANDING_PATH=/var/cfg/branding-test.json
front:
image: hardcoreeng/front
pull_policy: never
links:
- account
- mongodb
- minio
- elastic
- collaborator
- transactor
ports:
- 8083:8083
volumes:
- ./branding-test.json:/app/dist/branding-test.json
environment:
- SERVER_PORT=8083
- SERVER_SECRET=secret
- ACCOUNTS_URL=http://localhost:3003
- MONGO_URL=mongodb://mongodb:27018
- UPLOAD_URL=/files
- ELASTIC_URL=http://elastic:9200
- GMAIL_URL=http://localhost:8088
- CALENDAR_URL=http://localhost:8095
- REKONI_URL=http://rekoni:4005
- TELEGRAM_URL=http://localhost:8086
- COLLABORATOR_URL=ws://localhost:3079
- COLLABORATOR_API_URL=http://localhost:3079
- STORAGE_CONFIG=${STORAGE_CONFIG}
- BRANDING_URL=http://localhost:8083/branding-test.json
transactor:
image: hardcoreeng/transactor
pull_policy: never
links:
- mongodb
- elastic
- minio
- rekoni
- account
ports:
- 3334:3334
volumes:
- ./branding-test.json:/var/cfg/branding-test.json
environment:
- SERVER_PROVIDER=${SERVER_PROVIDER}
- SERVER_PORT=3334
- SERVER_SECRET=secret
- ELASTIC_URL=http://elastic:9200
- MONGO_URL=mongodb://mongodb:27018
- METRICS_CONSOLE=false
- METRICS_FILE=metrics.txt
- STORAGE_CONFIG=${STORAGE_CONFIG}
- REKONI_URL=http://rekoni:4005
- FRONT_URL=http://localhost:8083
- UPLOAD_URL=http://localhost:8083/files
- ACCOUNTS_URL=http://account:3003
- LAST_NAME_FIRST=true
- ELASTIC_INDEX_NAME=local_storage_index
- BRANDING_PATH=/var/cfg/branding-test.json
collaborator:
image: hardcoreeng/collaborator
links:
- mongodb
- minio
- transactor
ports:
- 3079:3079
environment:
- COLLABORATOR_PORT=3079
- SECRET=secret
- ACCOUNTS_URL=http://account:3003
- TRANSACTOR_URL=ws://transactor:3334
- UPLOAD_URL=/files
- MONGO_URL=mongodb://mongodb:27018
- STORAGE_CONFIG=${STORAGE_CONFIG}
restart: unless-stopped
rekoni:
image: hardcoreeng/rekoni-service
restart: on-failure
ports:
- 4005:4004
deploy:
resources:
limits:
memory: 1024M
# qms-workers:
# image: hardcoreeng/qms-workers
# restart: always
# links:
# - transactor
# - account
# environment:
# - SECRET=secret
# - TRANSACTOR_URL=ws://transactor:3334
# - ACCOUNTS_URL=http://account:3003
volumes:
files:

35
qms-tests/prepare.sh Executable file
View File

@ -0,0 +1,35 @@
#!/bin/bash
docker compose -p sanity kill
docker compose -p sanity down --volumes
docker compose -p sanity up -d --force-recreate --renew-anon-volumes
docker_exit=$?
if [ ${docker_exit} -eq 0 ]; then
echo "Container started successfully"
else
echo "Container started with errors"
exit ${docker_exit}
fi
# Create init workspace
./tool.sh create-workspace init-ws-qms -w InitTest
./tool.sh configure init-ws-qms --enable=*
./tool.sh configure init-ws-qms --list
# Create workspace record in accounts
./tool.sh create-workspace sanity-ws-qms -w SanityTest
# Create user record in accounts
./tool.sh create-account user1 -f John -l Appleseed -p 1234
./tool.sh create-account user2 -f Kainin -l Dirak -p 1234
./tool.sh assign-workspace user1 sanity-ws-qms
./tool.sh assign-workspace user2 sanity-ws-qms
# Make user the workspace maintainer
./tool.sh confirm-email user1
./tool.sh confirm-email user2
./tool.sh create-account user_qara -f Qara -l Admin -p 1234
./tool.sh assign-workspace user_qara sanity-ws-qms
./tool.sh confirm-email user_qara
./restore-workspace.sh

14
qms-tests/restore-workspace.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
# Restore workspace contents in mongo/elastic
./tool.sh backup-restore ./sanity-ws-qms/ sanity-ws-qms
./tool.sh upgrade-workspace sanity-ws-qms
# Re-assign user to workspace.
./tool.sh assign-workspace user1 sanity-ws-qms
./tool.sh assign-workspace user2 sanity-ws-qms
./tool.sh assign-workspace user_qara sanity-ws-qms
./tool.sh configure sanity-ws-qms --enable=*
./tool.sh configure sanity-ws-qms --list

Binary file not shown.

9
qms-tests/sanity/.env Normal file
View File

@ -0,0 +1,9 @@
PLATFORM_URI='http://localhost:8083'
PLATFORM_TRANSACTOR='ws://localhost:3334'
PLATFORM_USER='user1'
PLATFORM_USER_SECOND='user2'
PLATFORM_TOKEN='eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb25maXJtZWQiOnRydWUsImVtYWlsIjoidXNlcjEiLCJ3b3Jrc3BhY2UiOiJzYW5pdHktd3MtcW1zIiwicHJvZHVjdElkIjoiIn0.vQK1jI8gHkjnNJf5XZ71L4dCqyHNmKW4_iBGrhXrqW8'
SETTING=storage.json
SETTING_SECOND=storageSecond.json
SETTING_QARA_MANAGER=storageQaraManager.json
PLATFORM_PASSWORD='1234'

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

5
qms-tests/sanity/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
playwright-report
test-results/*
allure-report
allure-results
screenshots/*

View File

@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "default"
}

View File

@ -0,0 +1,53 @@
{
"name": "@hcengineering/qms-tests-sanity",
"version": "0.6.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"author": "Hardcore Engineering Inc",
"template": "@hcengineering/default-package",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"build:watch": "compile",
"_phase:build": "compile transpile tests",
"_phase:test": "",
"_phase:format": "format src",
"_phase:validate": "compile validate",
"lint:fix": "eslint --fix tests",
"lint": "eslint tests",
"format": "format tests",
"ci": "playwright install --with-deps chromium",
"test": "",
"uitest": "cross-env LOCAL_URL=http://localhost:3003/ DEV_URL= playwright test -c ./tests/playwright.config.ts",
"staging-uitest": "cross-env PLATFORM_URI=https://front.hc.engineering/ playwright test -c ./tests/playwright.config.ts --grep @staging",
"dev-uitest": "cross-env PLATFORM_URI=http://localhost:8080 PLATFORM_TRANSACTOR=ws://localhost:3333 SETTING=storage-dev.json SETTING_SECOND=storageSecond-dev.json DEV_URL=http://localhost:8080/account playwright test -c ./tests/playwright.config.ts",
"debug": "playwright test -c ./tests/playwright.config.ts --debug --headed",
"dev-debug": "cross-env PLATFORM_URI=http://localhost:8080 PLATFORM_TRANSACTOR=ws://localhost:3333 SETTING=storage-dev.json SETTING_SECOND=storageSecond-dev.json playwright test -c ./tests/playwright.config.ts --debug --headed",
"codegen": "playwright codegen --load-storage storage.json http://localhost:8083/workbench/sanity-ws/",
"dev-codegen": "cross-env playwright codegen --load-storage storage-dev.json http://localhost:8080/workbench/sanity-ws/",
"allure:generate": "allure generate allure-results -o allure-report --clean"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@types/jest": "^29.5.5",
"@types/node": "~20.11.16",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"typescript": "^5.3.3",
"@playwright/test": "^1.41.2",
"allure-playwright": "^2.9.2"
},
"dependencies": {
"dotenv": "~16.0.0",
"cross-env": "~7.0.3",
"@hcengineering/core": "^0.6.32",
"@hcengineering/client-resources": "^0.6.27"
}
}

View File

@ -0,0 +1,34 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "login:metadata:LoginEmail",
"value": "user1"
},
{
"name": "login:metadata:LoginTokens",
"value": "{\"sanity-ws-qms\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb25maXJtZWQiOnRydWUsImVtYWlsIjoidXNlcjEiLCJ3b3Jrc3BhY2UiOiJzYW5pdHktd3MtcW1zIiwicHJvZHVjdElkIjoiIn0.vQK1jI8gHkjnNJf5XZ71L4dCqyHNmKW4_iBGrhXrqW8\"}"
},
{
"name": "login:metadata:LoginEndpoint",
"value": "ws://localhost:3334"
},
{
"name": "#platform.notification.logging",
"value": "false"
},
{
"name": "#platform.lazy.loading",
"value": "false"
},
{
"name": "flagOpenInDesktopApp",
"value": "true"
}
]
}
]
}

View File

@ -0,0 +1,34 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8083",
"localStorage": [
{
"name": "login:metadata:LoginEmail",
"value": "user1"
},
{
"name": "login:metadata:LoginTokens",
"value": "{\"sanity-ws-qms\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb25maXJtZWQiOnRydWUsImVtYWlsIjoidXNlcjEiLCJ3b3Jrc3BhY2UiOiJzYW5pdHktd3MtcW1zIiwicHJvZHVjdElkIjoiIn0.vQK1jI8gHkjnNJf5XZ71L4dCqyHNmKW4_iBGrhXrqW8\"}"
},
{
"name": "login:metadata:LoginEndpoint",
"value": "ws://localhost:3334"
},
{
"name": "#platform.notification.logging",
"value": "false"
},
{
"name": "#platform.lazy.loading",
"value": "false"
},
{
"name": "flagOpenInDesktopApp",
"value": "true"
}
]
}
]
}

View File

@ -0,0 +1,34 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8083",
"localStorage": [
{
"name": "login:metadata:LoginEmail",
"value": "user_qara"
},
{
"name": "login:metadata:LoginTokens",
"value": "{\"sanity-ws-qms\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb25maXJtZWQiOnRydWUsImVtYWlsIjoidXNlcl9xYXJhIiwid29ya3NwYWNlIjoic2FuaXR5LXdzLXFtcyIsInByb2R1Y3RJZCI6IiJ9.6OuCr6aIkzS9TiUAWbnFbsK98NKnCNqAY92ZkmYvw6k\"}"
},
{
"name": "login:metadata:LoginEndpoint",
"value": "ws://localhost:3334"
},
{
"name": "#platform.notification.logging",
"value": "false"
},
{
"name": "#platform.lazy.loading",
"value": "false"
},
{
"name": "flagOpenInDesktopApp",
"value": "true"
}
]
}
]
}

View File

@ -0,0 +1,34 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "login:metadata:LoginEmail",
"value": "user2"
},
{
"name": "login:metadata:LoginTokens",
"value": "{\"sanity-ws-qms\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb25maXJtZWQiOnRydWUsImVtYWlsIjoidXNlcjIiLCJ3b3Jrc3BhY2UiOiJzYW5pdHktd3MtcW1zIiwicHJvZHVjdElkIjoiIn0.-al3BXM41qvOaZ4s7c96xQZO73udvQrs8dH2bmhScww\"}"
},
{
"name": "login:metadata:LoginEndpoint",
"value": "ws://localhost:3334"
},
{
"name": "#platform.notification.logging",
"value": "false"
},
{
"name": "#platform.lazy.loading",
"value": "false"
},
{
"name": "flagOpenInDesktopApp",
"value": "true"
}
]
}
]
}

View File

@ -0,0 +1,34 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8083",
"localStorage": [
{
"name": "login:metadata:LoginEmail",
"value": "user2"
},
{
"name": "login:metadata:LoginTokens",
"value": "{\"sanity-ws-qms\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb25maXJtZWQiOnRydWUsImVtYWlsIjoidXNlcjIiLCJ3b3Jrc3BhY2UiOiJzYW5pdHktd3MtcW1zIiwicHJvZHVjdElkIjoiIn0.-al3BXM41qvOaZ4s7c96xQZO73udvQrs8dH2bmhScww\"}"
},
{
"name": "login:metadata:LoginEndpoint",
"value": "ws://localhost:3334"
},
{
"name": "#platform.notification.logging",
"value": "false"
},
{
"name": "#platform.lazy.loading",
"value": "false"
},
{
"name": "flagOpenInDesktopApp",
"value": "true"
}
]
}
]
}

View File

@ -0,0 +1,170 @@
import { test } from '@playwright/test'
import { attachScreenshot, generateId, HomepageURI, PlatformSetting, PlatformURI } from '../utils'
import { DocumentStatus, NewCategory, NewTemplate, UpdateCategory } from '../model/types'
import { allure } from 'allure-playwright'
import { CategoriesPage } from '../model/documents/categories-page'
import { CategoryDetailsPage } from '../model/documents/category-details-page'
import { prepareCategoryStep } from './common-documents-steps'
import { LeftSideMenuPage } from '../model/left-side-menu-page'
import { NavigationMenuPage } from '../model/documents/navigation-menu-page'
import { TemplatesPage } from '../model/documents/templates-page'
import { DocumentContentPage } from '../model/documents/document-content-page'
test.use({
storageState: PlatformSetting
})
test.describe('QMS. Categories tests', () => {
test.beforeEach(async ({ page }) => {
await (await page.goto(`${PlatformURI}/${HomepageURI}`))?.finished()
})
test('TESTS-131. Create a new category', async ({ page }) => {
await allure.description('Requirement\nUsers need to create a new category')
await allure.tms('TESTS-131', 'https://front.hc.engineering/workbench/platform/tracker/TESTS-131')
const newCategory: NewCategory = {
title: `New category-${generateId()}`,
code: `CODE-${generateId(4)}`,
description: `New category description-${generateId()}`,
attachFileName: 'cat.jpeg'
}
await prepareCategoryStep(page, newCategory)
await test.step('2. Check category information', async () => {
const categoriesPage = new CategoriesPage(page)
await categoriesPage.openCategory(newCategory.title)
const categoryDetailsPage = new CategoryDetailsPage(page)
await categoryDetailsPage.checkTitle(newCategory.title)
})
await attachScreenshot('TESTS-131_create_category.png', page)
})
test('TESTS-132. Edit a Category', async ({ page }) => {
await allure.description('Requirement\nUsers need to edit a category')
await allure.tms('TESTS-132', 'https://front.hc.engineering/workbench/platform/tracker/TESTS-132')
const editCategory: NewCategory = {
title: `Edit category-${generateId()}`,
code: `EDIT-${generateId(4)}`,
description: `Edit category description-${generateId()}`
}
const updatedEditCategory: UpdateCategory = {
description: `Updated edit category description-${generateId()}`,
attachFileName: 'cat.jpeg'
}
await prepareCategoryStep(page, editCategory)
await test.step('2. Edit Description and add Attachments', async () => {
const categoriesPage = new CategoriesPage(page)
await categoriesPage.openCategory(editCategory.title)
const categoryDetailsPage = new CategoryDetailsPage(page)
await categoryDetailsPage.checkCategory(editCategory)
await categoryDetailsPage.editCategory(updatedEditCategory)
})
await test.step('3. Check category information', async () => {
const categoryDetailsPage = new CategoryDetailsPage(page)
await categoryDetailsPage.checkCategory({
...editCategory,
...updatedEditCategory
})
})
await attachScreenshot('TESTS-132_edit_category.png', page)
})
test('TESTS-133. Delete Category', async ({ page }) => {
await allure.description('Requirement\nUsers need to delete category')
await allure.tms('TESTS-133', 'https://front.hc.engineering/workbench/platform/tracker/TESTS-133')
const editCategory: NewCategory = {
title: `Delete category-${generateId()}`,
code: `DELETE-${generateId(4)}`,
description: `Delete category description-${generateId()}`
}
await prepareCategoryStep(page, editCategory)
await test.step('2. Delete a Category', async () => {
const categoriesPage = new CategoriesPage(page)
await categoriesPage.openCategory(editCategory.title)
const categoryDetailsPage = new CategoryDetailsPage(page)
await categoryDetailsPage.checkCategory(editCategory)
await categoryDetailsPage.executeMoreAction('Delete')
})
await test.step('3. Check category doesnt exist', async () => {
const categoriesPage = new CategoriesPage(page)
await categoriesPage.checkCategoryNotExist(editCategory.title)
})
await attachScreenshot('TESTS-133_delete_category.png', page)
})
test('TESTS-215. Can not Delete Category if there are templates associated', async ({ page }) => {
await allure.description('Requirement\nUsers can not delete the category if there are templates associated')
await allure.tms('TESTS-215', 'https://front.hc.engineering/workbench/platform/tracker/TESTS-215')
const canNotDeleteCategory: NewCategory = {
title: `Can not Delete category-${generateId()}`,
code: `CANT_DELETE-${generateId(4)}`,
description: `Can not Delete category description-${generateId()}`
}
const canNotDeleteCategoryTemplate: NewTemplate = {
location: {
space: 'Quality documents',
parent: 'Quality documents'
},
title: `Can not Delete Category Template-${generateId()}`,
description: `Can not Delete Category Template description-${generateId()}`,
code: generateId(4),
category: canNotDeleteCategory.title,
reason: 'Test reason',
reviewers: ['Appleseed John'],
approvers: ['Appleseed John']
}
await prepareCategoryStep(page, canNotDeleteCategory)
await test.step('2. Create new templates to the category', async () => {
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.buttonDocuments.click()
const navigationMenuPage = new NavigationMenuPage(page)
await navigationMenuPage.buttonTemplates.click()
const templatesPage = new TemplatesPage(page)
await templatesPage.buttonCreateTemplate.click()
await templatesPage.createTemplate(canNotDeleteCategoryTemplate)
const documentContentPage = new DocumentContentPage(page)
await documentContentPage.checkDocumentTitle(canNotDeleteCategoryTemplate.title)
await documentContentPage.checkDocument({
type: 'N/A',
category: canNotDeleteCategoryTemplate.category ?? '',
version: 'v0.1',
status: DocumentStatus.DRAFT,
owner: 'Appleseed John',
author: 'Appleseed John'
})
})
await test.step('3. Try to Delete a Category', async () => {
const navigationMenuPage = new NavigationMenuPage(page)
await navigationMenuPage.buttonCategories.click()
const categoriesPage = new CategoriesPage(page)
await categoriesPage.openCategory(canNotDeleteCategory.title)
const categoryDetailsPage = new CategoryDetailsPage(page)
await categoryDetailsPage.checkCategory(canNotDeleteCategory)
await categoryDetailsPage.checkMoreActionNotExist('Delete')
})
await attachScreenshot('TESTS-215_can_not_delete_category.png', page)
})
})

View File

@ -0,0 +1,35 @@
import { Page, test } from '@playwright/test'
import { LeftSideMenuPage } from '../model/left-side-menu-page'
import { DocumentsPage } from '../model/documents/documents-page'
import { NewCategory, NewDocument } from '../model/types'
import { NavigationMenuPage } from '../model/documents/navigation-menu-page'
import { CategoriesPage } from '../model/documents/categories-page'
import { CategoryCreatePopup } from '../model/documents/category-create-popup'
export async function prepareDocumentStep (page: Page, document: NewDocument, stepNumber: number = 1): Promise<void> {
await test.step(`${stepNumber}. Create a new document`, async () => {
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.buttonDocuments.click()
const documentsPage = new DocumentsPage(page)
await documentsPage.buttonCreateDocument.click()
await documentsPage.createDocument(document)
})
}
export async function prepareCategoryStep (page: Page, newCategory: NewCategory): Promise<void> {
await test.step('1. Create a new category', async () => {
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.buttonDocuments.click()
const navigationMenuPage = new NavigationMenuPage(page)
await navigationMenuPage.buttonCategories.click()
const categoriesPage = new CategoriesPage(page)
await categoriesPage.buttonCreateCategory.click()
const categoryCreatePopup = new CategoryCreatePopup(page)
await categoryCreatePopup.createCategory(newCategory)
})
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,104 @@
import { test } from '@playwright/test'
import { attachScreenshot, generateId, HomepageURI, PlatformSetting, PlatformURI } from '../utils'
import { LeftSideMenuPage } from '../model/left-side-menu-page'
import { DocumentStatus, NewTemplate } from '../model/types'
import { DocumentContentPage } from '../model/documents/document-content-page'
import { NavigationMenuPage } from '../model/documents/navigation-menu-page'
import { TemplatesPage } from '../model/documents/templates-page'
import { allure } from 'allure-playwright'
test.use({
storageState: PlatformSetting
})
test.describe('QMS. Templates tests', () => {
test.beforeEach(async ({ page }) => {
await (await page.goto(`${PlatformURI}/${HomepageURI}`))?.finished()
})
test('TESTS-129. Create Template', async ({ page }) => {
await allure.description('Requirement\nUsers need to create a new template')
await allure.tms('TESTS-129', 'https://front.hc.engineering/workbench/platform/tracker/TESTS-129')
const newTemplate: NewTemplate = {
location: {
space: 'Quality documents',
parent: 'Quality documents'
},
title: `New Template-${generateId()}`,
description: `New Template description-${generateId()}`,
code: generateId(4),
category: 'Document-Control',
reason: 'Test reason',
reviewers: ['Appleseed John'],
approvers: ['Appleseed John']
}
await test.step('1. Create a new template', async () => {
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.buttonDocuments.click()
const navigationMenuPage = new NavigationMenuPage(page)
await navigationMenuPage.buttonTemplates.click()
const templatesPage = new TemplatesPage(page)
await templatesPage.buttonCreateTemplate.click()
await templatesPage.createTemplate(newTemplate)
})
await test.step('2. Check document information', async () => {
const documentContentPage = new DocumentContentPage(page)
await documentContentPage.checkDocumentTitle(newTemplate.title)
await documentContentPage.checkDocument({
type: 'N/A',
category: newTemplate.category ?? '',
version: 'v0.1',
status: DocumentStatus.DRAFT,
owner: 'Appleseed John',
author: 'Appleseed John'
})
})
await attachScreenshot('TESTS-129_create_template.png', page)
})
test('TESTS-181. Delete template', async ({ page }) => {
await allure.description('Requirement\nUsers need to delete the template')
await allure.tms('TESTS-181', 'https://front.hc.engineering/workbench/platform/tracker/TESTS-181')
const deleteTemplate: NewTemplate = {
title: `Delete Template-${generateId()}`,
description: `Delete Template description-${generateId()}`,
code: generateId(4),
location: {
space: 'Quality documents',
parent: 'Quality documents'
}
}
const documentContentPage = new DocumentContentPage(page)
await test.step('1. Create a new template', async () => {
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.buttonDocuments.click()
const navigationMenuPage = new NavigationMenuPage(page)
await navigationMenuPage.buttonTemplates.click()
const templatesPage = new TemplatesPage(page)
await templatesPage.buttonCreateTemplate.click()
await templatesPage.createTemplate(deleteTemplate)
await documentContentPage.checkDocumentTitle(deleteTemplate.title)
await documentContentPage.checkDocumentStatus(DocumentStatus.DRAFT)
})
await test.step('2. Delete the template', async () => {
await documentContentPage.executeMoreActions('Delete')
await documentContentPage.pressYesForPopup(page)
})
await test.step('3. Check that the document status is equal to the deleted', async () => {
await documentContentPage.checkDocumentStatus(DocumentStatus.DELETED)
})
await attachScreenshot('TESTS-181_delete_template.png', page)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,79 @@
import { expect, test } from '@playwright/test'
import { attachScreenshot, generateId, PlatformURI, randomString } from '../utils'
import { allure } from 'allure-playwright'
import { UserSignUp } from '../model/types'
import { LoginPage } from '../model/login-page'
import { SignupPage } from '../model/signup-page'
import { SelectWorkspacePage } from '../model/select-workspace-page'
import { LeftSideMenuPage } from '../model/left-side-menu-page'
test.describe('Registration tests', () => {
test.beforeEach(async ({ page }) => {
await (await page.goto(`${PlatformURI}`))?.finished()
})
// Skipped until we fix init workspace for tracex
test.skip('TESTS-143. User Registration', async ({ page }) => {
await allure.description('Requirement\nUsers need to register with the system')
await allure.tms('TESTS-143', 'https://front.hc.engineering/workbench/platform/tracker/TESTS-143')
await allure.description('User Registration')
const workspaceName = `workspace-${generateId(4)}`
const signUpUserData: UserSignUp = {
firstName: randomString(),
lastName: randomString(),
email: `${randomString()}@gmail.com`,
password: '1234'
}
await test.step('1. Registration', async () => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.buttonSignUp.click()
const signupPage = new SignupPage(page)
await signupPage.signup(signUpUserData)
await attachScreenshot('TESTS-143_registration.png', page)
await signupPage.buttonSignUp.click()
})
await test.step('2. Create workspace', async () => {
const selectWorkspacePage = new SelectWorkspacePage(page)
await selectWorkspacePage.createWorkspace(workspaceName)
await attachScreenshot('TESTS-143_create_workspace.png', page)
await selectWorkspacePage.buttonCreateWorkspace.click()
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.buttonDocuments.waitFor({ state: 'visible' })
await expect(leftSideMenuPage.buttonDocuments).toBeVisible()
})
})
test('TESTS-144. Negative - Duplicate Email', async ({ page }) => {
await allure.description('Requirement\nThe system should reject the registration with an email that already exists')
await allure.tms('TESTS-144', 'https://front.hc.engineering/workbench/platform/tracker/TESTS-144')
const signUpUserData: UserSignUp = {
firstName: randomString(),
lastName: randomString(),
email: 'user1',
password: '1234'
}
await test.step('1. Attempt to register a user with an email that already exists', async () => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.buttonSignUp.click()
const signupPage = new SignupPage(page)
await signupPage.signup(signUpUserData)
await signupPage.buttonSignUp.click()
await expect(signupPage.textError).toHaveText('Account already exists')
})
await attachScreenshot('TESTS-144_duplicate_email.png', page)
})
})

View File

@ -0,0 +1,41 @@
import { Locator, Page } from '@playwright/test'
import { CommonPage } from './common-page'
export class CalendarPage extends CommonPage {
readonly page: Page
readonly buttonDatePopupToday: Locator
readonly inputPopupDateDay: Locator
readonly inputPopupDateMonth: Locator
readonly inputPopupDateYear: Locator
readonly inputPopupTime: Locator
readonly inputPopupDateSave: Locator
constructor (page: Page) {
super()
this.page = page
this.buttonDatePopupToday = page.locator('div.popup div.today:not(.wrongMonth)')
this.inputPopupDateDay = page.locator('div[class*="date-popup"] div.datetime-input span.digit:first-child')
this.inputPopupDateMonth = page.locator('div[class*="date-popup"] div.datetime-input span.digit:nth-child(3)')
this.inputPopupDateYear = page.locator('div[class*="date-popup"] div.datetime-input span.digit:nth-child(5)')
this.inputPopupTime = page.locator('div[class*="date-popup"] div.datetime-input span.digit:nth-child(7)')
this.inputPopupDateSave = page.locator('div[class*="date-popup"] div.footer button')
}
async fillSelectDatePopup (day: string, month: string, year: string, time: string): Promise<void> {
await this.inputPopupDateDay.click()
await this.inputPopupDateDay.pressSequentially(day)
await this.inputPopupDateMonth.click()
await this.inputPopupDateMonth.pressSequentially(month)
await this.inputPopupDateYear.click()
await this.inputPopupDateYear.pressSequentially(year)
await this.inputPopupTime.click()
await this.inputPopupTime.pressSequentially(time)
await this.inputPopupDateSave.click()
}
async fillSelectDateByShortcut (shortcut: string): Promise<void> {
await this.page.locator('div.shift-container div.btn > span', { hasText: shortcut }).click()
}
}

View File

@ -0,0 +1,45 @@
import { expect, Page } from '@playwright/test'
export class CommonPage {
async selectListItemWithSearch (page: Page, name: string, fullWordSearch: boolean = false): Promise<void> {
if (name !== 'first') {
const searchWord = fullWordSearch ? name : name.split(' ')[0]
await page.locator('div.selectPopup input').fill(searchWord)
}
await page.locator('div.selectPopup div.list-item:first-child').click({ delay: 100 })
}
async selectListItem (page: Page, name: string): Promise<void> {
await page.locator('div.selectPopup div.list-item', { hasText: name }).click({ delay: 100 })
}
async pressCreateButtonSelectPopup (page: Page): Promise<void> {
await page.locator('div.selectPopup div.header button:last-child').click()
}
async addNewTagPopup (page: Page, title: string, description: string): Promise<void> {
await page.locator('div.popup form[id="tags:string:AddTag"] input[placeholder$="title"]').fill(title)
await page
.locator('div.popup form[id="tags:string:AddTag"] input[placeholder="Please type description here"]')
.fill(description)
await page.locator('div.popup form[id="tags:string:AddTag"] button[type="submit"]').click()
}
async selectMenuItem (page: Page, point: string): Promise<void> {
await page.locator('div.selectPopup button.menu-item', { hasText: point }).click()
}
async pressYesDeletePopup (page: Page): Promise<void> {
await page.locator('div.popup button.dangerous').click()
await expect(page.locator('div.popup button.dangerous')).toBeHidden()
}
async selectFromDropdown (page: Page, point: string): Promise<void> {
await page.locator('div[class$="opup"] span[class*="label"]', { hasText: point }).click()
}
async pressYesForPopup (page: Page): Promise<void> {
await expect(page.locator('div.popup button[type="submit"]')).toBeVisible()
await page.locator('div.popup button[type="submit"]').click()
}
}

View File

@ -0,0 +1,23 @@
import { type Locator, type Page, expect } from '@playwright/test'
import { CalendarPage } from '../calendar-page'
export class CategoriesPage extends CalendarPage {
readonly page: Page
readonly buttonCreateCategory: Locator
constructor (page: Page) {
super(page)
this.page = page
this.buttonCreateCategory = page.locator('div[slot="header-tools"] button[type="submit"]', { hasText: 'Category' })
}
async openCategory (categoryTitle: string): Promise<void> {
await this.page.locator(`//span[text()='${categoryTitle}']/../../..//div[contains(@class, "firstCell")]/a`).click()
}
async checkCategoryNotExist (categoryTitle: string): Promise<void> {
await expect(
this.page.locator(`//span[text()='${categoryTitle}']/../../..//div[contains(@class, "firstCell")]/a`)
).toBeVisible({ visible: false })
}
}

View File

@ -0,0 +1,40 @@
import { type Locator, type Page, expect } from '@playwright/test'
import { CalendarPage } from '../calendar-page'
import { NewCategory } from '../types'
import path from 'path'
export class CategoryCreatePopup extends CalendarPage {
readonly page: Page
readonly inputNewCategoryTitle: Locator
readonly inputNewCategoryDescription: Locator
readonly inputNewCategoryCode: Locator
readonly inputAttachFile: Locator
readonly buttonNewCategoryCreate: Locator
constructor (page: Page) {
super(page)
this.page = page
this.inputNewCategoryTitle = page.locator('input[placeholder="Title"]')
this.inputNewCategoryCode = page.locator('input[placeholder="Code"]')
this.inputNewCategoryDescription = page.locator('div.inputMsg div.tiptap')
this.inputAttachFile = page.locator('form[id="documents:string:CreateDocumentCategory"] input[type="file"]#file')
this.buttonNewCategoryCreate = page.locator(
'form[id="documents:string:CreateDocumentCategory"] button[type="submit"]'
)
}
async createCategory (data: NewCategory): Promise<void> {
await this.inputNewCategoryTitle.fill(data.title)
await this.inputNewCategoryCode.fill(data.code)
await this.inputNewCategoryDescription.fill(data.description)
if (data.attachFileName != null) {
await this.inputAttachFile.setInputFiles(path.join(__dirname, `../../files/${data.attachFileName}`))
await expect(
this.page.locator('div[class*="attachments"] div.name', { hasText: data.attachFileName })
).toBeVisible()
}
await this.buttonNewCategoryCreate.click()
}
}

View File

@ -0,0 +1,59 @@
import { type Locator, type Page, expect } from '@playwright/test'
import { CalendarPage } from '../calendar-page'
import { NewCategory, UpdateCategory } from '../types'
import path from 'path'
export class CategoryDetailsPage extends CalendarPage {
readonly page: Page
readonly inputCategoryTitle: Locator
readonly inputCategoryCode: Locator
readonly textDescription: Locator
readonly inputAttachFile: Locator
readonly textAttachFile: Locator
readonly buttonMoreActions: Locator
constructor (page: Page) {
super(page)
this.page = page
this.inputCategoryTitle = page.locator('input[placeholder="documents:string:DomainTitle"]')
this.inputCategoryCode = page.locator('input[placeholder="Category"]')
this.textDescription = page.locator('div.grid div.tiptap')
this.inputAttachFile = page.locator('div.grid input#file')
this.buttonMoreActions = page.locator('div[class*="title"] > div:last-child > button:first-child')
this.textAttachFile = page.locator('div.attachment-grid-container div[class*="attachment"] div.name')
}
async checkTitle (categoryTitle: string): Promise<void> {
await expect(this.inputCategoryTitle).toHaveValue(categoryTitle)
}
async editCategory (data: UpdateCategory): Promise<void> {
await this.textDescription.fill(data.description)
if (data.attachFileName != null) {
await this.inputAttachFile.setInputFiles(path.join(__dirname, `../../files/${data.attachFileName}`))
await expect(this.textAttachFile.filter({ hasText: data.attachFileName })).toBeVisible()
}
}
async checkCategory (data: NewCategory): Promise<void> {
await expect(this.inputCategoryTitle).toHaveValue(data.title)
await expect(this.inputCategoryCode).toHaveValue(data.code)
await expect(this.textDescription).toContainText(data.description)
if (data.attachFileName != null) {
await expect(this.textAttachFile.filter({ hasText: data.attachFileName })).toBeVisible()
}
}
async executeMoreAction (action: string): Promise<void> {
await this.buttonMoreActions.click()
await this.selectFromDropdown(this.page, action)
await this.pressYesDeletePopup(this.page)
}
async checkMoreActionNotExist (action: string): Promise<void> {
await this.buttonMoreActions.click()
await expect(this.page.locator('div.popup button.ap-menuItem', { hasText: action })).toBeVisible({ visible: false })
}
}

View File

@ -0,0 +1,26 @@
import { type Page, expect } from '@playwright/test'
import { DocumentCommonPage } from './document-common-page'
export class DocumentApprovalsPage extends DocumentCommonPage {
readonly page: Page
constructor (page: Page) {
super(page)
this.page = page
}
async checkRejectApproval (approvalName: string, message: string): Promise<void> {
await expect(
this.page
.locator('div.reject-message', { hasText: message })
.locator('xpath=..')
.locator('div.approver span.ap-label')
).toHaveText(approvalName)
}
async checkSuccessApproval (approvalName: string): Promise<void> {
await expect(this.page.locator('svg[fill*="accepted"]').locator('xpath=..').locator('span.ap-label')).toHaveText(
approvalName
)
}
}

View File

@ -0,0 +1,133 @@
import { type Locator, type Page, expect } from '@playwright/test'
import { DocumentCommonPage } from './document-common-page'
export class DocumentCommentsPage extends DocumentCommonPage {
readonly page: Page
readonly buttonDocumentTitle: Locator
constructor (page: Page) {
super(page)
this.page = page
this.buttonDocumentTitle = page.locator('button.version-item span.name')
}
async checkCommentExist (message: string, count: number = 1): Promise<void> {
await expect(this.page.locator('div[data-float="aside"] div.root span', { hasText: message })).toHaveCount(count)
}
async resolveComments (message: string, position: string = '1'): Promise<void> {
await this.page
.locator('div[data-float="aside"] div.root span', { hasText: message })
.locator('xpath=..')
.locator('span:first-child', { hasText: position })
.hover()
await this.page
.locator('div[data-float="aside"] div.root span', { hasText: message })
.locator('xpath=..')
.locator('span:first-child', { hasText: position })
.locator('xpath=../..')
.locator('div.tools button')
.click()
}
async resolveAllComments (): Promise<void> {
const buttonsCount: number = await this.page.locator('div[data-float="aside"] div.root div.tools button').count()
for (let i = 0; i < buttonsCount; i++) {
await this.page.locator('div[data-float="aside"] div.root').first().click()
await this.page.locator('div[data-float="aside"] div.root div.tools button').first().click()
}
}
async checkCommentNotExist (message: string): Promise<void> {
await expect(this.page.locator('div[data-float="aside"] div.root span', { hasText: message })).toHaveCount(0)
}
async checkCommentCanBeResolved (message: string, position: number): Promise<void> {
await this.page
.locator('div[data-float="aside"] div.root span', { hasText: message })
.locator('xpath=..')
.locator('span:first-child', { hasText: String(position) })
.hover()
await expect(
this.page
.locator('div[data-float="aside"] div.root span', { hasText: message })
.locator('xpath=..')
.locator('span:first-child', { hasText: String(position) })
.locator('xpath=../..')
.locator('div.tools button')
).toBeEnabled()
}
async checkCommentCanNotBeResolved (message: string, position: number): Promise<void> {
await this.page
.locator('div[data-float="aside"] div.root span', { hasText: message })
.locator('xpath=..')
.locator('span:first-child', { hasText: String(position) })
.hover()
await expect(
this.page
.locator('div[data-float="aside"] div.root span', { hasText: message })
.locator('xpath=..')
.locator('span:first-child', { hasText: String(position) })
.locator('xpath=../..')
.locator('div.tools button')
).not.toBeVisible()
}
async addReplyInPopupByCommentId (commentId: number, replyText: string): Promise<void> {
const comment = this.page
.locator('div.popup div.root div.header span:first-child', { hasText: String(commentId) })
.locator('xpath=../../../..')
await comment.locator('div.ref-input div.tiptap').fill(replyText)
await comment.locator('div.ref-input div.buttons-panel > button').click()
}
async checkCommentInPopupById (
commentId: number,
header: string,
author: string,
message: string,
reply: string
): Promise<void> {
const comment = this.page
.locator('div.popup div.root div.header > div > span:first-child', { hasText: String(commentId) })
.locator('xpath=../../../..')
// check header
await expect(comment.locator('div.header > div > span:last-child')).toHaveText(header)
// can be resolved
await comment.locator('div.header div.tools button').hover()
await expect(comment.locator('div.header div.tools button')).toBeEnabled()
// check author
await expect(comment.locator('div.root div.header > a span[class*="label"]').first()).toHaveText(author)
// check message
await expect(comment.locator('div.activityMessage div.flex-col div.clear-mins > p').first()).toHaveText(message)
// check comment
await expect(comment.locator('div.activityMessage div.flex-col div.clear-mins > p').last()).toHaveText(reply)
}
async checkCommentInPanelById (
commentId: number,
header: string,
author: string,
message: string,
reply: string
): Promise<void> {
const comment = this.page
.locator('div.box div.root div.header > div > span:first-child', { hasText: String(commentId) })
.locator('xpath=../../../..')
// check header
await expect(comment.locator('div.header > div > span:last-child')).toHaveText(header)
// can be resolved
await comment.locator('div.header > div > span:last-child').hover()
await expect(comment.locator('div.header div.tools button')).toBeEnabled()
// check author
await expect(comment.locator('div.root div.header > a span[class*="label"]').first()).toHaveText(author)
// check message
await expect(comment.locator('div.activityMessage div.flex-col div.clear-mins > p').first()).toHaveText(message)
// check comment
await expect(comment.locator('div.activityMessage div.flex-col div.clear-mins > p').last()).toHaveText(reply)
}
}

View File

@ -0,0 +1,28 @@
import { type Locator, type Page } from '@playwright/test'
import { CalendarPage } from '../calendar-page'
export class DocumentCommonPage extends CalendarPage {
readonly page: Page
readonly inputMessageText: Locator
readonly buttonMessageSend: Locator
readonly buttonReleaseTab: Locator
readonly buttonContentTab: Locator
readonly buttonReasonAndImpactTab: Locator
readonly buttonHistoryTab: Locator
constructor (page: Page) {
super(page)
this.page = page
this.inputMessageText = page.locator('div.popup div.tiptap')
this.buttonMessageSend = page.locator('div.popup button.primary')
this.buttonReleaseTab = page.locator('div.tab', { hasText: 'Release' })
this.buttonContentTab = page.locator('div.tab', { hasText: 'Content' })
this.buttonReasonAndImpactTab = page.locator('div.tab', { hasText: 'Reason & Impact' })
this.buttonHistoryTab = page.locator('div.tab', { hasText: 'History' })
}
async addMessage (message: string): Promise<void> {
await this.inputMessageText.fill(message)
await this.buttonMessageSend.click()
}
}

View File

@ -0,0 +1,306 @@
import { expect, type Locator, type Page } from '@playwright/test'
import { Content, DocumentDetails, DocumentRights, DocumentStatus } from '../types'
import { DocumentCommonPage } from './document-common-page'
import { iterateLocator, PlatformPassword } from '../../utils'
export class DocumentContentPage extends DocumentCommonPage {
readonly page: Page
readonly buttonDocumentTitle: Locator
readonly buttonMoreActions: Locator
readonly textDocumentStatus: Locator
readonly textType: Locator
readonly textCategory: Locator
readonly textVersion: Locator
readonly textStatus: Locator
readonly textOwner: Locator
readonly textAuthor: Locator
readonly buttonSelectNewOwner: Locator
readonly buttonSelectNewOwnerChange: Locator
readonly buttonSendForReview: Locator
readonly buttonSendForApproval: Locator
readonly buttonAddMembers: Locator
readonly buttonSelectMemberSubmit: Locator
readonly textSelectReviewersPopup: Locator
readonly textSelectApproversPopup: Locator
readonly buttonCurrentRights: Locator
readonly buttonAddMessageToText: Locator
readonly buttonComments: Locator
readonly textDocumentTitle: Locator
readonly buttonCompleteReview: Locator
readonly inputPassword: Locator
readonly buttonSubmit: Locator
readonly buttonReject: Locator
readonly inputRejectionReason: Locator
readonly buttonApprove: Locator
readonly buttonEditDocument: Locator
readonly buttonDraftNewVersion: Locator
readonly buttonDocumentInformation: Locator
readonly buttonDocument: Locator
readonly buttonDocumentApprovals: Locator
readonly textPageHeader: Locator
readonly buttonSelectNewOwnerChangeByQaraManager: Locator
readonly textId: Locator
readonly sectionsLocatorViewRight: Locator
readonly sectionsLocatorEditRight: Locator
constructor (page: Page) {
super(page)
this.page = page
this.buttonDocumentTitle = page.locator('button.version-item span.name')
this.buttonMoreActions = page.locator('div.popupPanel-title > [class="no-print"] > button:not([id])')
this.textDocumentStatus = page.locator('button.version-item div.root span.label')
this.textType = page.locator('div.flex:has(div.label:text("Template name")) div.field')
this.textCategory = page.locator('div.flex:has(div.label:text("Category")) div.field')
this.textVersion = page.locator('div.flex:has(div.label:text("Version")) div.field')
this.textStatus = page.locator('div.flex:has(div.label:text("Status")) div.field')
this.textOwner = page.locator('div.flex:has(div.label:text("Owner")) div.field')
this.textAuthor = page.locator('div.flex:has(div.label:text("Author")) div.field')
this.buttonSelectNewOwner = page.locator('div.popup button.small')
this.buttonSelectNewOwnerChange = page.locator('div.popup button.dangerous')
this.buttonSendForReview = page.locator('div.popupPanel-title button[type="button"] > span', {
hasText: 'Send for review'
})
this.buttonSendForApproval = page.locator('div.popupPanel-title button[type="button"] > span', {
hasText: 'Send for approval'
})
this.buttonAddMembers = page.locator('div.popup div.addButton')
this.buttonSelectMemberSubmit = page.locator('div.popup div.footer button[type="submit"]')
this.textSelectReviewersPopup = page.locator('div.popup span.label', { hasText: 'Select reviewers' })
this.textSelectApproversPopup = page.locator('div.popup span.label', { hasText: 'Select approvers' })
this.buttonCurrentRights = page.locator('div.popupPanel-title button[type="button"] > span[slot="content"]')
this.buttonAddMessageToText = page.locator('div.text-editor-toolbar > button:last-child')
this.buttonComments = page.locator('button[id$="comment"]')
this.textDocumentTitle = page.locator('div.panel div.title')
this.buttonCompleteReview = page.locator('div.popupPanel-title button[type="button"] > span', {
hasText: 'Complete review'
})
this.inputPassword = page.locator('input[name="documents:string:Password"]')
this.buttonSubmit = page.locator('div.popup button[type="submit"]')
this.buttonReject = page.locator('button[type="button"] > span', { hasText: 'Reject' })
this.inputRejectionReason = page.locator('div.popup div[id="rejection-reason"] input')
this.buttonApprove = page.locator('button[type="button"] > span', { hasText: 'Approve' })
this.buttonDocument = page.locator('button[id$="info"]')
this.buttonEditDocument = page.locator('div.popupPanel-title button[type="button"] > span', {
hasText: 'Edit document'
})
this.buttonDraftNewVersion = page.locator('div.popupPanel-title button[type="button"] > span', {
hasText: 'Draft new version'
})
this.buttonDocumentInformation = page.locator('button[id$="info"]')
this.buttonDocumentApprovals = page.locator('button[id$="approvals"]')
this.textPageHeader = page.locator('div.hulyNavPanel-header')
this.buttonSelectNewOwnerChangeByQaraManager = page.locator('div.popup button[type="submit"]')
this.textId = page.locator('div.flex:has(div.label:text("ID")) div.field')
this.sectionsLocatorViewRight = page.locator('div.section span.label')
this.sectionsLocatorEditRight = page.locator('div.section span.label input')
}
async checkDocumentTitle (title: string): Promise<void> {
await expect(this.buttonDocumentTitle).toContainText(title)
}
async updateSectionTitle (sectionId: string, title: string): Promise<void> {
await this.page
.locator('span.hdr-alignment:not([class*="label"])', { hasText: sectionId })
.locator('xpath=..')
.locator('span.label input')
.fill(title)
}
async addContentToTheSection (content: Content): Promise<void> {
const section = await this.getSectionLocator(content.sectionTitle)
await section
.locator('xpath=../../../../../../../..')
.locator('div.content-container div.tiptap')
.clear({ force: true })
await section
.locator('xpath=../../../../../../../..')
.locator('div.content-container div.tiptap')
.fill(content.content)
}
async checkContentForTheSection (content: Content): Promise<void> {
const section = await this.getSectionLocator(content.sectionTitle)
const parentLocator =
(await this.buttonCurrentRights.textContent()) === DocumentRights.EDITING
? '../../../../../../../..'
: '../../../..'
await expect(section.locator(`xpath=${parentLocator}`).locator('div.content-container div.tiptap')).toHaveText(
content.content
)
}
async executeMoreActions (action: string): Promise<void> {
await this.buttonMoreActions.click()
await this.selectFromDropdown(this.page, action)
}
async checkDocumentStatus (status: DocumentStatus): Promise<void> {
await expect(this.textDocumentStatus).toHaveText(status)
}
async checkDocument (data: DocumentDetails): Promise<void> {
if (data.type != null && data.type !== 'N/A') {
await expect(this.textType).toHaveText(data.type)
}
if (data.category != null) {
await expect(this.textCategory).toContainText(data.category)
}
if (data.version != null) {
await expect(this.textVersion).toHaveText(data.version)
}
if (data.status != null) {
await expect(this.textStatus).toHaveText(data.status)
}
if (data.owner != null) {
await expect(this.textOwner).toHaveText(data.owner)
}
if (data.author != null) {
await expect(this.textAuthor).toHaveText(data.author)
}
if (data.id != null) {
await expect(this.textId).toHaveText(data.id)
}
}
async fillChangeDocumentOwnerPopup (newOwner: string): Promise<void> {
await this.buttonSelectNewOwner.click()
await this.selectListItemWithSearch(this.page, newOwner)
await this.buttonSelectNewOwnerChange.click()
}
async fillSelectReviewersForm (reviewers: Array<string>): Promise<void> {
await this.buttonAddMembers.click()
for (const reviewer of reviewers) {
await this.selectListItemWithSearch(this.page, reviewer)
}
await this.textSelectReviewersPopup.click({ force: true })
await this.buttonSelectMemberSubmit.click()
}
async fillSelectApproversForm (approvers: Array<string>): Promise<void> {
await this.buttonAddMembers.click()
for (const approver of approvers) {
await this.selectListItemWithSearch(this.page, approver)
}
await this.textSelectApproversPopup.click({ force: true })
await this.buttonSelectMemberSubmit.click()
}
async checkCurrentRights (right: DocumentRights): Promise<void> {
await expect(this.buttonCurrentRights).toHaveText(right)
}
async addMessageToTheText (text: string, message: string, closePopup: boolean = true): Promise<void> {
await this.page.getByText(text).click()
await this.page.getByText(text).dblclick()
await this.buttonAddMessageToText.click()
await this.addMessage(message)
if (closePopup) {
await this.closeNewMessagePopup()
}
}
async addMessageToTheSectionTitle (title: string, message: string, closePopup: boolean = true): Promise<void> {
const locator = await this.getSectionLocator(title)
const parentLocator =
(await this.buttonCurrentRights.textContent()) === DocumentRights.EDITING ? '../../../../..' : '../..'
await locator.locator(`xpath=${parentLocator}`).hover()
await locator.locator(`xpath=${parentLocator}`).locator('div.tools button[type="button"]').click()
await this.addMessage(message)
if (closePopup) {
await this.closeNewMessagePopup()
}
}
async closeNewMessagePopup (): Promise<void> {
await this.textPageHeader.press('Escape', { delay: 300 })
await this.textPageHeader.click({ force: true, delay: 300, position: { x: 1, y: 1 } })
}
async completeReview (): Promise<void> {
await this.buttonCompleteReview.click()
await this.inputPassword.fill(PlatformPassword)
await this.buttonSubmit.click()
}
async confirmRejection (rejectionReason: string): Promise<void> {
await this.buttonReject.click()
await this.inputPassword.fill(PlatformPassword)
await this.inputRejectionReason.fill(rejectionReason)
await this.buttonSubmit.click()
}
async confirmApproval (): Promise<void> {
await this.buttonApprove.click()
await this.inputPassword.fill(PlatformPassword)
await this.buttonSubmit.click()
}
async addNewSection (startSectionId: string, direction: 'above' | 'below'): Promise<void> {
await this.page.locator('span.hdr-alignment:not([class*="label"])', { hasText: startSectionId }).hover()
await this.page
.locator('span.hdr-alignment:not([class*="label"])', { hasText: startSectionId })
.locator('xpath=../../..')
.locator('div.tools[draggable="true"]')
.click()
await this.selectFromDropdown(this.page, `Add new section ${direction}`)
}
async changeCurrentRight (newRight: DocumentRights): Promise<void> {
await this.buttonCurrentRights.click()
await this.selectMenuItem(this.page, newRight)
}
async checkComparingTextAdded (text: string): Promise<void> {
await expect(this.page.locator('span.text-editor-highlighted-node-add', { hasText: text }).first()).toBeVisible()
}
async checkComparingTextDeleted (text: string): Promise<void> {
await expect(this.page.locator('span.text-editor-highlighted-node-delete', { hasText: text }).first()).toBeVisible()
}
async openApprovals (): Promise<void> {
await expect(this.buttonDocumentApprovals).toBeVisible()
await this.buttonDocumentApprovals.click({ position: { x: 1, y: 1 }, force: true })
}
async fillChangeDocumentOwnerPopupByQaraManager (newOwner: string): Promise<void> {
await this.buttonSelectNewOwner.click()
await this.selectListItemWithSearch(this.page, newOwner)
await this.buttonSelectNewOwnerChangeByQaraManager.click()
}
private async getSectionLocator (title: string): Promise<Locator> {
let result: Locator | null = null
const sectionsLocator = iterateLocator(
(await this.buttonCurrentRights.textContent()) === DocumentRights.EDITING
? this.sectionsLocatorEditRight
: this.sectionsLocatorViewRight
)
for await (const locator of sectionsLocator) {
const value =
(await this.buttonCurrentRights.textContent()) === DocumentRights.EDITING
? await locator.inputValue()
: ((await locator.textContent()) ?? '').trim()
if (value === title) {
result = locator
break
}
}
if (result == null) {
throw new Error('Section not found')
}
return result
}
}

View File

@ -0,0 +1,15 @@
import { expect, type Page } from '@playwright/test'
import { DocumentCommonPage } from './document-common-page'
export class DocumentHistoryPage extends DocumentCommonPage {
readonly page: Page
constructor (page: Page) {
super(page)
this.page = page
}
async checkHistoryEventExist (eventDescription: string): Promise<void> {
await expect(this.page.locator('div.root div.description', { hasText: eventDescription })).toBeVisible()
}
}

View File

@ -0,0 +1,66 @@
import { expect, type Locator, type Page } from '@playwright/test'
import { DocumentCommonPage } from './document-common-page'
export class DocumentReasonAndImpactPage extends DocumentCommonPage {
readonly page: Page
readonly buttonReasonAndImpactTabSelected: Locator
readonly textAreaDescription: Locator
readonly textAreaReason: Locator
readonly textAreaImpactAnalysis: Locator
readonly buttonImpactedDocuments: Locator
readonly textDescription: Locator
readonly textReason: Locator
readonly textImpactAnalysis: Locator
readonly textImpactedDocuments: Locator
constructor (page: Page) {
super(page)
this.page = page
this.buttonReasonAndImpactTabSelected = page.locator('div.tab.selected', { hasText: 'Reason & Impact' })
this.textAreaDescription = page.locator('div.box textarea.root').first()
this.textAreaReason = page.locator('div.box textarea.root').nth(1)
this.textAreaImpactAnalysis = page.locator('div.box textarea.root').last()
this.buttonImpactedDocuments = page.locator('div.addButton')
this.textDescription = page.locator('//div[contains(@class, "title") and text()="Description"]/..')
this.textReason = page.locator('//div[contains(@class, "title") and text()="Reason"]/..')
this.textImpactAnalysis = page.locator('//div[contains(@class, "title") and text()="Impact analysis"]/..')
this.textImpactedDocuments = page.locator(
'//div[contains(@class, "title") and text()="Impacted documents"]/..//a/span'
)
}
async setReasonAndImpactData (
description: string,
reason: string,
analysis: string,
documentName: string
): Promise<void> {
await this.textAreaDescription.clear()
await this.textAreaDescription.fill(description)
await this.textAreaReason.clear()
await this.textAreaReason.fill(reason)
await this.textAreaImpactAnalysis.focus()
await this.textAreaImpactAnalysis.press('Meta+A')
await this.textAreaImpactAnalysis.press('Backspace')
await this.textAreaImpactAnalysis.fill(analysis)
await this.buttonImpactedDocuments.click()
await this.selectListItem(this.page, documentName)
await this.buttonReasonAndImpactTabSelected.click({ force: true })
}
async checkReasonAndImpactData (
description: string,
reason: string,
analysis: string,
documentName: string
): Promise<void> {
await expect(this.textDescription).toContainText(description)
await expect(this.textReason).toContainText(reason)
await expect(this.textImpactAnalysis).toContainText(analysis)
await expect(this.textImpactedDocuments).toHaveText(documentName)
}
}

View File

@ -0,0 +1,22 @@
import { type Locator, type Page } from '@playwright/test'
import { DocumentCommonPage } from './document-common-page'
export class DocumentReleasePage extends DocumentCommonPage {
readonly page: Page
readonly buttonReleaseTabSelected: Locator
readonly buttonMakeEffectiveOn: Locator
readonly buttonDateTimeButton: Locator
constructor (page: Page) {
super(page)
this.page = page
this.buttonReleaseTabSelected = page.locator('div.tab.selected', { hasText: 'Release' })
this.buttonMakeEffectiveOn = page.locator('label[for="documents:string:EffectiveOn"]')
this.buttonDateTimeButton = page.locator('label[for="documents:string:EffectiveOn"] button.datetime-button')
}
async setEffectiveDate (delayTime: string): Promise<void> {
await this.buttonDateTimeButton.click()
await this.fillSelectDateByShortcut(delayTime)
}
}

View File

@ -0,0 +1,70 @@
import { type Locator, type Page } from '@playwright/test'
import { CalendarPage } from '../calendar-page'
import { NewDocument } from '../types'
export class DocumentsPage extends CalendarPage {
readonly page: Page
readonly buttonCreateDocument: Locator
readonly buttonPopupNextStep: Locator
readonly buttonSpaceSelector: Locator
readonly buttonParentSelector: Locator
readonly inputNewDocumentTitle: Locator
readonly inputNewDocumentDescription: Locator
readonly inputNewDocumentCreateDaft: Locator
constructor (page: Page) {
super(page)
this.page = page
this.buttonCreateDocument = page.locator(
'div[data-float="navigator"] button[type="submit"]:not([class*="only-icon"])'
)
this.buttonPopupNextStep = page.locator('div.popup button[type="submit"]')
this.buttonSpaceSelector = page.locator('button[id="space.selector"]')
this.buttonParentSelector = page.locator('div.parentSelector span[class*="label"]')
this.inputNewDocumentTitle = page.locator('div[id="doc-title"] input')
this.inputNewDocumentDescription = page.locator('div[id="doc-description"] input')
this.inputNewDocumentCreateDaft = page.locator('div.footer div.footerButtons button[type="button"]')
}
async createDocument (data: NewDocument, startSecondStep: boolean = false): Promise<void> {
if (data.location != null) {
await this.buttonSpaceSelector.click()
await this.selectListItemWithSearch(this.page, data.location.space ?? '')
await this.page.locator('div.parentSelector span[class*="label"]', { hasText: data.location.parent }).click()
}
// template
if (!startSecondStep) {
await this.buttonPopupNextStep.click()
}
await this.page.locator('div.templates div.tmpHeader', { hasText: data.template }).click()
// title
await this.buttonPopupNextStep.click()
await this.inputNewDocumentTitle.fill(data.title)
await this.inputNewDocumentDescription.fill(data.description)
if (data.reason != null) {
await this.page.locator('div.radio label', { hasText: data.reason }).click()
}
// team
await this.buttonPopupNextStep.click()
await this.inputNewDocumentCreateDaft.click()
}
async openDocument (name: string): Promise<void> {
await this.page.locator('button.hulyNavItem-container > span[class*="label"]', { hasText: name }).click()
}
async executeMoreActionsOnDocument (documentName: string, action: string): Promise<void> {
await this.page.locator('button.hulyNavItem-container > span[class*="label"]', { hasText: documentName }).hover()
await this.page
.locator('button.hulyNavItem-container > span[class*="label"]', { hasText: documentName })
.locator('xpath=..')
.locator('div[class*="actions"]:not([class*="arrow"])')
.click()
await this.selectFromDropdown(this.page, action)
}
}

View File

@ -0,0 +1,13 @@
import { type Locator, type Page } from '@playwright/test'
export class NavigationMenuPage {
readonly page: Page
readonly buttonTemplates: Locator
readonly buttonCategories: Locator
constructor (page: Page) {
this.page = page
this.buttonTemplates = page.locator('a[href$="templates"]', { hasText: 'Templates' })
this.buttonCategories = page.locator('a[href$="categories"]', { hasText: 'Categories' })
}
}

View File

@ -0,0 +1,84 @@
import { type Locator, type Page } from '@playwright/test'
import { CalendarPage } from '../calendar-page'
import { NewTemplate } from '../types'
export class TemplatesPage extends CalendarPage {
readonly page: Page
readonly buttonCreateTemplate: Locator
readonly buttonSpaceSelector: Locator
readonly buttonPopupNextStep: Locator
readonly inputNewTemplateTitle: Locator
readonly inputNewTemplateDescription: Locator
readonly inputNewTemplateCreateDaft: Locator
readonly buttonNewTemplateCategory: Locator
readonly buttonNewTemplateCustomReason: Locator
readonly inputNewTemplateCustomReason: Locator
readonly sectionHighlightedTeam: Locator
readonly inputNewTemplateCode: Locator
constructor (page: Page) {
super(page)
this.page = page
this.buttonCreateTemplate = page.locator('div[slot="header-tools"] button[type="submit"]')
this.buttonSpaceSelector = page.locator('button[id="space.selector"]')
this.buttonPopupNextStep = page.locator('div.popup button[type="submit"]')
this.inputNewTemplateTitle = page.locator('div[id="doc-title"] input')
this.inputNewTemplateDescription = page.locator('div[id="doc-description"] input')
this.inputNewTemplateCreateDaft = page.locator('div.footer div.footerButtons button[type="button"]')
this.buttonNewTemplateCategory = page.locator('div.popup div.sectionContent button')
this.buttonNewTemplateCustomReason = page.locator('div.radio div.antiRadio:last-child')
this.inputNewTemplateCustomReason = page.locator('div.popup div[id="doc-reason"] input')
this.sectionHighlightedTeam = page.locator('div.popup div.highlighted', { hasText: 'Team' })
this.inputNewTemplateCode = page.locator('input[placeholder="DOC-1"]')
}
async createTemplate (data: NewTemplate): Promise<void> {
if (data.location?.space != null) {
await this.buttonSpaceSelector.click()
await this.selectMenuItem(this.page, data.location.space)
}
if (data.location?.parent != null) {
await this.page.locator('div.parentSelector span[class*="label"]', { hasText: data.location.parent }).click()
}
// title
await this.buttonPopupNextStep.click()
await this.inputNewTemplateTitle.fill(data.title)
await this.inputNewTemplateDescription.fill(data.description)
if (data.code != null) {
await this.inputNewTemplateCode.fill(data.code)
}
if (data.category != null) {
await this.buttonNewTemplateCategory.click()
await this.selectListItemWithSearch(this.page, data.category, true)
}
if (data.reason != null) {
await this.buttonNewTemplateCustomReason.click()
await this.inputNewTemplateCustomReason.fill(data.reason)
}
// team
await this.buttonPopupNextStep.click()
if (data.reviewers != null) {
await this.page.locator('div.addButton').nth(1).click()
for (const reviewer of data.reviewers) {
await this.selectListItemWithSearch(this.page, reviewer)
}
await this.sectionHighlightedTeam.click({ force: true })
}
if (data.approvers != null) {
await this.page.locator('div.addButton').last().click()
for (const approver of data.approvers) {
await this.selectListItemWithSearch(this.page, approver)
}
await this.sectionHighlightedTeam.click({ force: true })
}
await this.inputNewTemplateCreateDaft.click()
}
}

View File

@ -0,0 +1,15 @@
import { type Locator, type Page } from '@playwright/test'
export class LeftSideMenuPage {
readonly page: Page
readonly buttonPlanning: Locator
readonly buttonTeam: Locator
readonly buttonDocuments: Locator
constructor (page: Page) {
this.page = page
this.buttonPlanning = page.locator('button[id$="Planning"]')
this.buttonTeam = page.locator('button[id$="Team"]')
this.buttonDocuments = page.locator('button[id$="documents:string:DocumentApplication"]')
}
}

View File

@ -0,0 +1,29 @@
import { expect, type Locator, type Page } from '@playwright/test'
import { PlatformURI } from '../utils'
export class LoginPage {
readonly page: Page
readonly inputEmail: Locator
readonly inputPassword: Locator
readonly buttonLogin: Locator
readonly buttonSignUp: Locator
constructor (page: Page) {
this.page = page
this.inputEmail = page.locator('input[name=email]')
this.inputPassword = page.locator('input[name=current-password]')
this.buttonLogin = page.locator('button', { hasText: 'Log In' })
this.buttonSignUp = page.locator('a.title', { hasText: 'Sign Up' })
}
async goto (): Promise<void> {
await (await this.page.goto(`${PlatformURI}/login/login`))?.finished()
}
async login (email: string, password: string): Promise<void> {
await this.inputEmail.fill(email)
await this.inputPassword.fill(password)
expect(await this.buttonLogin.isEnabled()).toBe(true)
await this.buttonLogin.click()
}
}

View File

@ -0,0 +1,25 @@
import { expect, type Locator, type Page } from '@playwright/test'
export class SelectWorkspacePage {
readonly page: Page
readonly buttonWorkspace: Locator
readonly buttonCreateWorkspace: Locator
readonly inputWorkspaceName: Locator
constructor (page: Page) {
this.page = page
this.buttonWorkspace = page.locator('div[class*="workspace"]')
this.buttonCreateWorkspace = page.locator('button > span', { hasText: 'Create workspace' })
this.inputWorkspaceName = page.locator('div.form input')
}
async selectWorkspace (workspace: string): Promise<void> {
await this.buttonWorkspace.filter({ hasText: workspace }).click()
}
async createWorkspace (workspaceName: string): Promise<void> {
await this.buttonCreateWorkspace.waitFor({ state: 'visible' })
await expect(this.inputWorkspaceName).toBeVisible()
await this.inputWorkspaceName.fill(workspaceName)
}
}

View File

@ -0,0 +1,32 @@
import { type Locator, type Page } from '@playwright/test'
import { UserSignUp } from './types'
export class SignupPage {
readonly page: Page
readonly inputFirstName: Locator
readonly inputLastName: Locator
readonly inputEmail: Locator
readonly inputNewPassword: Locator
readonly inputRepeatNewPassword: Locator
readonly buttonSignUp: Locator
readonly textError: Locator
constructor (page: Page) {
this.page = page
this.inputFirstName = page.locator('input[name="given-name"]')
this.inputLastName = page.locator('input[name="family-name"]')
this.inputEmail = page.locator('input[name="email"]')
this.inputNewPassword = page.locator('input[name="new-password"]').nth(0)
this.inputRepeatNewPassword = page.locator('input[name="new-password"]').nth(1)
this.buttonSignUp = page.locator('div.send button')
this.textError = page.locator('div.ERROR > span')
}
async signup (userData: UserSignUp): Promise<void> {
await this.inputFirstName.fill(userData.firstName)
await this.inputLastName.fill(userData.lastName)
await this.inputEmail.fill(userData.email)
await this.inputNewPassword.fill(userData.password)
await this.inputRepeatNewPassword.fill(userData.password)
}
}

View File

@ -0,0 +1,73 @@
export interface NewDocument {
location?: Location
template: string
title: string
description: string
reason?: string
reviewers?: Array<string>
approvers?: Array<string>
}
export interface Location {
space?: string
parent?: string
}
export interface Content {
sectionTitle: string
content: string
}
export interface UserSignUp {
firstName: string
lastName: string
email: string
password: string
}
export enum DocumentStatus {
DELETED = 'Deleted',
DRAFT = 'Draft',
IN_REVIEW = 'In Review',
IN_APPROVAL = 'In Approval',
REVIEWED = 'Reviewed',
REJECTED = 'Rejected',
APPROVED = 'Approved',
EFFECTIVE = 'Effective'
}
export interface DocumentDetails {
type: string
category: string
version: string
status: string
owner: string
author: string
id?: string
}
export enum DocumentRights {
VIEWING = 'Viewing',
EDITING = 'Editing',
COMPARING = 'Comparing'
}
export interface NewTemplate {
location?: Location
title: string
description: string
code?: string
category?: string
reason?: string
reviewers?: Array<string>
approvers?: Array<string>
}
export interface NewCategory extends UpdateCategory {
title: string
code: string
}
export interface UpdateCategory {
description: string
attachFileName?: string
}

View File

@ -0,0 +1,46 @@
import { devices, PlaywrightTestConfig } from '@playwright/test'
import { config as dotenvConfig } from 'dotenv'
dotenvConfig()
let maxFailures: number | undefined
if (process.env.TESTS_MAX_FAILURES !== undefined) {
maxFailures = parseInt(process.env.TESTS_MAX_FAILURES)
}
const config: PlaywrightTestConfig = {
projects: [
{
name: 'QMS',
use: {
permissions: ['clipboard-read', 'clipboard-write'],
...devices['Desktop Chrome'],
screenshot: 'only-on-failure',
viewport: {
width: 1440,
height: 900
},
trace: {
mode: 'retain-on-failure',
snapshots: true,
screenshots: true,
sources: true
}
}
}
],
retries: 1,
timeout: 60000,
maxFailures,
reporter: [
['list'],
['html'],
[
'allure-playwright',
{
detail: false,
suiteTitle: false
}
]
]
}
export default config

View File

@ -0,0 +1,75 @@
import { Browser, Locator, Page } from '@playwright/test'
import { allure } from 'allure-playwright'
export const PlatformURI = process.env.PLATFORM_URI as string
export const PlatformTransactor = process.env.PLATFORM_TRANSACTOR as string
export const PlatformUser = process.env.PLATFORM_USER as string
export const PlatformToken = process.env.PLATFORM_TOKEN as string
export const PlatformSetting = process.env.SETTING as string
export const PlatformSettingSecond = process.env.SETTING_SECOND as string
export const PlatformSettingQaraManager = process.env.SETTING_QARA_MANAGER as string
export const PlatformPassword = process.env.PLATFORM_PASSWORD as string
export const DefaultWorkspace = 'sanity-ws-qms'
export const DocumentURI = `workbench/${DefaultWorkspace}/documents`
export const HomepageURI = `workbench/${DefaultWorkspace}`
function toHex (value: number, chars: number): string {
const result = value.toString(16)
if (result.length < chars) {
return '0'.repeat(chars - result.length) + result
}
return result
}
let counter = 0
const random = toHex((Math.random() * (1 << 24)) | 0, 6) + toHex((Math.random() * (1 << 16)) | 0, 4)
function timestamp (): string {
const time = (Date.now() / 1000) | 0
return toHex(time, 8)
}
function count (): string {
const val = counter++ & 0xffffff
return toHex(val, 6)
}
/**
* @public
* @returns
*/
export function generateId (len = 100): string {
const v = timestamp() + random
let s = v.length - len
if (s < 0) {
s = 0
}
return v.slice(s, v.length) + count()
}
export async function getSecondPage (browser: Browser): Promise<Page> {
const userSecondContext = await browser.newContext({ storageState: PlatformSettingSecond })
return await userSecondContext.newPage()
}
export function randomString (): string {
return (Math.random() * 1000000).toString(36).replace('.', '')
}
export async function attachScreenshot (name: string, page: Page): Promise<void> {
await allure.attachment(name, await page.screenshot(), {
contentType: 'image/png'
})
await page.screenshot({ path: `screenshots/${name}` })
}
export async function getQaraManagerPage (browser: Browser): Promise<Page> {
const qaraManagerContext = await browser.newContext({ storageState: PlatformSettingQaraManager })
return await qaraManagerContext.newPage()
}
export async function * iterateLocator (locator: Locator): AsyncGenerator<Locator> {
for (let index = 0; index < (await locator.count()); index++) {
yield locator.nth(index)
}
}

View File

@ -0,0 +1,9 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./tests",
"outDir": "./lib",
"declarationDir": "./types",
"lib": ["esnext", "dom"]
}
}

12
qms-tests/tool.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
export MINIO_ACCESS_KEY=minioadmin
export MINIO_SECRET_KEY=minioadmin
export MINIO_ENDPOINT=localhost:9002
export MONGO_URL=mongodb://localhost:27018
export TRANSACTOR_URL=ws://localhost:3334
export ELASTIC_URL=http://localhost:9201
export SERVER_SECRET=secret
export STORAGE_CONFIG="minio|localhost:9002?accessKey=minioadmin&secretKey=minioadmin"
node ../dev/tool/bundle/bundle.js $@

5
qms-tests/update-snapshot.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
location="${1:-./sanity-ws-qms}"
#backup
./tool.sh backup ${location} sanity-ws-qms

View File

@ -1056,6 +1056,11 @@
"projectFolder": "tests/sanity",
"shouldPublish": false
},
{
"packageName": "@hcengineering/qms-tests-sanity",
"projectFolder": "qms-tests/sanity",
"shouldPublish": false
},
{
"packageName": "@hcengineering/rekoni",
"projectFolder": "packages/rekoni",