Merge branch 'develop' into user-permission-not-saved-fix
This commit is contained in:
commit
1d03424e26
1998 changed files with 65265 additions and 155568 deletions
|
|
@ -9,6 +9,6 @@ trim_trailing_whitespace = true
|
|||
charset = utf-8
|
||||
|
||||
# python, js indentation settings
|
||||
[{*.py,*.js}]
|
||||
[{*.py,*.js,*.vue}]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@
|
|||
"validate_email": true,
|
||||
"validate_name": true,
|
||||
"validate_phone": true,
|
||||
"validate_url": true,
|
||||
"get_number_format": true,
|
||||
"format_number": true,
|
||||
"format_currency": true,
|
||||
|
|
@ -135,7 +136,6 @@
|
|||
"PhotoSwipeUI_Default": true,
|
||||
"fluxify": true,
|
||||
"io": true,
|
||||
"QUnit": true,
|
||||
"JsBarcode": true,
|
||||
"L": true,
|
||||
"Chart": true,
|
||||
|
|
@ -143,11 +143,14 @@
|
|||
"Cypress": true,
|
||||
"cy": true,
|
||||
"it": true,
|
||||
"describe": true,
|
||||
"expect": true,
|
||||
"context": true,
|
||||
"before": true,
|
||||
"beforeEach": true,
|
||||
"after": true,
|
||||
"qz": true,
|
||||
"localforage": true
|
||||
"localforage": true,
|
||||
"extend_cscript": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
33
.flake8
Normal file
33
.flake8
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
[flake8]
|
||||
ignore =
|
||||
E121,
|
||||
E126,
|
||||
E127,
|
||||
E128,
|
||||
E203,
|
||||
E225,
|
||||
E226,
|
||||
E231,
|
||||
E241,
|
||||
E251,
|
||||
E261,
|
||||
E265,
|
||||
E302,
|
||||
E303,
|
||||
E305,
|
||||
E402,
|
||||
E501,
|
||||
E741,
|
||||
W291,
|
||||
W292,
|
||||
W293,
|
||||
W391,
|
||||
W503,
|
||||
W504,
|
||||
F403,
|
||||
B007,
|
||||
B950,
|
||||
W191,
|
||||
|
||||
max-line-length = 200
|
||||
exclude=.github/helper/semgrep_rules
|
||||
21
.git-blame-ignore-revs
Normal file
21
.git-blame-ignore-revs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Since version 2.23 (released in August 2019), git-blame has a feature
|
||||
# to ignore or bypass certain commits.
|
||||
#
|
||||
# This file contains a list of commits that are not likely what you
|
||||
# are looking for in a blame, such as mass reformatting or renaming.
|
||||
# You can set this file as a default ignore file for blame by running
|
||||
# the following command.
|
||||
#
|
||||
# $ git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||
|
||||
# Replace use of Class.extend with native JS class
|
||||
fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85
|
||||
|
||||
# Updating license headers
|
||||
34460265554242a8d05fb09f049033b1117e1a2b
|
||||
|
||||
# Refactor "not a in b" -> "a not in b"
|
||||
745297a49d516e5e3c4bb3e1b0c4235e7d31165d
|
||||
|
||||
# Clean up whitespace
|
||||
b2fc959307c7c79f5584625569d5aed04133ba13
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"db_host": "localhost",
|
||||
"db_host": "127.0.0.1",
|
||||
"db_port": 3306,
|
||||
"db_name": "test_frappe_consumer",
|
||||
"db_password": "test_frappe",
|
||||
"allow_tests": true,
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"db_host": "localhost",
|
||||
"db_host": "127.0.0.1",
|
||||
"db_port": 5432,
|
||||
"db_name": "test_frappe_consumer",
|
||||
"db_password": "test_frappe",
|
||||
"db_type": "postgres",
|
||||
8
.github/helper/documentation.py
vendored
8
.github/helper/documentation.py
vendored
|
|
@ -24,6 +24,8 @@ def docs_link_exists(body):
|
|||
parts = parsed_url.path.split('/')
|
||||
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
|
||||
return True
|
||||
if parsed_url.netloc in ["docs.erpnext.com", "frappeframework.com"]:
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
@ -32,9 +34,9 @@ if __name__ == "__main__":
|
|||
|
||||
if response.ok:
|
||||
payload = response.json()
|
||||
title = payload.get("title", "").lower()
|
||||
head_sha = payload.get("head", {}).get("sha")
|
||||
body = payload.get("body", "").lower()
|
||||
title = (payload.get("title") or "").lower()
|
||||
head_sha = (payload.get("head") or {}).get("sha")
|
||||
body = (payload.get("body") or "").lower()
|
||||
|
||||
if title.startswith("feat") and head_sha and "no-docs" not in body:
|
||||
if docs_link_exists(body):
|
||||
|
|
|
|||
65
.github/helper/install.sh
vendored
Normal file
65
.github/helper/install.sh
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
cd ~ || exit
|
||||
|
||||
pip install frappe-bench
|
||||
|
||||
bench init frappe-bench --skip-assets --python "$(which python)" --frappe-path "${GITHUB_WORKSPACE}"
|
||||
|
||||
mkdir ~/frappe-bench/sites/test_site
|
||||
cp "${GITHUB_WORKSPACE}/.github/helper/consumer_db/$DB.json" ~/frappe-bench/sites/test_site/site_config.json
|
||||
|
||||
if [ "$TYPE" == "server" ]; then
|
||||
mkdir ~/frappe-bench/sites/test_site_producer;
|
||||
cp "${GITHUB_WORKSPACE}/.github/helper/producer_db/$DB.json" ~/frappe-bench/sites/test_site_producer/site_config.json;
|
||||
fi
|
||||
|
||||
if [ "$DB" == "mariadb" ];then
|
||||
sudo apt update && sudo apt install mariadb-client-10.3
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'";
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'";
|
||||
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe_consumer";
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe_consumer'@'localhost' IDENTIFIED BY 'test_frappe_consumer'";
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe_consumer\`.* TO 'test_frappe_consumer'@'localhost'";
|
||||
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe_producer";
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe_producer'@'localhost' IDENTIFIED BY 'test_frappe_producer'";
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe_producer\`.* TO 'test_frappe_producer'@'localhost'";
|
||||
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'";
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES";
|
||||
fi
|
||||
|
||||
if [ "$DB" == "postgres" ];then
|
||||
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe_consumer" -U postgres;
|
||||
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe_consumer WITH PASSWORD 'test_frappe'" -U postgres;
|
||||
|
||||
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe_producer" -U postgres;
|
||||
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe_producer WITH PASSWORD 'test_frappe'" -U postgres;
|
||||
fi
|
||||
|
||||
cd ./frappe-bench || exit
|
||||
|
||||
sed -i 's/^watch:/# watch:/g' Procfile
|
||||
sed -i 's/^schedule:/# schedule:/g' Procfile
|
||||
|
||||
if [ "$TYPE" == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; fi
|
||||
if [ "$TYPE" == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi
|
||||
|
||||
if [ "$TYPE" == "ui" ]; then bench setup requirements --node; fi
|
||||
bench setup requirements --dev
|
||||
|
||||
if [ "$TYPE" == "ui" ]; then sed -i 's/^web: bench serve/web: bench serve --with-coverage/g' Procfile; fi
|
||||
|
||||
# install node-sass which is required for website theme test
|
||||
cd ./apps/frappe || exit
|
||||
yarn add node-sass@4.13.1
|
||||
cd ../..
|
||||
|
||||
bench start &
|
||||
bench --site test_site reinstall --yes
|
||||
if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi
|
||||
if [ "$TYPE" == "server" ]; then CI=Yes bench build --app frappe; fi
|
||||
16
.github/helper/install_dependencies.sh
vendored
Normal file
16
.github/helper/install_dependencies.sh
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# install wkhtmltopdf
|
||||
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
|
||||
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
|
||||
sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
|
||||
sudo chmod o+x /usr/local/bin/wkhtmltopdf
|
||||
|
||||
# install cups
|
||||
sudo apt-get install libcups2-dev
|
||||
|
||||
# install redis
|
||||
sudo apt-get install redis-server
|
||||
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"db_host": "localhost",
|
||||
"db_host": "127.0.0.1",
|
||||
"db_port": 3306,
|
||||
"db_name": "test_frappe_producer",
|
||||
"db_password": "test_frappe",
|
||||
"allow_tests": true,
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"db_host": "localhost",
|
||||
"db_host": "127.0.0.1",
|
||||
"db_port": 5432,
|
||||
"db_name": "test_frappe_producer",
|
||||
"db_password": "test_frappe",
|
||||
"db_type": "postgres",
|
||||
78
.github/helper/roulette.py
vendored
Normal file
78
.github/helper/roulette.py
vendored
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
|
||||
def get_files_list(pr_number, repo="frappe/frappe"):
|
||||
req = urllib.request.Request(f"https://api.github.com/repos/{repo}/pulls/{pr_number}/files")
|
||||
res = urllib.request.urlopen(req)
|
||||
dump = json.loads(res.read().decode('utf8'))
|
||||
return [change["filename"] for change in dump]
|
||||
|
||||
def get_output(command, shell=True):
|
||||
print(command)
|
||||
command = shlex.split(command)
|
||||
return subprocess.check_output(command, shell=shell, encoding="utf8").strip()
|
||||
|
||||
def is_py(file):
|
||||
return file.endswith("py")
|
||||
|
||||
def is_ci(file):
|
||||
return ".github" in file
|
||||
|
||||
def is_frontend_code(file):
|
||||
return file.lower().endswith((".css", ".scss", ".less", ".sass", ".styl", ".js", ".ts", ".vue"))
|
||||
|
||||
def is_docs(file):
|
||||
regex = re.compile(r'\.(md|png|jpg|jpeg|csv)$|^.github|LICENSE')
|
||||
return bool(regex.search(file))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
files_list = sys.argv[1:]
|
||||
build_type = os.environ.get("TYPE")
|
||||
pr_number = os.environ.get("PR_NUMBER")
|
||||
repo = os.environ.get("REPO_NAME")
|
||||
|
||||
# this is a push build, run all builds
|
||||
if not pr_number:
|
||||
os.system('echo "::set-output name=build::strawberry"')
|
||||
os.system('echo "::set-output name=build-server::strawberry"')
|
||||
sys.exit(0)
|
||||
|
||||
files_list = files_list or get_files_list(pr_number=pr_number, repo=repo)
|
||||
|
||||
if not files_list:
|
||||
print("No files' changes detected. Build is shutting")
|
||||
sys.exit(0)
|
||||
|
||||
ci_files_changed = any(f for f in files_list if is_ci(f))
|
||||
only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
|
||||
only_frontend_code_changed = len(list(filter(is_frontend_code, files_list))) == len(files_list)
|
||||
updated_py_file_count = len(list(filter(is_py, files_list)))
|
||||
only_py_changed = updated_py_file_count == len(files_list)
|
||||
|
||||
if ci_files_changed:
|
||||
print("CI related files were updated, running all build processes.")
|
||||
|
||||
elif only_docs_changed:
|
||||
print("Only docs were updated, stopping build process.")
|
||||
sys.exit(0)
|
||||
|
||||
elif only_frontend_code_changed and build_type == "server":
|
||||
print("Only Frontend code was updated; Stopping Python build process.")
|
||||
sys.exit(0)
|
||||
|
||||
elif build_type == "ui":
|
||||
if only_py_changed:
|
||||
print("Only Python code was updated, stopping Cypress build process.")
|
||||
sys.exit(0)
|
||||
elif updated_py_file_count > 0:
|
||||
# both frontend and backend code were updated
|
||||
os.system('echo "::set-output name=build-server::strawberry"')
|
||||
|
||||
os.system('echo "::set-output name=build::strawberry"')
|
||||
17
.github/semantic.yml
vendored
17
.github/semantic.yml
vendored
|
|
@ -11,3 +11,20 @@ allowRevertCommits: true
|
|||
|
||||
# For allowed PR types: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json
|
||||
# Tool Reference: https://github.com/zeke/semantic-pull-requests
|
||||
|
||||
# By default types specified in commitizen/conventional-commit-types is used.
|
||||
# See: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json
|
||||
# You can override the valid types
|
||||
types:
|
||||
- BREAKING CHANGE
|
||||
- feat
|
||||
- fix
|
||||
- docs
|
||||
- style
|
||||
- refactor
|
||||
- perf
|
||||
- test
|
||||
- build
|
||||
- ci
|
||||
- chore
|
||||
- revert
|
||||
|
|
|
|||
4
.github/stale.yml
vendored
4
.github/stale.yml
vendored
|
|
@ -1,7 +1,7 @@
|
|||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 10
|
||||
daysUntilStale: 7
|
||||
|
||||
# Number of days of inactivity before a stale Issue or Pull Request is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
|
|
@ -28,7 +28,7 @@ markComment: >
|
|||
you can always reopen the PR when you're ready. Thank you for contributing.
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 30
|
||||
limitPerRun: 10
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
only: pulls
|
||||
|
|
|
|||
32
.github/try-on-f-cloud-button.svg
vendored
Normal file
32
.github/try-on-f-cloud-button.svg
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<svg width="201" height="60" viewBox="0 0 201 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_dd)">
|
||||
<rect x="4" y="2" width="193" height="52" rx="6" fill="#2490EF"/>
|
||||
<path d="M28 22.2891H32.8786V35.5H36.2088V22.2891H41.0874V19.5H28V22.2891Z" fill="white"/>
|
||||
<path d="M41.6982 35.5H45.0129V28.7109C45.0129 27.2344 46.0866 26.2188 47.5494 26.2188C48.0085 26.2188 48.6388 26.2969 48.95 26.3984V23.4453C48.6543 23.375 48.2419 23.3281 47.9074 23.3281C46.5691 23.3281 45.472 24.1094 45.0362 25.5938H44.9117V23.5H41.6982V35.5Z" fill="white"/>
|
||||
<path d="M52.8331 40C55.2996 40 56.6068 38.7344 57.2837 36.7969L61.9289 23.5156L58.4197 23.5L55.9221 32.3125H55.7976L53.3233 23.5H49.8374L54.1247 35.8437L53.9302 36.3516C53.4944 37.4766 52.6619 37.5312 51.4947 37.1719L50.7478 39.6562C51.2224 39.8594 51.9927 40 52.8331 40Z" fill="white"/>
|
||||
<path d="M73.6142 35.7344C77.2401 35.7344 79.4966 33.2422 79.4966 29.5469C79.4966 25.8281 77.2401 23.3438 73.6142 23.3438C69.9883 23.3438 67.7319 25.8281 67.7319 29.5469C67.7319 33.2422 69.9883 35.7344 73.6142 35.7344ZM73.6298 33.1562C71.9569 33.1562 71.101 31.6171 71.101 29.5233C71.101 27.4296 71.9569 25.8827 73.6298 25.8827C75.2715 25.8827 76.1274 27.4296 76.1274 29.5233C76.1274 31.6171 75.2715 33.1562 73.6298 33.1562Z" fill="white"/>
|
||||
<path d="M84.7253 28.5625C84.7331 27.0156 85.6512 26.1094 86.9895 26.1094C88.3201 26.1094 89.1215 26.9844 89.1137 28.4531V35.5H92.4284V27.8594C92.4284 25.0625 90.7945 23.3438 88.3046 23.3438C86.5306 23.3438 85.2466 24.2187 84.7097 25.6172H84.5697V23.5H81.4106V35.5H84.7253V28.5625Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M102.429 19.5H113.429V22.3141H102.429V19.5ZM102.429 35.5V26.6794H112.699V29.4982H105.94V35.5H102.429Z" fill="white"/>
|
||||
<path d="M131.584 24.9625C131.09 21.5057 128.345 19.5 124.785 19.5C120.589 19.5 117.429 22.463 117.429 27.4924C117.429 32.5142 120.55 35.4848 124.785 35.4848C128.604 35.4848 131.137 33.0916 131.584 30.1211L128.651 30.1059C128.282 31.9293 126.745 32.9549 124.824 32.9549C122.22 32.9549 120.354 31.0632 120.354 27.4924C120.354 23.9824 122.204 22.0299 124.832 22.0299C126.784 22.0299 128.314 23.1011 128.651 24.9625H131.584Z" fill="white"/>
|
||||
<path d="M136.409 19.7124H133.571V35.2718H136.409V19.7124Z" fill="white"/>
|
||||
<path d="M144.031 35.5001C147.56 35.5001 149.803 33.0917 149.803 29.483C149.803 25.8667 147.56 23.4507 144.031 23.4507C140.502 23.4507 138.259 25.8667 138.259 29.483C138.259 33.0917 140.502 35.5001 144.031 35.5001ZM144.047 33.2969C142.094 33.2969 141.137 31.6103 141.137 29.4754C141.137 27.3406 142.094 25.6312 144.047 25.6312C145.968 25.6312 146.925 27.3406 146.925 29.4754C146.925 31.6103 145.968 33.2969 144.047 33.2969Z" fill="white"/>
|
||||
<path d="M159.338 30.3641C159.338 32.1419 158.028 33.0232 156.773 33.0232C155.409 33.0232 154.499 32.0887 154.499 30.6072V23.6025H151.66V31.0327C151.66 33.8361 153.307 35.4239 155.675 35.4239C157.479 35.4239 158.749 34.5046 159.298 33.1979H159.424V35.272H162.176V23.6025H159.338V30.3641Z" fill="white"/>
|
||||
<path d="M169.014 35.4769C171.084 35.4769 172.017 34.2841 172.464 33.4332H172.637V35.2718H175.429V19.7124H172.582V25.532H172.464C172.033 24.6887 171.147 23.4503 169.022 23.4503C166.238 23.4503 164.05 25.5624 164.05 29.4522C164.05 33.2965 166.175 35.4769 169.014 35.4769ZM169.806 33.2205C167.931 33.2205 166.943 31.6251 166.943 29.437C166.943 27.2642 167.916 25.7067 169.806 25.7067C171.633 25.7067 172.637 27.173 172.637 29.437C172.637 31.701 171.617 33.2205 169.806 33.2205Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd" x="0" y="0" width="201" height="60" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.13 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
2
.github/workflows/docker-release.yml
vendored
2
.github/workflows/docker-release.yml
vendored
|
|
@ -12,4 +12,4 @@ jobs:
|
|||
- name: curl
|
||||
run: |
|
||||
apk add curl bash
|
||||
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token ${{ secrets.TRAVIS_CI_TOKEN }}" -d '{"request":{"branch":"master"}}' https://api.travis-ci.com/repo/frappe%2Ffrappe_docker/requests
|
||||
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer ${{ secrets.CI_PAT }}" https://api.github.com/repos/frappe/frappe_docker/actions/workflows/build_stable.yml/dispatches -d '{"ref":"main"}'
|
||||
|
|
|
|||
2
.github/workflows/docs-checker.yml
vendored
2
.github/workflows/docs-checker.yml
vendored
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
- name: 'Setup Environment'
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.6
|
||||
python-version: 3.8
|
||||
|
||||
- name: 'Clone repo'
|
||||
uses: actions/checkout@v2
|
||||
|
|
|
|||
31
.github/workflows/linters.yml
vendored
Normal file
31
.github/workflows/linters.yml
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
name: Linters
|
||||
|
||||
on:
|
||||
pull_request: { }
|
||||
|
||||
jobs:
|
||||
|
||||
linters:
|
||||
name: Frappe Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
uses: pre-commit/action@v2.0.3
|
||||
|
||||
- name: Download Semgrep rules
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
- uses: returntocorp/semgrep-action@v1
|
||||
env:
|
||||
SEMGREP_TIMEOUT: 120
|
||||
with:
|
||||
config: >-
|
||||
r/python.lang.correctness
|
||||
./frappe-semgrep-rules/rules
|
||||
131
.github/workflows/patch-mariadb-tests.yml
vendored
Normal file
131
.github/workflows/patch-mariadb-tests.yml
vendored
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
name: Patch
|
||||
|
||||
on: [pull_request, workflow_dispatch]
|
||||
|
||||
|
||||
concurrency:
|
||||
group: patch-mariadb-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
name: Patch Test
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mariadb:10.3
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: YES
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
check-latest: true
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
env:
|
||||
TYPE: "server"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
|
||||
- name: Add to Hosts
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
|
||||
TYPE: server
|
||||
|
||||
- name: Install
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: server
|
||||
|
||||
- name: Run Patch Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: |
|
||||
cd ~/frappe-bench/
|
||||
wget https://frappeframework.com/files/v10-frappe.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/v10-frappe.sql.gz
|
||||
|
||||
source env/bin/activate
|
||||
cd apps/frappe/
|
||||
git remote set-url upstream https://github.com/frappe/frappe.git
|
||||
|
||||
for version in $(seq 12 13)
|
||||
do
|
||||
echo "Updating to v$version"
|
||||
branch_name="version-$version-hotfix"
|
||||
git fetch --depth 1 upstream $branch_name:$branch_name
|
||||
|
||||
git checkout -q -f $branch_name
|
||||
pip install -q -r requirements.txt
|
||||
bench --site test_site migrate
|
||||
done
|
||||
|
||||
echo "Updating to last commit"
|
||||
git checkout -q -f "$GITHUB_SHA"
|
||||
bench setup requirements --python
|
||||
bench --site test_site migrate
|
||||
8
.github/workflows/publish-assets-develop.yml
vendored
8
.github/workflows/publish-assets-develop.yml
vendored
|
|
@ -15,11 +15,11 @@ jobs:
|
|||
path: 'frappe'
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
python-version: '12.x'
|
||||
node-version: 14
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.6'
|
||||
- name: Set up bench for current push
|
||||
python-version: '3.9'
|
||||
- name: Set up bench and build assets
|
||||
run: |
|
||||
npm install -g yarn
|
||||
pip3 install -U frappe-bench
|
||||
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
- name: Package assets
|
||||
run: |
|
||||
mkdir -p $GITHUB_WORKSPACE/build
|
||||
tar -cvpzf $GITHUB_WORKSPACE/build/$GITHUB_SHA.tar.gz ./frappe-bench/sites/assets/js ./frappe-bench/sites/assets/css
|
||||
tar -cvpzf $GITHUB_WORKSPACE/build/$GITHUB_SHA.tar.gz ./frappe-bench/sites/assets/frappe/dist
|
||||
|
||||
- name: Publish assets to S3
|
||||
uses: jakejarvis/s3-sync-action@master
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ jobs:
|
|||
python-version: '12.x'
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.6'
|
||||
- name: Set up bench for current push
|
||||
python-version: '3.9'
|
||||
- name: Set up bench and build assets
|
||||
run: |
|
||||
npm install -g yarn
|
||||
pip3 install -U frappe-bench
|
||||
|
|
@ -32,7 +32,7 @@ jobs:
|
|||
- name: Package assets
|
||||
run: |
|
||||
mkdir -p $GITHUB_WORKSPACE/build
|
||||
tar -cvpzf $GITHUB_WORKSPACE/build/assets.tar.gz ./frappe-bench/sites/assets/js ./frappe-bench/sites/assets/css
|
||||
tar -cvpzf $GITHUB_WORKSPACE/build/assets.tar.gz ./frappe-bench/sites/assets/frappe/dist
|
||||
|
||||
- name: Get release
|
||||
id: get_release
|
||||
|
|
|
|||
132
.github/workflows/server-mariadb-tests.yml
vendored
Normal file
132
.github/workflows/server-mariadb-tests.yml
vendored
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
name: Server
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
|
||||
concurrency:
|
||||
group: server-mariadb-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
container: [1, 2]
|
||||
|
||||
name: Python Unit Tests (MariaDB)
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mariadb:10.3
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: YES
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
env:
|
||||
TYPE: "server"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
with:
|
||||
node-version: 14
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: |
|
||||
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
|
||||
TYPE: server
|
||||
|
||||
- name: Install
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: server
|
||||
|
||||
- name: Run Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator --with-coverage
|
||||
env:
|
||||
CI_BUILD_ID: ${{ github.run_id }}
|
||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
||||
|
||||
- name: Upload coverage data
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
name: MariaDB
|
||||
fail_ci_if_error: true
|
||||
files: /home/runner/frappe-bench/sites/coverage.xml
|
||||
verbose: true
|
||||
flags: server
|
||||
135
.github/workflows/server-postgres-tests.yml
vendored
Normal file
135
.github/workflows/server-postgres-tests.yml
vendored
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
name: Server
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
|
||||
concurrency:
|
||||
group: server-postgres-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
container: [1, 2]
|
||||
|
||||
name: Python Unit Tests (Postgres)
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12.4
|
||||
env:
|
||||
POSTGRES_PASSWORD: travis
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
env:
|
||||
TYPE: "server"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
with:
|
||||
node-version: '14'
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: |
|
||||
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
|
||||
TYPE: server
|
||||
|
||||
- name: Install
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: postgres
|
||||
TYPE: server
|
||||
|
||||
- name: Run Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator --with-coverage
|
||||
env:
|
||||
CI_BUILD_ID: ${{ github.run_id }}
|
||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
||||
|
||||
- name: Upload coverage data
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
name: Postgres
|
||||
fail_ci_if_error: true
|
||||
files: /home/runner/frappe-bench/sites/coverage.xml
|
||||
verbose: true
|
||||
flags: server
|
||||
22
.github/workflows/translation_linter.yml
vendored
22
.github/workflows/translation_linter.yml
vendored
|
|
@ -1,22 +0,0 @@
|
|||
name: Frappe Linter
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- version-12-hotfix
|
||||
- version-11-hotfix
|
||||
jobs:
|
||||
check_translation:
|
||||
name: Translation Syntax Check
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup python3
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.6
|
||||
- name: Validating Translation Syntax
|
||||
run: |
|
||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||
python $GITHUB_WORKSPACE/.github/helper/translation.py $files
|
||||
174
.github/workflows/ui-tests.yml
vendored
Normal file
174
.github/workflows/ui-tests.yml
vendored
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
name: UI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
|
||||
concurrency:
|
||||
group: ui-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
containers: [1, 2]
|
||||
|
||||
name: UI Tests (Cypress)
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mariadb:10.3
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: YES
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
env:
|
||||
TYPE: "ui"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
with:
|
||||
node-version: 14
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: |
|
||||
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Cache cypress binary
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache
|
||||
key: ${{ runner.os }}-cypress-
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cypress-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
|
||||
TYPE: ui
|
||||
|
||||
- name: Install
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: ui
|
||||
|
||||
- name: Instrument Source Code
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/apps/frappe/ && npx nyc instrument -x 'frappe/public/dist/**' -x 'frappe/public/js/lib/**' -x '**/*.bundle.js' --compact=false --in-place frappe
|
||||
|
||||
- name: Build
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench build --apps frappe
|
||||
|
||||
- name: Site Setup
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site execute frappe.utils.install.complete_setup_wizard
|
||||
|
||||
- name: UI Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --with-coverage --headless --parallel --ci-build-id $GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb
|
||||
|
||||
- name: Stop server
|
||||
if: ${{ steps.check-build.outputs.build-server == 'strawberry' }}
|
||||
run: |
|
||||
ps -ef | grep "frappe serve" | awk '{print $2}' | xargs kill -s SIGINT 2> /dev/null || true
|
||||
sleep 5
|
||||
|
||||
- name: Check If Coverage Report Exists
|
||||
id: check_coverage
|
||||
uses: andstor/file-existence-action@v1
|
||||
with:
|
||||
files: "/home/runner/frappe-bench/apps/frappe/.cypress-coverage/clover.xml"
|
||||
|
||||
- name: Upload Coverage Data
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' && steps.check_coverage.outputs.files_exists == 'true' }}
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
name: Cypress
|
||||
fail_ci_if_error: true
|
||||
directory: /home/runner/frappe-bench/apps/frappe/.cypress-coverage/
|
||||
verbose: true
|
||||
flags: ui-tests
|
||||
|
||||
- name: Upload Server Coverage Data
|
||||
if: ${{ steps.check-build.outputs.build-server == 'strawberry' }}
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
name: MariaDB
|
||||
fail_ci_if_error: true
|
||||
files: /home/runner/frappe-bench/sites/coverage.xml
|
||||
verbose: true
|
||||
flags: server
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -9,7 +9,9 @@ locale
|
|||
dist/
|
||||
# build/
|
||||
frappe/docs/current
|
||||
frappe/public/dist
|
||||
.vscode
|
||||
.vs
|
||||
node_modules
|
||||
.kdev4/
|
||||
*.kdev4
|
||||
|
|
@ -66,6 +68,7 @@ coverage.xml
|
|||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
.cypress-coverage
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
|
|
|||
34
.mergify.yml
34
.mergify.yml
|
|
@ -1,9 +1,30 @@
|
|||
pull_request_rules:
|
||||
- name: Auto-close PRs on stable branch
|
||||
conditions:
|
||||
- and:
|
||||
- and:
|
||||
- author!=surajshetty3416
|
||||
- author!=gavindsouza
|
||||
- or:
|
||||
- base=version-13
|
||||
- base=version-12
|
||||
actions:
|
||||
close:
|
||||
comment:
|
||||
message: |
|
||||
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
|
||||
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
|
||||
|
||||
- name: Automatic merge on CI success and review
|
||||
conditions:
|
||||
- status-success=Sider
|
||||
- status-success=Semantic Pull Request
|
||||
- status-success=Travis CI - Pull Request
|
||||
- status-success=Python Unit Tests (MariaDB) (1)
|
||||
- status-success=Python Unit Tests (MariaDB) (2)
|
||||
- status-success=Python Unit Tests (Postgres) (1)
|
||||
- status-success=Python Unit Tests (Postgres) (2)
|
||||
- status-success=UI Tests (Cypress) (1)
|
||||
- status-success=UI Tests (Cypress) (2)
|
||||
- status-success=security/snyk (frappe)
|
||||
- label!=dont-merge
|
||||
- label!=squash
|
||||
|
|
@ -14,7 +35,12 @@ pull_request_rules:
|
|||
- name: Automatic squash on CI success and review
|
||||
conditions:
|
||||
- status-success=Sider
|
||||
- status-success=Travis CI - Pull Request
|
||||
- status-success=Python Unit Tests (MariaDB) (1)
|
||||
- status-success=Python Unit Tests (MariaDB) (2)
|
||||
- status-success=Python Unit Tests (Postgres) (1)
|
||||
- status-success=Python Unit Tests (Postgres) (2)
|
||||
- status-success=UI Tests (Cypress) (1)
|
||||
- status-success=UI Tests (Cypress) (2)
|
||||
- status-success=security/snyk (frappe)
|
||||
- label!=dont-merge
|
||||
- label=squash
|
||||
|
|
@ -22,3 +48,7 @@ pull_request_rules:
|
|||
actions:
|
||||
merge:
|
||||
method: squash
|
||||
commit_message_template: |
|
||||
{{ title }} (#{{ number }})
|
||||
|
||||
{{ body }}
|
||||
|
|
|
|||
23
.pre-commit-config.yaml
Normal file
23
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
exclude: 'node_modules|.git'
|
||||
default_stages: [commit]
|
||||
fail_fast: false
|
||||
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.0.1
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
files: "frappe.*"
|
||||
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
|
||||
- id: check-yaml
|
||||
- id: no-commit-to-branch
|
||||
args: ['--branch', 'develop']
|
||||
- id: check-merge-conflict
|
||||
- id: check-ast
|
||||
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: weekly
|
||||
skip: []
|
||||
submodules: false
|
||||
129
.travis.yml
129
.travis.yml
|
|
@ -1,129 +0,0 @@
|
|||
language: python
|
||||
dist: bionic
|
||||
|
||||
addons:
|
||||
hosts:
|
||||
- test_site
|
||||
- test_site_producer
|
||||
mariadb: 10.3
|
||||
postgresql: 9.5
|
||||
chrome: stable
|
||||
|
||||
services:
|
||||
- xvfb
|
||||
- mysql
|
||||
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
cache:
|
||||
pip: true
|
||||
npm: true
|
||||
yarn: true
|
||||
directories:
|
||||
# we also need to cache folder with Cypress binary
|
||||
# https://docs.cypress.io/guides/guides/continuous-integration.html#Caching
|
||||
- ~/.cache
|
||||
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- name: "Python 3.7 MariaDB"
|
||||
python: 3.7
|
||||
env: DB=mariadb TYPE=server
|
||||
script: bench --verbose --site test_site run-tests --coverage
|
||||
|
||||
- name: "Python 3.7 PostgreSQL"
|
||||
python: 3.7
|
||||
env: DB=postgres TYPE=server
|
||||
script: bench --verbose --site test_site run-tests --coverage
|
||||
|
||||
- name: "Cypress"
|
||||
python: 3.7
|
||||
env: DB=mariadb TYPE=ui
|
||||
before_script:
|
||||
- bench --site test_site execute frappe.utils.install.complete_setup_wizard
|
||||
script: bench --site test_site run-ui-tests frappe --headless
|
||||
|
||||
before_install:
|
||||
# do we really want to run travis?
|
||||
- |
|
||||
python ./.travis/roulette.py
|
||||
if [[ $? != 2 ]];then
|
||||
exit;
|
||||
fi
|
||||
|
||||
# install wkhtmltopdf
|
||||
- wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
|
||||
- tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
|
||||
- sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
|
||||
- sudo chmod o+x /usr/local/bin/wkhtmltopdf
|
||||
|
||||
# install cups
|
||||
- sudo apt-get install libcups2-dev
|
||||
|
||||
install:
|
||||
- cd ~
|
||||
- source ./.nvm/nvm.sh
|
||||
- nvm install 12
|
||||
|
||||
- pip install frappe-bench
|
||||
|
||||
- bench init frappe-bench --skip-assets --python $(which python) --frappe-path $TRAVIS_BUILD_DIR
|
||||
|
||||
- mkdir ~/frappe-bench/sites/test_site
|
||||
- cp $TRAVIS_BUILD_DIR/.travis/consumer_db/$DB.json ~/frappe-bench/sites/test_site/site_config.json
|
||||
|
||||
- if [ $TYPE == "server" ]; then
|
||||
mkdir ~/frappe-bench/sites/test_site_producer;
|
||||
cp $TRAVIS_BUILD_DIR/.travis/producer_db/$DB.json ~/frappe-bench/sites/test_site_producer/site_config.json;
|
||||
fi
|
||||
|
||||
- if [ $DB == "mariadb" ];then
|
||||
mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'";
|
||||
mysql -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'";
|
||||
|
||||
mysql -u root -e "CREATE DATABASE test_frappe_consumer";
|
||||
mysql -u root -e "CREATE USER 'test_frappe_consumer'@'localhost' IDENTIFIED BY 'test_frappe_consumer'";
|
||||
mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe_consumer\`.* TO 'test_frappe_consumer'@'localhost'";
|
||||
|
||||
mysql -u root -e "CREATE DATABASE test_frappe_producer";
|
||||
mysql -u root -e "CREATE USER 'test_frappe_producer'@'localhost' IDENTIFIED BY 'test_frappe_producer'";
|
||||
mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe_producer\`.* TO 'test_frappe_producer'@'localhost'";
|
||||
|
||||
mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'";
|
||||
mysql -u root -e "FLUSH PRIVILEGES";
|
||||
fi
|
||||
|
||||
- if [ $DB == "postgres" ];then
|
||||
psql -c "CREATE DATABASE test_frappe_consumer" -U postgres;
|
||||
psql -c "CREATE USER test_frappe_consumer WITH PASSWORD 'test_frappe'" -U postgres;
|
||||
|
||||
psql -c "CREATE DATABASE test_frappe_producer" -U postgres;
|
||||
psql -c "CREATE USER test_frappe_producer WITH PASSWORD 'test_frappe'" -U postgres;
|
||||
fi
|
||||
|
||||
- cd ./frappe-bench
|
||||
|
||||
- sed -i 's/^watch:/# watch:/g' Procfile
|
||||
- sed -i 's/^schedule:/# schedule:/g' Procfile
|
||||
|
||||
- if [ $TYPE == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; fi
|
||||
- if [ $TYPE == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi
|
||||
|
||||
- if [ $TYPE == "ui" ]; then bench setup requirements --node; fi
|
||||
|
||||
# install node-sass which is required for website theme test
|
||||
- cd ./apps/frappe
|
||||
- yarn add node-sass@4.13.1
|
||||
- cd ../..
|
||||
|
||||
- bench start &
|
||||
- bench --site test_site reinstall --yes
|
||||
- if [ $TYPE == "server" ]; then bench --site test_site_producer reinstall --yes; fi
|
||||
- bench build --app frappe
|
||||
|
||||
after_script:
|
||||
- pip install coverage==4.5.4
|
||||
- pip install python-coveralls
|
||||
- coveralls -b apps/frappe -d ../../sites/.coverage
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
# if the script ends with exit code 0, then no tests are run further, else all tests are run
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def get_output(command, shell=True):
|
||||
print(command)
|
||||
command = shlex.split(command)
|
||||
return subprocess.check_output(command, shell=shell, encoding="utf8").strip()
|
||||
|
||||
def is_py(file):
|
||||
return file.endswith("py")
|
||||
|
||||
def is_js(file):
|
||||
return file.endswith("js")
|
||||
|
||||
def is_docs(file):
|
||||
regex = re.compile('\.(md|png|jpg|jpeg)$|^.github|LICENSE')
|
||||
return bool(regex.search(file))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_type = os.environ.get("TYPE")
|
||||
commit_range = os.environ.get("TRAVIS_COMMIT_RANGE")
|
||||
print("Build Type: {}".format(build_type))
|
||||
print("Commit Range: {}".format(commit_range))
|
||||
|
||||
try:
|
||||
files_changed = get_output("git diff --name-only {}".format(commit_range), shell=False)
|
||||
except Exception:
|
||||
sys.exit(2)
|
||||
|
||||
if "fatal" not in files_changed:
|
||||
files_list = files_changed.split()
|
||||
only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
|
||||
only_js_changed = len(list(filter(is_js, files_list))) == len(files_list)
|
||||
only_py_changed = len(list(filter(is_py, files_list))) == len(files_list)
|
||||
|
||||
if only_docs_changed:
|
||||
print("Only docs were updated, stopping build process.")
|
||||
sys.exit(0)
|
||||
|
||||
if only_js_changed and build_type == "server":
|
||||
print("Only JavaScript code was updated; Stopping Python build process.")
|
||||
sys.exit(0)
|
||||
|
||||
if only_py_changed and build_type == "ui":
|
||||
print("Only Python code was updated, stopping Cypress build process.")
|
||||
sys.exit(0)
|
||||
|
||||
sys.exit(2)
|
||||
29
CODEOWNERS
29
CODEOWNERS
|
|
@ -3,17 +3,18 @@
|
|||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
* @frappe/frappe-review-team
|
||||
website/ @prssanna
|
||||
web_form/ @prssanna
|
||||
templates/ @surajshetty3416
|
||||
www/ @surajshetty3416
|
||||
integrations/ @nextchamp-saqib
|
||||
patches/ @surajshetty3416
|
||||
dashboard/ @prssanna
|
||||
email/ @saurabh6790
|
||||
event_streaming/ @ruchamahabal
|
||||
data_import* @netchampfaris
|
||||
core/ @surajshetty3416
|
||||
requirements.txt @gavindsouza
|
||||
commands/ @gavindsouza
|
||||
* @frappe/frappe-review-team
|
||||
templates/ @surajshetty3416
|
||||
www/ @surajshetty3416
|
||||
integrations/ @leela
|
||||
patches/ @surajshetty3416 @gavindsouza
|
||||
email/ @leela
|
||||
event_streaming/ @ruchamahabal
|
||||
data_import* @netchampfaris
|
||||
core/ @surajshetty3416
|
||||
database @gavindsouza
|
||||
model @gavindsouza
|
||||
requirements.txt @gavindsouza
|
||||
query_builder/ @gavindsouza
|
||||
commands/ @gavindsouza
|
||||
workspace @shariquerik
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License
|
||||
|
||||
Copyright (c) 2016-2018 Frappe Technologies Pvt. Ltd. <developers@frappe.io>
|
||||
Copyright (c) 2016-2021 Frappe Technologies Pvt. Ltd. <developers@frappe.io>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
51
README.md
51
README.md
|
|
@ -14,42 +14,57 @@
|
|||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://travis-ci.com/frappe/frappe">
|
||||
<img src="https://travis-ci.com/frappe/frappe.svg?branch=develop">
|
||||
</a>
|
||||
<a href='https://frappeframework.com/docs'>
|
||||
<img src='https://img.shields.io/badge/docs-📖-7575FF.svg?style=flat-square'/>
|
||||
</a>
|
||||
<a href="https://github.com/frappe/frappe/actions/workflows/server-mariadb-tests.yml">
|
||||
<img src="https://github.com/frappe/frappe/actions/workflows/server-mariadb-tests.yml/badge.svg">
|
||||
</a>
|
||||
<a href="https://github.com/frappe/frappe/actions/workflows/ui-tests.yml">
|
||||
<img src="https://github.com/frappe/frappe/actions/workflows/ui-tests.yml/badge.svg?branch=develop">
|
||||
</a>
|
||||
<a href='https://frappeframework.com/docs'>
|
||||
<img src='https://img.shields.io/badge/docs-📖-7575FF.svg?style=flat-square'/>
|
||||
</a>
|
||||
<a href='https://www.codetriage.com/frappe/frappe'>
|
||||
<img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'>
|
||||
</a>
|
||||
<a href='https://coveralls.io/github/frappe/frappe?branch=develop'>
|
||||
<img src='https://coveralls.io/repos/github/frappe/frappe/badge.svg?branch=develop'>
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/frappe/frappe">
|
||||
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [ERPNext](https://erpnext.com)
|
||||
|
||||
### Table of Contents
|
||||
* [Installation](https://frappeframework.com/docs/user/en/installation)
|
||||
* [Documentation](https://frappeframework.com/docs)
|
||||
<div align="center">
|
||||
<a href="https://frappecloud.com/deploy?apps=frappe&source=frappe_readme">
|
||||
<img src=".github/try-on-f-cloud-button.svg" height="40">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Table of Contents
|
||||
* [Installation](#installation)
|
||||
* [Contributing](#contributing)
|
||||
* [Resources](#resources)
|
||||
* [License](#license)
|
||||
|
||||
### Installation
|
||||
## Installation
|
||||
|
||||
[Install via Frappe Bench](https://github.com/frappe/bench)
|
||||
* [Install via Docker](https://github.com/frappe/frappe_docker)
|
||||
* [Install via Frappe Bench](https://github.com/frappe/bench)
|
||||
* [Offical Documentation](https://frappeframework.com/docs/user/en/installation)
|
||||
* [Managed Hosting on Frappe Cloud](https://frappecloud.com/deploy?apps=frappe&source=frappe_readme)
|
||||
|
||||
## Contributing
|
||||
|
||||
1. [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||
1. [Contribution Guidelines](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
|
||||
1. [Security Policy](SECURITY.md)
|
||||
1. [Translations](https://translate.erpnext.com)
|
||||
|
||||
### Website
|
||||
## Resources
|
||||
|
||||
For details and documentation, see the website
|
||||
[https://frappeframework.com](https://frappeframework.com)
|
||||
1. [frappeframework.com](https://frappeframework.com) - Official documentation of the Frappe Framework.
|
||||
1. [frappe.school](https://frappe.school) - Pick from the various courses by the maintainers or from the community.
|
||||
|
||||
### License
|
||||
## License
|
||||
This repository has been released under the [MIT License](LICENSE).
|
||||
|
|
|
|||
35
codecov.yml
Normal file
35
codecov.yml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
codecov:
|
||||
require_ci_to_pass: yes
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default: false
|
||||
server:
|
||||
target: auto
|
||||
threshold: 0.5%
|
||||
flags:
|
||||
- server
|
||||
patch:
|
||||
default: false
|
||||
server:
|
||||
target: 85%
|
||||
threshold: 0%
|
||||
only_pulls: true
|
||||
if_ci_failed: ignore
|
||||
flags:
|
||||
- server
|
||||
|
||||
comment:
|
||||
layout: "diff, flags"
|
||||
require_changes: true
|
||||
|
||||
flags:
|
||||
server:
|
||||
paths:
|
||||
- ".*\\.py"
|
||||
carryforward: true
|
||||
ui-tests:
|
||||
paths:
|
||||
- ".*\\.js"
|
||||
carryforward: true
|
||||
|
|
@ -4,8 +4,12 @@
|
|||
"adminPassword": "admin",
|
||||
"defaultCommandTimeout": 20000,
|
||||
"pageLoadTimeout": 15000,
|
||||
"video": true,
|
||||
"videoUploadOnPasses": false,
|
||||
"retries": {
|
||||
"runMode": 2,
|
||||
"openMode": 2
|
||||
}
|
||||
},
|
||||
"integrationFolder": ".",
|
||||
"testFiles": ["cypress/integration/*.js", "**/ui_test_*.js"]
|
||||
}
|
||||
|
|
|
|||
30
cypress/fixtures/child_table_doctype.js
Normal file
30
cypress/fixtures/child_table_doctype.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
export default {
|
||||
name: "Child Table Doctype",
|
||||
actions: [],
|
||||
custom: 1,
|
||||
autoname: "field:title",
|
||||
creation: "2022-02-09 20:15:21.242213",
|
||||
doctype: "DocType",
|
||||
editable_grid: 1,
|
||||
engine: "InnoDB",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "title",
|
||||
fieldtype: "Data",
|
||||
in_list_view: 1,
|
||||
label: "Title",
|
||||
unique: 1
|
||||
}
|
||||
],
|
||||
links: [],
|
||||
istable: 1,
|
||||
modified: "2022-02-10 12:03:12.603763",
|
||||
modified_by: "Administrator",
|
||||
module: "Custom",
|
||||
naming_rule: "By fieldname",
|
||||
owner: "Administrator",
|
||||
permissions: [],
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
59
cypress/fixtures/child_table_doctype_1.js
Normal file
59
cypress/fixtures/child_table_doctype_1.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
export default {
|
||||
name: "Child Table Doctype 1",
|
||||
actions: [],
|
||||
custom: 1,
|
||||
autoname: "format: Test-{####}",
|
||||
creation: "2022-02-09 20:15:21.242213",
|
||||
doctype: "DocType",
|
||||
editable_grid: 1,
|
||||
engine: "InnoDB",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "data",
|
||||
fieldtype: "Data",
|
||||
in_list_view: 1,
|
||||
label: "Data"
|
||||
},
|
||||
{
|
||||
fieldname: "barcode",
|
||||
fieldtype: "Barcode",
|
||||
in_list_view: 1,
|
||||
label: "Barcode"
|
||||
},
|
||||
{
|
||||
fieldname: "check",
|
||||
fieldtype: "Check",
|
||||
in_list_view: 1,
|
||||
label: "Check"
|
||||
},
|
||||
{
|
||||
fieldname: "rating",
|
||||
fieldtype: "Rating",
|
||||
in_list_view: 1,
|
||||
label: "Rating"
|
||||
},
|
||||
{
|
||||
fieldname: "duration",
|
||||
fieldtype: "Duration",
|
||||
in_list_view: 1,
|
||||
label: "Duration"
|
||||
},
|
||||
{
|
||||
fieldname: "date",
|
||||
fieldtype: "Date",
|
||||
in_list_view: 1,
|
||||
label: "Date"
|
||||
}
|
||||
],
|
||||
links: [],
|
||||
istable: 1,
|
||||
modified: "2022-02-10 12:03:12.603763",
|
||||
modified_by: "Administrator",
|
||||
module: "Custom",
|
||||
naming_rule: "By fieldname",
|
||||
owner: "Administrator",
|
||||
permissions: [],
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
65
cypress/fixtures/data_field_validation_doctype.js
Normal file
65
cypress/fixtures/data_field_validation_doctype.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
export default {
|
||||
name: 'Validation Test',
|
||||
custom: 1,
|
||||
actions: [],
|
||||
creation: '2019-03-15 06:29:07.215072',
|
||||
doctype: 'DocType',
|
||||
editable_grid: 1,
|
||||
engine: 'InnoDB',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'email',
|
||||
fieldtype: 'Data',
|
||||
label: 'Email',
|
||||
options: 'Email'
|
||||
},
|
||||
{
|
||||
fieldname: 'URL',
|
||||
fieldtype: 'Data',
|
||||
label: 'URL',
|
||||
options: 'URL'
|
||||
},
|
||||
{
|
||||
fieldname: 'Phone',
|
||||
fieldtype: 'Data',
|
||||
label: 'Phone',
|
||||
options: 'Phone'
|
||||
},
|
||||
{
|
||||
fieldname: 'person_name',
|
||||
fieldtype: 'Data',
|
||||
label: 'Person Name',
|
||||
options: 'Name'
|
||||
},
|
||||
{
|
||||
fieldname: 'read_only_url',
|
||||
fieldtype: 'Data',
|
||||
label: 'Read Only URL',
|
||||
options: 'URL',
|
||||
read_only: '1',
|
||||
default: 'https://frappe.io'
|
||||
}
|
||||
],
|
||||
issingle: 1,
|
||||
links: [],
|
||||
modified: '2021-04-19 14:40:53.127615',
|
||||
modified_by: 'Administrator',
|
||||
module: 'Custom',
|
||||
owner: 'Administrator',
|
||||
permissions: [
|
||||
{
|
||||
create: 1,
|
||||
delete: 1,
|
||||
email: 1,
|
||||
print: 1,
|
||||
read: 1,
|
||||
role: 'System Manager',
|
||||
share: 1,
|
||||
write: 1
|
||||
}
|
||||
],
|
||||
quick_entry: 1,
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
45
cypress/fixtures/doctype_to_link.js
Normal file
45
cypress/fixtures/doctype_to_link.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
export default {
|
||||
name: "Doctype to Link",
|
||||
actions: [],
|
||||
custom: 1,
|
||||
naming_rule: "By fieldname",
|
||||
autoname: "field:title",
|
||||
creation: "2022-02-09 20:15:21.242213",
|
||||
doctype: "DocType",
|
||||
editable_grid: 1,
|
||||
engine: "InnoDB",
|
||||
fields: [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
links: [
|
||||
{
|
||||
"group": "Child Doctype",
|
||||
"link_doctype": "Doctype With Child Table",
|
||||
"link_fieldname": "title"
|
||||
}
|
||||
],
|
||||
modified: "2022-02-10 12:03:12.603763",
|
||||
modified_by: "Administrator",
|
||||
module: "Custom",
|
||||
owner: "Administrator",
|
||||
permissions: [
|
||||
{
|
||||
create: 1,
|
||||
delete: 1,
|
||||
email: 1,
|
||||
print: 1,
|
||||
read: 1,
|
||||
role: 'System Manager',
|
||||
share: 1,
|
||||
write: 1
|
||||
}
|
||||
],
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
52
cypress/fixtures/doctype_with_child_table.js
Normal file
52
cypress/fixtures/doctype_with_child_table.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
export default {
|
||||
name: "Doctype With Child Table",
|
||||
actions: [],
|
||||
custom: 1,
|
||||
autoname: "field:title",
|
||||
creation: "2022-02-09 20:15:21.242213",
|
||||
doctype: "DocType",
|
||||
editable_grid: 1,
|
||||
engine: "InnoDB",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "title",
|
||||
fieldtype: "Data",
|
||||
label: "Title",
|
||||
unique: 1
|
||||
},
|
||||
{
|
||||
fieldname: "child_table",
|
||||
fieldtype: "Table",
|
||||
label: "Child Table",
|
||||
options: "Child Table Doctype",
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: "child_table_1",
|
||||
fieldtype: "Table",
|
||||
label: "Child Table 1",
|
||||
options: "Child Table Doctype 1"
|
||||
}
|
||||
],
|
||||
links: [],
|
||||
modified: "2022-02-10 12:03:12.603763",
|
||||
modified_by: "Administrator",
|
||||
module: "Custom",
|
||||
naming_rule: "By fieldname",
|
||||
owner: "Administrator",
|
||||
permissions: [
|
||||
{
|
||||
create: 1,
|
||||
delete: 1,
|
||||
email: 1,
|
||||
print: 1,
|
||||
read: 1,
|
||||
role: 'System Manager',
|
||||
share: 1,
|
||||
write: 1
|
||||
}
|
||||
],
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
54
cypress/fixtures/doctype_with_tab_break.js
Normal file
54
cypress/fixtures/doctype_with_tab_break.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
export default {
|
||||
name: 'Form With Tab Break',
|
||||
custom: 1,
|
||||
actions: [],
|
||||
doctype: 'DocType',
|
||||
engine: 'InnoDB',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'username',
|
||||
fieldtype: 'Data',
|
||||
label: 'Name',
|
||||
options: 'Name'
|
||||
},
|
||||
{
|
||||
fieldname: 'tab',
|
||||
fieldtype: 'Tab Break',
|
||||
label: 'Tab 2',
|
||||
},
|
||||
{
|
||||
fieldname: 'Phone',
|
||||
fieldtype: 'Data',
|
||||
label: 'Phone',
|
||||
options: 'Phone',
|
||||
reqd: 1
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
"group": "Profile",
|
||||
"link_doctype": "Contact",
|
||||
"link_fieldname": "user"
|
||||
},
|
||||
],
|
||||
modified_by: 'Administrator',
|
||||
module: 'Custom',
|
||||
owner: 'Administrator',
|
||||
permissions: [
|
||||
{
|
||||
create: 1,
|
||||
delete: 1,
|
||||
email: 1,
|
||||
print: 1,
|
||||
read: 1,
|
||||
role: 'System Manager',
|
||||
share: 1,
|
||||
write: 1
|
||||
}
|
||||
],
|
||||
quick_entry: 1,
|
||||
autoname: "format: Test-{####}",
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
BIN
cypress/fixtures/sample_image.jpg
Normal file
BIN
cypress/fixtures/sample_image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 244 KiB |
|
|
@ -31,8 +31,13 @@ context('API Resources', () => {
|
|||
});
|
||||
|
||||
it('Removes the Comments', () => {
|
||||
cy.get_list('Comment').then(body => body.data.forEach(comment => {
|
||||
cy.remove_doc('Comment', comment.name);
|
||||
}));
|
||||
cy.get_list('Comment').then(body => {
|
||||
let comment_names = [];
|
||||
body.data.map(comment => comment_names.push(comment.name));
|
||||
comment_names = [...new Set(comment_names)]; // remove duplicates
|
||||
comment_names.forEach((comment_name) => {
|
||||
cy.remove_doc('Comment', comment_name);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ context('Awesome Bar', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
cy.get('.navbar .navbar-home').click();
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)').clear();
|
||||
});
|
||||
|
||||
it('navigates to doctype list', () => {
|
||||
cy.get('#navbar-search').type('todo', { delay: 200 });
|
||||
cy.get('#navbar-search + ul').should('be.visible');
|
||||
cy.get('#navbar-search').type('{downarrow}{enter}', { delay: 100 });
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('todo', { delay: 700 });
|
||||
cy.get('.awesomplete').findByRole('listbox').should('be.visible');
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('{downarrow}{enter}', { delay: 700 });
|
||||
|
||||
cy.get('.title-text').should('contain', 'To Do');
|
||||
|
||||
|
|
@ -20,25 +21,25 @@ context('Awesome Bar', () => {
|
|||
});
|
||||
|
||||
it('find text in doctype list', () => {
|
||||
cy.get('#navbar-search')
|
||||
.type('test in todo{downarrow}{enter}', { delay: 200 });
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)')
|
||||
.type('test in todo{downarrow}{enter}', { delay: 700 });
|
||||
|
||||
cy.get('.title-text').should('contain', 'To Do');
|
||||
|
||||
cy.get('[data-original-title="Name"] > .input-with-feedback')
|
||||
cy.findByPlaceholderText('Name')
|
||||
.should('have.value', '%test%');
|
||||
});
|
||||
|
||||
it('navigates to new form', () => {
|
||||
cy.get('#navbar-search')
|
||||
.type('new blog post{downarrow}{enter}', { delay: 200 });
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)')
|
||||
.type('new blog post{downarrow}{enter}', { delay: 700 });
|
||||
|
||||
cy.get('.title-text:visible').should('have.text', 'New Blog Post');
|
||||
});
|
||||
|
||||
it('calculates math expressions', () => {
|
||||
cy.get('#navbar-search')
|
||||
.type('55 + 32{downarrow}{enter}', { delay: 200 });
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)')
|
||||
.type('55 + 32{downarrow}{enter}', { delay: 700 });
|
||||
|
||||
cy.get('.modal-title').should('contain', 'Result');
|
||||
cy.get('.msgprint').should('contain', '55 + 32 = 87');
|
||||
|
|
|
|||
57
cypress/integration/control_autocomplete.js
Normal file
57
cypress/integration/control_autocomplete.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
context('Control Autocomplete', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
function get_dialog_with_autocomplete(options) {
|
||||
cy.visit('/app/website');
|
||||
return cy.dialog({
|
||||
title: 'Autocomplete',
|
||||
fields: [
|
||||
{
|
||||
'label': 'Select an option',
|
||||
'fieldname': 'autocomplete',
|
||||
'fieldtype': 'Autocomplete',
|
||||
'options': options || ['Option 1', 'Option 2', 'Option 3'],
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
it('should set the valid value', () => {
|
||||
get_dialog_with_autocomplete().as('dialog');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=autocomplete] input').focus().as('input');
|
||||
cy.wait(1000);
|
||||
cy.get('@input').type('2', { delay: 300 });
|
||||
cy.get('.frappe-control[data-fieldname=autocomplete]').findByRole('listbox').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname=autocomplete] input').type('{enter}', { delay: 300 });
|
||||
cy.get('.frappe-control[data-fieldname=autocomplete] input').blur();
|
||||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('autocomplete');
|
||||
expect(value).to.eq('Option 2');
|
||||
dialog.clear();
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the valid value with different label', () => {
|
||||
const options_with_label = [
|
||||
{ label: "Option 1", value: "option_1" },
|
||||
{ label: "Option 2", value: "option_2" }
|
||||
];
|
||||
get_dialog_with_autocomplete(options_with_label).as('dialog');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=autocomplete] input').focus().as('input');
|
||||
cy.get('.frappe-control[data-fieldname=autocomplete]').findByRole('listbox').should('be.visible');
|
||||
cy.get('@input').type('2', { delay: 300 });
|
||||
cy.get('.frappe-control[data-fieldname=autocomplete] input').type('{enter}', { delay: 300 });
|
||||
cy.get('.frappe-control[data-fieldname=autocomplete] input').blur();
|
||||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('autocomplete');
|
||||
expect(value).to.eq('option_2');
|
||||
dialog.clear();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -20,8 +20,7 @@ context('Control Barcode', () => {
|
|||
it('should generate barcode on setting a value', () => {
|
||||
get_dialog_with_barcode().as('dialog');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=barcode] input')
|
||||
.focus()
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
.type('123456789')
|
||||
.blur();
|
||||
cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]')
|
||||
|
|
@ -37,11 +36,10 @@ context('Control Barcode', () => {
|
|||
it('should reset when input is cleared', () => {
|
||||
get_dialog_with_barcode().as('dialog');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=barcode] input')
|
||||
.focus()
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
.type('123456789')
|
||||
.blur();
|
||||
cy.get('.frappe-control[data-fieldname=barcode] input')
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
.clear()
|
||||
.blur();
|
||||
cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]')
|
||||
|
|
|
|||
|
|
@ -33,12 +33,13 @@ context('Control Duration', () => {
|
|||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('duration');
|
||||
expect(value).to.equal(3889800);
|
||||
cy.hide_dialog();
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide days or seconds according to duration options', () => {
|
||||
get_dialog_with_duration(1, 1).as('dialog');
|
||||
cy.get('.frappe-control[data-fieldname=duration] input').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=duration] input').first();
|
||||
cy.get('.duration-input[data-duration=days]').should('not.be.visible');
|
||||
cy.get('.duration-input[data-duration=seconds]').should('not.be.visible');
|
||||
});
|
||||
|
|
|
|||
93
cypress/integration/control_float.js
Normal file
93
cypress/integration/control_float.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
context("Control Float", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit("/app/website");
|
||||
});
|
||||
|
||||
function get_dialog_with_float() {
|
||||
return cy.dialog({
|
||||
title: "Float Check",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "float_number",
|
||||
fieldtype: "Float",
|
||||
Label: "Float"
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
it("check value changes", () => {
|
||||
get_dialog_with_float().as("dialog");
|
||||
|
||||
let data = get_data();
|
||||
data.forEach(x => {
|
||||
cy.window()
|
||||
.its("frappe")
|
||||
.then(frappe => {
|
||||
frappe.boot.sysdefaults.number_format = x.number_format;
|
||||
});
|
||||
x.values.forEach(d => {
|
||||
cy.get_field("float_number", "Float").clear();
|
||||
cy.fill_field("float_number", d.input, "Float").blur();
|
||||
cy.get_field("float_number", "Float").should(
|
||||
"have.value",
|
||||
d.blur_expected
|
||||
);
|
||||
|
||||
cy.get_field("float_number", "Float").focus();
|
||||
cy.get_field("float_number", "Float").blur();
|
||||
cy.get_field("float_number", "Float").focus();
|
||||
cy.get_field("float_number", "Float").should(
|
||||
"have.value",
|
||||
d.focus_expected
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function get_data() {
|
||||
return [
|
||||
{
|
||||
number_format: "#.###,##",
|
||||
values: [
|
||||
{
|
||||
input: "364.87,334",
|
||||
blur_expected: "36.487,334",
|
||||
focus_expected: "36487.334"
|
||||
},
|
||||
{
|
||||
input: "36487,334",
|
||||
blur_expected: "36.487,334",
|
||||
focus_expected: "36487.334"
|
||||
},
|
||||
{
|
||||
input: "100",
|
||||
blur_expected: "100,000",
|
||||
focus_expected: "100"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
number_format: "#,###.##",
|
||||
values: [
|
||||
{
|
||||
input: "364,87.334",
|
||||
blur_expected: "36,487.334",
|
||||
focus_expected: "36487.334"
|
||||
},
|
||||
{
|
||||
input: "36487.334",
|
||||
blur_expected: "36,487.334",
|
||||
focus_expected: "36487.334"
|
||||
},
|
||||
{
|
||||
input: "100",
|
||||
blur_expected: "100.000",
|
||||
focus_expected: "100"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
50
cypress/integration/control_icon.js
Normal file
50
cypress/integration/control_icon.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
context('Control Icon', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
function get_dialog_with_icon() {
|
||||
return cy.dialog({
|
||||
title: 'Icon',
|
||||
fields: [{
|
||||
label: 'Icon',
|
||||
fieldname: 'icon',
|
||||
fieldtype: 'Icon'
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
it('should set icon', () => {
|
||||
get_dialog_with_icon().as('dialog');
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').click();
|
||||
|
||||
cy.get('.icon-picker .icon-wrapper[id=heart-active]').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'heart-active');
|
||||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('icon');
|
||||
expect(value).to.equal('heart-active');
|
||||
});
|
||||
|
||||
cy.get('.icon-picker .icon-wrapper[id=heart]').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'heart');
|
||||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('icon');
|
||||
expect(value).to.equal('heart');
|
||||
});
|
||||
});
|
||||
|
||||
it('search for icon and clear search input', () => {
|
||||
let search_text = 'ed';
|
||||
cy.get('.icon-picker').findByRole('searchbox').click().type(search_text);
|
||||
cy.get('.icon-section .icon-wrapper:not(.hidden)').then(i => {
|
||||
cy.get(`.icon-section .icon-wrapper[id*='${search_text}']`).then(icons => {
|
||||
expect(i.length).to.equal(icons.length);
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('.icon-picker').findByRole('searchbox').clear().blur();
|
||||
cy.get('.icon-section .icon-wrapper').should('not.have.class', 'hidden');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -35,7 +35,7 @@ context('Control Link', () => {
|
|||
cy.wait('@search_link');
|
||||
cy.get('@input').type('todo for link', { delay: 200 });
|
||||
cy.wait('@search_link');
|
||||
cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname=link]').findByRole('listbox').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 });
|
||||
cy.get('.frappe-control[data-fieldname=link] input').blur();
|
||||
cy.get('@dialog').then(dialog => {
|
||||
|
|
@ -49,7 +49,7 @@ context('Control Link', () => {
|
|||
it('should unset invalid value', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.intercept('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
|
||||
cy.intercept('POST', '/api/method/frappe.client.validate_link').as('validate_link');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=link] input')
|
||||
.type('invalid value', { delay: 100 })
|
||||
|
|
@ -58,10 +58,27 @@ context('Control Link', () => {
|
|||
cy.get('.frappe-control[data-fieldname=link] input').should('have.value', '');
|
||||
});
|
||||
|
||||
it("should be possible set empty value explicitly", () => {
|
||||
get_dialog_with_link().as("dialog");
|
||||
|
||||
cy.intercept("POST", "/api/method/frappe.client.validate_link").as("validate_link");
|
||||
|
||||
cy.get(".frappe-control[data-fieldname=link] input")
|
||||
.type(" ", { delay: 100 })
|
||||
.blur();
|
||||
cy.wait("@validate_link");
|
||||
cy.get(".frappe-control[data-fieldname=link] input").should("have.value", "");
|
||||
cy.window()
|
||||
.its("cur_dialog")
|
||||
.then((dialog) => {
|
||||
expect(dialog.get_value("link")).to.equal('');
|
||||
});
|
||||
});
|
||||
|
||||
it('should route to form on arrow click', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.intercept('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
|
||||
cy.intercept('POST', '/api/method/frappe.client.validate_link').as('validate_link');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
|
||||
|
||||
cy.get('@todos').then(todos => {
|
||||
|
|
@ -71,10 +88,130 @@ context('Control Link', () => {
|
|||
cy.get('@input').type(todos[0]).blur();
|
||||
cy.wait('@validate_link');
|
||||
cy.get('@input').focus();
|
||||
cy.get('.frappe-control[data-fieldname=link] .link-btn')
|
||||
cy.findByTitle('Open Link')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.location('pathname').should('eq', `/app/todo/${todos[0]}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('show title field in link', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.insert_doc("Property Setter", {
|
||||
"doctype": "Property Setter",
|
||||
"doc_type": "ToDo",
|
||||
"property": "show_title_field_in_link",
|
||||
"property_type": "Check",
|
||||
"doctype_or_field": "DocType",
|
||||
"value": "1"
|
||||
}, true);
|
||||
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
if (!frappe.boot) {
|
||||
frappe.boot = {
|
||||
link_title_doctypes: ['ToDo']
|
||||
};
|
||||
} else {
|
||||
frappe.boot.link_title_doctypes = ['ToDo'];
|
||||
}
|
||||
});
|
||||
|
||||
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
|
||||
cy.wait('@search_link');
|
||||
cy.get('@input').type('todo for link');
|
||||
cy.wait('@search_link');
|
||||
cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 });
|
||||
cy.get('.frappe-control[data-fieldname=link] input').blur();
|
||||
cy.get('@dialog').then(dialog => {
|
||||
cy.get('@todos').then(todos => {
|
||||
let field = dialog.get_field('link');
|
||||
let value = field.get_value();
|
||||
let label = field.get_label_value();
|
||||
|
||||
expect(value).to.eq(todos[0]);
|
||||
expect(label).to.eq('this is a test todo for link');
|
||||
|
||||
cy.remove_doc("Property Setter", "ToDo-main-show_title_field_in_link");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should update dependant fields (via fetch_from)', () => {
|
||||
cy.get('@todos').then(todos => {
|
||||
cy.visit(`/app/todo/${todos[0]}`);
|
||||
cy.intercept('POST', '/api/method/frappe.client.validate_link').as('validate_link');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=assigned_by] input').focus().as('input');
|
||||
cy.get('@input').type('Administrator', {delay: 100}).blur();
|
||||
cy.wait('@validate_link');
|
||||
cy.get('.frappe-control[data-fieldname=assigned_by_full_name] .control-value').should(
|
||||
'contain', 'Administrator'
|
||||
);
|
||||
|
||||
cy.window()
|
||||
.its("cur_frm.doc.assigned_by")
|
||||
.should("eq", "Administrator");
|
||||
|
||||
// invalid input
|
||||
cy.get('@input').clear().type('invalid input', {delay: 100}).blur();
|
||||
cy.get('.frappe-control[data-fieldname=assigned_by_full_name] .control-value').should(
|
||||
'contain', ''
|
||||
);
|
||||
|
||||
cy.window()
|
||||
.its("cur_frm.doc.assigned_by")
|
||||
.should("eq", null);
|
||||
|
||||
// set valid value again
|
||||
cy.get('@input').clear().type('Administrator', {delay: 100}).blur();
|
||||
cy.wait('@validate_link');
|
||||
|
||||
cy.window()
|
||||
.its("cur_frm.doc.assigned_by")
|
||||
.should("eq", "Administrator");
|
||||
|
||||
// clear input
|
||||
cy.get('@input').clear().blur();
|
||||
cy.get('.frappe-control[data-fieldname=assigned_by_full_name] .control-value').should(
|
||||
'contain', ''
|
||||
);
|
||||
|
||||
cy.window()
|
||||
.its("cur_frm.doc.assigned_by")
|
||||
.should("eq", "");
|
||||
});
|
||||
});
|
||||
|
||||
it("should set default values", () => {
|
||||
cy.insert_doc("Property Setter", {
|
||||
"doctype_or_field": "DocField",
|
||||
"doc_type": "ToDo",
|
||||
"field_name": "assigned_by",
|
||||
"property": "default",
|
||||
"property_type": "Text",
|
||||
"value": "Administrator"
|
||||
}, true);
|
||||
cy.reload();
|
||||
cy.new_form("ToDo");
|
||||
cy.fill_field("description", "new", "Text Editor");
|
||||
cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form");
|
||||
cy.findByRole("button", {name: "Save"}).click();
|
||||
cy.wait("@save_form");
|
||||
cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should(
|
||||
"contain", "Administrator"
|
||||
);
|
||||
// if user clears default value explicitly, system should not reset default again
|
||||
cy.get_field("assigned_by").clear().blur();
|
||||
cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form");
|
||||
cy.findByRole("button", {name: "Save"}).click();
|
||||
cy.wait("@save_form");
|
||||
cy.get_field("assigned_by").should("have.value", "");
|
||||
cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should(
|
||||
"contain", ""
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ context('Control Rating', () => {
|
|||
fields: [{
|
||||
'fieldname': 'rate',
|
||||
'fieldtype': 'Rating',
|
||||
'options': 7
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
|
@ -19,12 +20,13 @@ context('Control Rating', () => {
|
|||
|
||||
cy.get('div.rating')
|
||||
.children('svg')
|
||||
.find('.right-half')
|
||||
.first()
|
||||
.click()
|
||||
.should('have.class', 'star-click');
|
||||
cy.get('@dialog').then(dialog => {
|
||||
var value = dialog.get_value('rate');
|
||||
expect(value).to.equal(1);
|
||||
expect(value).to.equal(1/7);
|
||||
dialog.hide();
|
||||
});
|
||||
});
|
||||
|
|
@ -34,10 +36,21 @@ context('Control Rating', () => {
|
|||
|
||||
cy.get('div.rating')
|
||||
.children('svg')
|
||||
.find('.right-half')
|
||||
.first()
|
||||
.invoke('trigger', 'mouseenter')
|
||||
.should('have.class', 'star-hover')
|
||||
.invoke('trigger', 'mouseleave')
|
||||
.should('not.have.class', 'star-hover');
|
||||
});
|
||||
|
||||
it('check number of stars in rating', () => {
|
||||
get_dialog_with_rating();
|
||||
|
||||
cy.get('div.rating')
|
||||
.first()
|
||||
.children('svg')
|
||||
.should('have.length', 7);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
38
cypress/integration/control_select.js
Normal file
38
cypress/integration/control_select.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
context('Control Select', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
function get_dialog_with_select() {
|
||||
return cy.dialog({
|
||||
title: 'Select',
|
||||
fields: [{
|
||||
'fieldname': 'select_control',
|
||||
'fieldtype': 'Select',
|
||||
'placeholder': 'Select an Option',
|
||||
'options': ['', 'Option 1', 'Option 2', 'Option 2'],
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
it('toggles placholder on clicking an option', () => {
|
||||
get_dialog_with_select().as('dialog');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=select_control] .control-input').as('control');
|
||||
cy.get('.frappe-control[data-fieldname=select_control] .control-input select').as('select');
|
||||
cy.get('@control').get('.select-icon').should('exist');
|
||||
cy.get('@control').get('.placeholder').should('have.css', 'display', 'block');
|
||||
cy.get('@select').select('Option 1');
|
||||
cy.findByDisplayValue('Option 1').should('exist');
|
||||
cy.get('@control').get('.placeholder').should('have.css', 'display', 'none');
|
||||
cy.get('@select').invoke('val', '');
|
||||
cy.findByDisplayValue('Option 1').should('not.exist');
|
||||
cy.get('@control').get('.placeholder').should('have.css', 'display', 'block');
|
||||
|
||||
|
||||
cy.get('@dialog').then(dialog => {
|
||||
dialog.hide();
|
||||
});
|
||||
});
|
||||
});
|
||||
22
cypress/integration/dashboard_chart.js
Normal file
22
cypress/integration/dashboard_chart.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
context('Dashboard Chart', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
it('Check filter populate for child table doctype', () => {
|
||||
cy.visit('/app/dashboard-chart/new-dashboard-chart-1');
|
||||
cy.get('[data-fieldname="parent_document_type"]').should('have.css', 'display', 'none');
|
||||
|
||||
cy.get_field('document_type', 'Link');
|
||||
cy.fill_field('document_type', 'Workspace Link', 'Link').focus().blur();
|
||||
cy.get_field('document_type', 'Link').should('have.value', 'Workspace Link');
|
||||
|
||||
cy.fill_field('chart_name', 'Test Chart', 'Data');
|
||||
|
||||
cy.get('[data-fieldname="filters_json"]').click().wait(200);
|
||||
cy.get('.modal-body .filter-action-buttons .add-filter').click();
|
||||
cy.get('.modal-body .fieldname-select-area').click();
|
||||
cy.get('.modal-actions .btn-modal-close').click();
|
||||
});
|
||||
});
|
||||
91
cypress/integration/dashboard_links.js
Normal file
91
cypress/integration/dashboard_links.js
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import doctype_with_child_table from '../fixtures/doctype_with_child_table';
|
||||
import child_table_doctype from '../fixtures/child_table_doctype';
|
||||
import child_table_doctype_1 from '../fixtures/child_table_doctype_1';
|
||||
import doctype_to_link from '../fixtures/doctype_to_link';
|
||||
const doctype_to_link_name = doctype_to_link.name;
|
||||
const child_table_doctype_name = child_table_doctype.name;
|
||||
|
||||
context('Dashboard links', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.insert_doc('DocType', child_table_doctype, true);
|
||||
cy.insert_doc('DocType', child_table_doctype_1, true);
|
||||
cy.insert_doc('DocType', doctype_with_child_table, true);
|
||||
cy.insert_doc('DocType', doctype_to_link, true);
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.xcall("frappe.tests.ui_test_helpers.update_child_table", {
|
||||
name: child_table_doctype_name
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Adding a new contact, checking for the counter on the dashboard and deleting the created contact', () => {
|
||||
cy.visit('/app/contact');
|
||||
cy.clear_filters();
|
||||
|
||||
cy.visit('/app/user');
|
||||
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click({ force: true });
|
||||
|
||||
//To check if initially the dashboard contains only the "Contact" link and there is no counter
|
||||
cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
|
||||
|
||||
//Adding a new contact
|
||||
cy.get('.document-link-badge[data-doctype="Contact"]').click();
|
||||
cy.wait(300);
|
||||
cy.findByRole('button', {name: 'Add Contact'}).should('be.visible');
|
||||
cy.findByRole('button', {name: 'Add Contact'}).click();
|
||||
cy.get('[data-doctype="Contact"][data-fieldname="first_name"]').type('Admin');
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.visit('/app/user');
|
||||
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click({ force: true });
|
||||
|
||||
//To check if the counter for contact doc is "1" after adding the contact
|
||||
cy.get('[data-doctype="Contact"] > .count').should('contain', '1');
|
||||
cy.get('[data-doctype="Contact"]').contains('Contact').click();
|
||||
|
||||
//Deleting the newly created contact
|
||||
cy.visit('/app/contact');
|
||||
cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click({ force: true });
|
||||
cy.findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.actions-btn-group [data-label="Delete"]').click();
|
||||
cy.findByRole('button', {name: 'Yes'}).click({delay: 700});
|
||||
|
||||
|
||||
//To check if the counter from the "Contact" doc link is removed
|
||||
cy.wait(700);
|
||||
cy.visit('/app/user');
|
||||
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click({ force: true });
|
||||
cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
|
||||
});
|
||||
|
||||
it('Report link in dashboard', () => {
|
||||
cy.visit('/app/user');
|
||||
cy.visit('/app/user/Administrator');
|
||||
cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
|
||||
cy.findByText('Connections');
|
||||
cy.window()
|
||||
.its('cur_frm')
|
||||
.then(cur_frm => {
|
||||
cur_frm.dashboard.data.reports = [
|
||||
{
|
||||
'label': 'Reports',
|
||||
'items': ['Website Analytics']
|
||||
}
|
||||
];
|
||||
cur_frm.dashboard.render_report_links();
|
||||
cy.get('[data-report="Website Analytics"]').contains('Website Analytics').click();
|
||||
cy.findByText('Website Analytics');
|
||||
});
|
||||
});
|
||||
|
||||
it('check if child table is populated with linked field on creation from dashboard link', () => {
|
||||
cy.new_form(doctype_to_link_name);
|
||||
cy.fill_field("title", "Test Linking");
|
||||
cy.findByRole("button", {name: "Save"}).click();
|
||||
|
||||
cy.get('.document-link .btn-new').click();
|
||||
cy.get('.frappe-control[data-fieldname="child_table"] .rows .data-row .col[data-fieldname="doctype_to_link"]')
|
||||
.should('contain.text', 'Test Linking');
|
||||
});
|
||||
});
|
||||
43
cypress/integration/data_field_form_validation.js
Normal file
43
cypress/integration/data_field_form_validation.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import data_field_validation_doctype from '../fixtures/data_field_validation_doctype';
|
||||
const doctype_name = data_field_validation_doctype.name;
|
||||
|
||||
|
||||
context('Data Field Input Validation in New Form', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
return cy.insert_doc('DocType', data_field_validation_doctype, true);
|
||||
});
|
||||
|
||||
function validateField(fieldname, invalid_value, valid_value) {
|
||||
// Invalid, should have has-error class
|
||||
cy.get_field(fieldname).clear().type(invalid_value).blur();
|
||||
cy.get(`.frappe-control[data-fieldname="${fieldname}"]`).should('have.class', 'has-error');
|
||||
// Valid value, should not have has-error class
|
||||
cy.get_field(fieldname).clear().type(valid_value);
|
||||
cy.get(`.frappe-control[data-fieldname="${fieldname}"]`).should('not.have.class', 'has-error');
|
||||
}
|
||||
|
||||
describe('Data Field Options', () => {
|
||||
it('should validate email address', () => {
|
||||
cy.new_form(doctype_name);
|
||||
validateField('email', 'captian', 'hello@test.com');
|
||||
});
|
||||
|
||||
it('should validate URL', () => {
|
||||
validateField('url', 'jkl', 'https://frappe.io');
|
||||
validateField('url', 'abcd.com', 'http://google.com/home');
|
||||
validateField('url', '&&http://google.uae', 'gopher://frappe.io');
|
||||
validateField('url', 'ftt2:://google.in?q=news', 'ftps2://frappe.io/__/#home');
|
||||
validateField('url', 'ftt2://', 'ntps://localhost'); // For intranet URLs
|
||||
});
|
||||
|
||||
it('should validate phone number', () => {
|
||||
validateField('phone', 'america', '89787878');
|
||||
});
|
||||
|
||||
it('should validate name', () => {
|
||||
validateField('person_name', ' 777Hello', 'James Bond');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -92,17 +92,18 @@ context('Control Date, Time and DateTime', () => {
|
|||
date_format: 'dd.mm.yyyy',
|
||||
time_format: 'HH:mm:ss',
|
||||
value: ' 02.12.2019 11:00:12',
|
||||
doc_value: '2019-12-02 11:00:12',
|
||||
input_value: '02.12.2019 11:00:12'
|
||||
doc_value: '2019-12-02 00:30:12', // system timezone (America/New_York)
|
||||
input_value: '02.12.2019 11:00:12' // admin timezone (Asia/Kolkata)
|
||||
},
|
||||
{
|
||||
date_format: 'mm-dd-yyyy',
|
||||
time_format: 'HH:mm',
|
||||
value: ' 12-02-2019 11:00:00',
|
||||
doc_value: '2019-12-02 11:00:00',
|
||||
input_value: '12-02-2019 11:00'
|
||||
doc_value: '2019-12-02 00:30:00', // system timezone (America/New_York)
|
||||
input_value: '12-02-2019 11:00' // admin timezone (Asia/Kolkata)
|
||||
}
|
||||
];
|
||||
|
||||
datetime_formats.forEach(d => {
|
||||
it(`test datetime format ${d.date_format} ${d.time_format}`, () => {
|
||||
cy.set_value('System Settings', 'System Settings', {
|
||||
|
|
|
|||
19
cypress/integration/datetime_field_form_validation.js
Normal file
19
cypress/integration/datetime_field_form_validation.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// TODO: Enable this again
|
||||
// currently this is flaky possibly because of different timezone in CI
|
||||
|
||||
// context('Datetime Field Validation', () => {
|
||||
// before(() => {
|
||||
// cy.login();
|
||||
// cy.visit('/app/communication');
|
||||
// });
|
||||
|
||||
// it('datetime field form validation', () => {
|
||||
// // validating datetime field value when value is set from backend and get validated on form load.
|
||||
// cy.window().its('frappe').then(frappe => {
|
||||
// return frappe.xcall("frappe.tests.ui_test_helpers.create_communication_record");
|
||||
// }).then(doc => {
|
||||
// cy.visit(`/app/communication/${doc.name}`);
|
||||
// cy.get('.indicator-pill').should('contain', 'Open').should('have.class', 'red');
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
|
@ -55,18 +55,39 @@ context('Depends On', () => {
|
|||
'read_only_depends_on': "eval:doc.test_field=='Some Other Value'",
|
||||
'options': "Child Test Depends On"
|
||||
},
|
||||
{
|
||||
"label": "Dependent Tab",
|
||||
"fieldname": "dependent_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"depends_on": "eval:doc.test_field=='Show Tab'"
|
||||
},
|
||||
{
|
||||
"fieldname": "tab_section",
|
||||
"fieldtype": "Section Break",
|
||||
},
|
||||
{
|
||||
"label": "Field in Tab",
|
||||
"fieldname": "field_in_tab",
|
||||
"fieldtype": "Data",
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should show the tab on other setting field value', () => {
|
||||
cy.new_form('Test Depends On');
|
||||
cy.fill_field('test_field', 'Show Tab');
|
||||
cy.get('body').click();
|
||||
cy.findByRole("tab", {name: "Dependent Tab"}).should('be.visible');
|
||||
});
|
||||
it('should set the field as mandatory depending on other fields value', () => {
|
||||
cy.new_form('Test Depends On');
|
||||
cy.fill_field('test_field', 'Some Value');
|
||||
cy.get('button.primary-action').contains('Save').click();
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('be.visible');
|
||||
cy.hide_dialog();
|
||||
cy.fill_field('test_field', 'Random value');
|
||||
cy.get('button.primary-action').contains('Save').click();
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('not.be.visible');
|
||||
});
|
||||
it('should set the field as read only depending on other fields value', () => {
|
||||
|
|
@ -84,7 +105,7 @@ context('Depends On', () => {
|
|||
cy.fill_field('dependant_field', 'Some Value');
|
||||
//cy.fill_field('test_field', 'Some Other Value');
|
||||
cy.get('.frappe-control[data-fieldname="child_test_depends_on_field"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('@table').findByRole('button', {name: 'Add Row'}).click();
|
||||
cy.get('@table').find('[data-idx="1"]').as('row1');
|
||||
cy.get('@row1').find('.btn-open-row').click();
|
||||
cy.get('@row1').find('.form-in-grid').as('row1-form_in_grid');
|
||||
|
|
|
|||
79
cypress/integration/discussions.js
Normal file
79
cypress/integration/discussions.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
context('Discussions', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.call('frappe.tests.ui_test_helpers.create_data_for_discussions');
|
||||
});
|
||||
});
|
||||
|
||||
const reply_through_modal = () => {
|
||||
cy.visit('/test-page-discussions');
|
||||
|
||||
// Open the modal
|
||||
cy.get('.reply').click();
|
||||
cy.wait(500);
|
||||
cy.get('.discussion-modal').should('be.visible');
|
||||
|
||||
// Enter title
|
||||
cy.get('.modal .topic-title').type('Discussion from tests')
|
||||
.should('have.value', 'Discussion from tests');
|
||||
|
||||
// Enter comment
|
||||
cy.get('.modal .comment-field')
|
||||
.type('This is a discussion from the cypress ui tests.')
|
||||
.should('have.value', 'This is a discussion from the cypress ui tests.');
|
||||
|
||||
// Submit
|
||||
cy.get('.modal .submit-discussion').click();
|
||||
cy.wait(2000);
|
||||
|
||||
// Check if discussion is added to page and content is visible
|
||||
cy.get('.sidebar-parent:first .discussion-topic-title').should('have.text', 'Discussion from tests');
|
||||
cy.get('.discussion-on-page:visible').should('have.class', 'show');
|
||||
cy.get('.discussion-on-page:visible .reply-card .reply-text')
|
||||
.should('have.text', 'This is a discussion from the cypress ui tests.\n');
|
||||
|
||||
};
|
||||
|
||||
const reply_through_comment_box = () => {
|
||||
cy.get('.discussion-on-page:visible .comment-field')
|
||||
.type('This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.')
|
||||
.should('have.value', 'This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.');
|
||||
|
||||
cy.get('.discussion-on-page:visible .submit-discussion').click();
|
||||
cy.wait(3000);
|
||||
cy.get('.discussion-on-page:visible').should('have.class', 'show');
|
||||
cy.get('.discussion-on-page:visible').children(".reply-card").eq(1).children(".reply-text")
|
||||
.should('have.text', 'This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.\n');
|
||||
};
|
||||
|
||||
const cancel_and_clear_comment_box = () => {
|
||||
cy.get('.discussion-on-page:visible .comment-field')
|
||||
.type('This is a discussion from the cypress ui tests.')
|
||||
.should('have.value', 'This is a discussion from the cypress ui tests.');
|
||||
|
||||
cy.get('.discussion-on-page:visible .cancel-comment').click();
|
||||
cy.get('.discussion-on-page:visible .comment-field').should('have.value', '');
|
||||
};
|
||||
|
||||
const single_thread_discussion = () => {
|
||||
cy.visit('/test-single-thread');
|
||||
cy.get('.discussions-sidebar').should('have.length', 0);
|
||||
cy.get('.reply').should('have.length', 0);
|
||||
|
||||
cy.get('.discussion-on-page .comment-field')
|
||||
.type('This comment is being made on a single thread discussion.')
|
||||
.should('have.value', 'This comment is being made on a single thread discussion.');
|
||||
|
||||
cy.get('.discussion-on-page .submit-discussion').click();
|
||||
cy.wait(3000);
|
||||
cy.get('.discussion-on-page').children(".reply-card").eq(-1).children(".reply-text")
|
||||
.should('have.text', 'This comment is being made on a single thread discussion.\n');
|
||||
};
|
||||
|
||||
it('reply through modal', reply_through_modal);
|
||||
it('reply through comment box', reply_through_comment_box);
|
||||
it('cancel and clear comment box', cancel_and_clear_comment_box);
|
||||
it('single thread discussion', single_thread_discussion);
|
||||
});
|
||||
|
|
@ -25,7 +25,7 @@ context('FileUploader', () => {
|
|||
|
||||
cy.get_open_dialog().find('.file-name').should('contain', 'example.json');
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-modal-primary').click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Upload'}).click();
|
||||
cy.wait('@upload_file').its('response.statusCode').should('eq', 200);
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
|
|
@ -33,11 +33,11 @@ context('FileUploader', () => {
|
|||
it('should accept uploaded files', () => {
|
||||
open_upload_dialog();
|
||||
|
||||
cy.get_open_dialog().find('.btn-file-upload div:contains("Library")').click();
|
||||
cy.get('.file-filter').type('example.json');
|
||||
cy.get_open_dialog().find('.tree-label:contains("example.json")').first().click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Library'}).click();
|
||||
cy.findByPlaceholderText('Search by filename or extension').type('example.json');
|
||||
cy.get_open_dialog().findAllByText('example.json').first().click();
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Upload'}).click();
|
||||
cy.wait('@upload_file').its('response.body.message')
|
||||
.should('have.property', 'file_name', 'example.json');
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
|
|
@ -46,12 +46,33 @@ context('FileUploader', () => {
|
|||
it('should accept web links', () => {
|
||||
open_upload_dialog();
|
||||
|
||||
cy.get_open_dialog().find('.btn-file-upload div:contains("Link")').click();
|
||||
cy.get_open_dialog().find('.file-web-link input').type('https://github.com', { delay: 100, force: true });
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Link'}).click();
|
||||
cy.get_open_dialog()
|
||||
.findByPlaceholderText('Attach a web link')
|
||||
.type('https://github.com', { delay: 100, force: true });
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Upload'}).click();
|
||||
cy.wait('@upload_file').its('response.body.message')
|
||||
.should('have.property', 'file_url', 'https://github.com');
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
|
||||
it('should allow cropping and optimization for valid images', () => {
|
||||
open_upload_dialog();
|
||||
|
||||
cy.get_open_dialog().find('.file-upload-area').attachFile('sample_image.jpg', {
|
||||
subjectType: 'drag-n-drop',
|
||||
});
|
||||
|
||||
cy.get_open_dialog().findAllByText('sample_image.jpg').should('exist');
|
||||
cy.get_open_dialog().find('.btn-crop').first().click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Crop'}).click();
|
||||
cy.get_open_dialog().findAllByRole('checkbox', {name: 'Optimize'}).should('exist');
|
||||
cy.get_open_dialog().findAllByLabelText('Optimize').first().click();
|
||||
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Upload'}).click();
|
||||
cy.wait('@upload_file').its('response.statusCode').should('eq', 200);
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
45
cypress/integration/first_day_of_the_week.js
Normal file
45
cypress/integration/first_day_of_the_week.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
context("First Day of the Week", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/app/system-settings');
|
||||
cy.findByText('Date and Number Format').click();
|
||||
});
|
||||
|
||||
it("Date control starts with same day as selected in System Settings", () => {
|
||||
cy.intercept('POST', '/api/method/frappe.core.doctype.system_settings.system_settings.load').as("load_settings");
|
||||
cy.fill_field('first_day_of_the_week', 'Tuesday', 'Select');
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.wait("@load_settings");
|
||||
cy.dialog({
|
||||
title: 'Date',
|
||||
fields: [
|
||||
{
|
||||
label: 'Date',
|
||||
fieldname: 'date',
|
||||
fieldtype: 'Date'
|
||||
}
|
||||
]
|
||||
});
|
||||
cy.get_field('date').click();
|
||||
cy.get('.datepicker--day-name').eq(0).should('have.text', 'Tu');
|
||||
});
|
||||
|
||||
it("Calendar view starts with same day as selected in System Settings", () => {
|
||||
cy.intercept('POST', '/api/method/frappe.core.doctype.system_settings.system_settings.load').as("load_settings");
|
||||
cy.fill_field('first_day_of_the_week', 'Monday', 'Select');
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.wait("@load_settings");
|
||||
cy.visit("app/todo/view/calendar/default");
|
||||
cy.get('.fc-day-header > span').eq(0).should('have.text', 'Mon');
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.visit('/app/system-settings');
|
||||
cy.findByText('Date and Number Format').click();
|
||||
cy.fill_field('first_day_of_the_week', 'Sunday', 'Select');
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
});
|
||||
});
|
||||
79
cypress/integration/folder_navigation.js
Normal file
79
cypress/integration/folder_navigation.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
context('Folder Navigation', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/file');
|
||||
});
|
||||
|
||||
it('Adding Folders', () => {
|
||||
//Adding filter to go into the home folder
|
||||
cy.get('.filter-selector > .btn').findByText('1 filter').click();
|
||||
cy.findByRole('button', {name: 'Clear Filters'}).click();
|
||||
cy.get('.filter-action-buttons > .text-muted').findByText('+ Add a Filter').click();
|
||||
cy.get('.fieldname-select-area > .awesomplete > .form-control').type('Fol{enter}');
|
||||
cy.get('.filter-field > .form-group > .link-field > .awesomplete > .input-with-feedback').type('Home{enter}');
|
||||
cy.get('.filter-action-buttons > div > .btn-primary').findByText('Apply Filters').click();
|
||||
|
||||
//Adding folder (Test Folder)
|
||||
cy.get('.menu-btn-group > .btn').click();
|
||||
cy.get('.menu-btn-group [data-label="New Folder"]').click();
|
||||
cy.get('form > [data-fieldname="value"]').type('Test Folder');
|
||||
cy.findByRole('button', {name: 'Create'}).click();
|
||||
});
|
||||
|
||||
it('Navigating the nested folders, checking if the URL formed is correct, checking if the added content in the child folder is correct', () => {
|
||||
//Navigating inside the Attachments folder
|
||||
cy.get('[title="Attachments"] > span').click();
|
||||
|
||||
//To check if the URL formed after visiting the attachments folder is correct
|
||||
cy.location('pathname').should('eq', '/app/file/view/home/Attachments');
|
||||
cy.visit('/app/file/view/home/Attachments');
|
||||
|
||||
//Adding folder inside the attachments folder
|
||||
cy.get('.menu-btn-group > .btn').click();
|
||||
cy.get('.menu-btn-group [data-label="New Folder"]').click();
|
||||
cy.get('form > [data-fieldname="value"]').type('Test Folder');
|
||||
cy.findByRole('button', {name: 'Create'}).click();
|
||||
|
||||
//Navigating inside the added folder in the Attachments folder
|
||||
cy.get('[title="Test Folder"] > span').click();
|
||||
|
||||
//To check if the URL is correct after visiting the Test Folder
|
||||
cy.location('pathname').should('eq', '/app/file/view/home/Attachments/Test%20Folder');
|
||||
cy.visit('/app/file/view/home/Attachments/Test%20Folder');
|
||||
|
||||
//Adding a file inside the Test Folder
|
||||
cy.findByRole('button', {name: 'Add File'}).eq(0).click({force: true});
|
||||
cy.get('.file-uploader').findByText('Link').click();
|
||||
cy.get('.input-group > .form-control').type('https://wallpaperplay.com/walls/full/8/2/b/72402.jpg');
|
||||
cy.findByRole('button', {name: 'Upload'}).click();
|
||||
|
||||
//To check if the added file is present in the Test Folder
|
||||
cy.get('span.level-item > span').should('contain', 'Test Folder');
|
||||
cy.get('.list-row-container').eq(0).should('contain.text', '72402.jpg');
|
||||
cy.get('.list-row-checkbox').eq(0).click();
|
||||
|
||||
//Deleting the added file from the Test folder
|
||||
cy.findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.actions-btn-group [data-label="Delete"]').click();
|
||||
cy.wait(700);
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
cy.wait(700);
|
||||
|
||||
//Deleting the Test Folder
|
||||
cy.visit('/app/file/view/home/Attachments');
|
||||
cy.get('.list-row-checkbox').eq(0).click();
|
||||
cy.findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.actions-btn-group [data-label="Delete"]').click();
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
});
|
||||
|
||||
it('Deleting Test Folder from the home', () => {
|
||||
//Deleting the Test Folder added in the home directory
|
||||
cy.visit('/app/file/view/home');
|
||||
cy.get('.level-left > .list-subject > .file-select >.list-row-checkbox').eq(0).click({force: true, delay: 500});
|
||||
cy.findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.actions-btn-group [data-label="Delete"]').click();
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
});
|
||||
});
|
||||
|
|
@ -8,8 +8,7 @@ context('Form', () => {
|
|||
});
|
||||
it('create a new form', () => {
|
||||
cy.visit('/app/todo/new');
|
||||
cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
|
||||
cy.wait(300);
|
||||
cy.get_field('description', 'Text Editor').type('this is a test todo', {force: true}).wait(200);
|
||||
cy.get('.page-title').should('contain', 'Not Saved');
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
|
|
@ -17,43 +16,64 @@ context('Form', () => {
|
|||
}).as('form_save');
|
||||
cy.get('.primary-action').click();
|
||||
cy.wait('@form_save').its('response.statusCode').should('eq', 200);
|
||||
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.title-text').should('be.visible').and('contain', 'To Do');
|
||||
cy.get('.page-head').findByTitle('To Do').should('exist');
|
||||
cy.get('.list-row').should('contain', 'this is a test todo');
|
||||
});
|
||||
|
||||
it('navigates between documents with child table list filters applied', () => {
|
||||
cy.visit('/app/contact');
|
||||
cy.add_filter();
|
||||
cy.get('.filter-field .input-with-feedback.form-control').type('123', { force: true });
|
||||
cy.get('.filter-popover .apply-filters').click({ force: true });
|
||||
cy.visit('/app/contact/Test Form Contact 3');
|
||||
|
||||
cy.clear_filters();
|
||||
cy.get('.standard-filter-section [data-fieldname="name"] input').type('Test Form Contact 3').blur();
|
||||
cy.click_listview_row_item(0);
|
||||
|
||||
cy.get('.prev-doc').should('be.visible').click();
|
||||
cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible');
|
||||
cy.hide_dialog();
|
||||
cy.get('.next-doc').click();
|
||||
cy.wait(200);
|
||||
|
||||
cy.get('.next-doc').should('be.visible').click();
|
||||
cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible');
|
||||
cy.hide_dialog();
|
||||
cy.contains('Test Form Contact 2').should('not.exist');
|
||||
cy.get('.title-text').should('contain', 'Test Form Contact 3');
|
||||
|
||||
cy.get('#page-Contact .page-head').findByTitle('Test Form Contact 3').should('exist');
|
||||
|
||||
// clear filters
|
||||
cy.visit('/app/contact');
|
||||
cy.clear_filters();
|
||||
});
|
||||
|
||||
it('validates behaviour of Data options validations in child table', () => {
|
||||
// test email validations for set_invalid controller
|
||||
let website_input = 'website.in';
|
||||
let valid_email = 'user@email.com';
|
||||
let expectBackgroundColor = 'rgb(255, 245, 245)';
|
||||
|
||||
cy.visit('/app/contact/new');
|
||||
cy.get('.frappe-control[data-fieldname="email_ids"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('.grid-body .rows [data-fieldname="email_id"]').click();
|
||||
cy.get('@table').find('input.input-with-feedback.form-control').as('email_input');
|
||||
cy.get('@email_input').type(website_input, { waitForAnimations: false });
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('@table').find('[data-idx="1"]').as('row1');
|
||||
cy.get('@table').find('[data-idx="2"]').as('row2');
|
||||
cy.get('@row1').click();
|
||||
cy.get('@row1').find('input.input-with-feedback.form-control').as('email_input1');
|
||||
|
||||
cy.get('@email_input1').type(website_input, { waitForAnimations: false });
|
||||
cy.fill_field('company_name', 'Test Company');
|
||||
cy.get('@email_input').should($div => {
|
||||
|
||||
cy.get('@row2').click();
|
||||
cy.get('@row2').find('input.input-with-feedback.form-control').as('email_input2');
|
||||
cy.get('@email_input2').type(valid_email, { waitForAnimations: false });
|
||||
|
||||
cy.get('@row1').click();
|
||||
cy.get('@email_input1').should($div => {
|
||||
const style = window.getComputedStyle($div[0]);
|
||||
expect(style.backgroundColor).to.equal(expectBackgroundColor);
|
||||
});
|
||||
cy.get('@email_input1').should('have.class', 'invalid');
|
||||
|
||||
cy.get('@row2').click();
|
||||
cy.get('@email_input2').should('not.have.class', 'invalid');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
31
cypress/integration/form_tab_break.js
Normal file
31
cypress/integration/form_tab_break.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import doctype_with_tab_break from '../fixtures/doctype_with_tab_break';
|
||||
const doctype_name = doctype_with_tab_break.name;
|
||||
context("Form Tab Break", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
return cy.insert_doc('DocType', doctype_with_tab_break, true);
|
||||
});
|
||||
it("Should switch tab and open correct tabs on validation error", () => {
|
||||
cy.new_form(doctype_name);
|
||||
// test tab switch
|
||||
cy.findByRole("tab", {name: "Tab 2"}).click();
|
||||
cy.findByText("Phone");
|
||||
cy.findByRole("tab", {name: "Details"}).click();
|
||||
cy.findByText("Name");
|
||||
|
||||
// form should switch to the tab with un-filled mandatory field
|
||||
cy.fill_field("username", "Test");
|
||||
cy.findByRole("button", {name: "Save"}).click();
|
||||
cy.findByText("Missing Fields");
|
||||
cy.hide_dialog();
|
||||
cy.findByText("Phone");
|
||||
cy.fill_field("phone", "12345678");
|
||||
cy.findByRole("button", {name: "Save"}).click();
|
||||
|
||||
// After save, first tab should have dashboard
|
||||
cy.get(".form-tabs > .nav-item").eq(0).click();
|
||||
cy.findByText("Connections");
|
||||
|
||||
});
|
||||
});
|
||||
88
cypress/integration/form_tour.js
Normal file
88
cypress/integration/form_tour.js
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
context('Form Tour', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/form-tour');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.call("frappe.tests.ui_test_helpers.create_form_tour");
|
||||
});
|
||||
});
|
||||
|
||||
const open_test_form_tour = () => {
|
||||
cy.visit('/app/form-tour/Test Form Tour');
|
||||
cy.findByRole('button', {name: 'Show Tour'}).should('be.visible').as('show_tour');
|
||||
cy.get('@show_tour').click();
|
||||
cy.wait(500);
|
||||
cy.url().should('include', '/app/contact');
|
||||
};
|
||||
|
||||
it('jump to a form tour', open_test_form_tour);
|
||||
|
||||
it('navigates a form tour', () => {
|
||||
open_test_form_tour();
|
||||
|
||||
cy.get('.frappe-driver').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname="first_name"]').as('first_name');
|
||||
cy.get('@first_name').should('have.class', 'driver-highlighted-element');
|
||||
cy.get('.frappe-driver').findByRole('button', {name: 'Next'}).as('next_btn');
|
||||
|
||||
// next btn shouldn't move to next step, if first name is not entered
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
cy.get('@first_name').should('have.class', 'driver-highlighted-element');
|
||||
|
||||
// after filling the field, next step should be highlighted
|
||||
cy.fill_field('first_name', 'Test Name', 'Data');
|
||||
cy.wait(500);
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert field is highlighted
|
||||
cy.get('.frappe-control[data-fieldname="last_name"]').as('last_name');
|
||||
cy.get('@last_name').should('have.class', 'driver-highlighted-element');
|
||||
|
||||
// after filling the field, next step should be highlighted
|
||||
cy.fill_field('last_name', 'Test Last Name', 'Data');
|
||||
cy.wait(500);
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert field is highlighted
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('phone_nos');
|
||||
cy.get('@phone_nos').should('have.class', 'driver-highlighted-element');
|
||||
|
||||
// move to next step
|
||||
cy.wait(500);
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert add row btn is highlighted
|
||||
cy.get('@phone_nos').find('.grid-add-row').as('add_row');
|
||||
cy.get('@add_row').should('have.class', 'driver-highlighted-element');
|
||||
|
||||
// add a row & move to next step
|
||||
cy.wait(500);
|
||||
cy.get('@add_row').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert table field is highlighted
|
||||
cy.get('.grid-row-open .frappe-control[data-fieldname="phone"]').as('phone');
|
||||
cy.get('@phone').should('have.class', 'driver-highlighted-element');
|
||||
// enter value in a table field
|
||||
let field = cy.fill_table_field('phone_nos', '1', 'phone', '1234567890');
|
||||
field.blur();
|
||||
|
||||
// move to collapse row step
|
||||
cy.wait(500);
|
||||
cy.get('.driver-popover-title').contains('Test Title 4').siblings().get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
// collapse row
|
||||
cy.get('.grid-row-open .grid-collapse-row').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert save btn is highlighted
|
||||
cy.get('.primary-action').should('have.class', 'driver-highlighted-element');
|
||||
cy.wait(500);
|
||||
cy.get('.frappe-driver').findByRole('button', {name: 'Save'}).should('be.visible');
|
||||
|
||||
});
|
||||
});
|
||||
92
cypress/integration/grid.js
Normal file
92
cypress/integration/grid.js
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
context('Grid', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.call("frappe.tests.ui_test_helpers.create_contact_phone_nos_records");
|
||||
});
|
||||
});
|
||||
it('update docfield property using update_docfield_property', () => {
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.window().its("cur_frm").then(frm => {
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
let field = frm.get_field("phone_nos");
|
||||
field.grid.update_docfield_property("is_primary_phone", "hidden", true);
|
||||
|
||||
cy.get('@table').find('[data-idx="1"] .edit-grid-row').click();
|
||||
cy.get('.grid-row-open').as('table-form');
|
||||
cy.get('@table-form').find('.frappe-control[data-fieldname="is_primary_phone"]').should("be.hidden");
|
||||
cy.get('@table-form').find('.grid-footer-toolbar').click();
|
||||
|
||||
cy.get('@table').find('[data-idx="2"] .edit-grid-row').click();
|
||||
cy.get('.grid-row-open').as('table-form');
|
||||
cy.get('@table-form').find('.frappe-control[data-fieldname="is_primary_phone"]').should("be.hidden");
|
||||
cy.get('@table-form').find('.grid-footer-toolbar').click();
|
||||
});
|
||||
});
|
||||
it('update docfield property using toggle_display', () => {
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.window().its("cur_frm").then(frm => {
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
let field = frm.get_field("phone_nos");
|
||||
field.grid.toggle_display("is_primary_mobile_no", false);
|
||||
|
||||
cy.get('@table').find('[data-idx="1"] .edit-grid-row').click();
|
||||
cy.get('.grid-row-open').as('table-form');
|
||||
cy.get('@table-form').find('.frappe-control[data-fieldname="is_primary_mobile_no"]').should("be.hidden");
|
||||
cy.get('@table-form').find('.grid-footer-toolbar').click();
|
||||
|
||||
cy.get('@table').find('[data-idx="2"] .edit-grid-row').click();
|
||||
cy.get('.grid-row-open').as('table-form');
|
||||
cy.get('@table-form').find('.frappe-control[data-fieldname="is_primary_mobile_no"]').should("be.hidden");
|
||||
cy.get('@table-form').find('.grid-footer-toolbar').click();
|
||||
});
|
||||
});
|
||||
it('update docfield property using toggle_enable', () => {
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.window().its("cur_frm").then(frm => {
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
let field = frm.get_field("phone_nos");
|
||||
field.grid.toggle_enable("phone", false);
|
||||
|
||||
|
||||
cy.get('@table').find('[data-idx="1"] .edit-grid-row').click();
|
||||
cy.get('.grid-row-open').as('table-form');
|
||||
cy.get('@table-form').find('.frappe-control[data-fieldname="phone"] .control-value').should('have.class', 'like-disabled-input');
|
||||
cy.get('@table-form').find('.grid-footer-toolbar').click();
|
||||
|
||||
cy.get('@table').find('[data-idx="2"] .edit-grid-row').click();
|
||||
cy.get('.grid-row-open').as('table-form');
|
||||
cy.get('@table-form').find('.frappe-control[data-fieldname="phone"] .control-value').should('have.class', 'like-disabled-input');
|
||||
cy.get('@table-form').find('.grid-footer-toolbar').click();
|
||||
});
|
||||
});
|
||||
it('update docfield property using toggle_reqd', () => {
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.window().its("cur_frm").then(frm => {
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
let field = frm.get_field("phone_nos");
|
||||
field.grid.toggle_reqd("phone", false);
|
||||
|
||||
cy.get('@table').find('[data-idx="1"] .edit-grid-row').click();
|
||||
cy.get('.grid-row-open').as('table-form');
|
||||
cy.get_field("phone").as('phone-field');
|
||||
cy.get('@phone-field').focus().clear().wait(500).blur();
|
||||
cy.get('@phone-field').should("not.have.class", "has-error");
|
||||
cy.get('@table-form').find('.grid-footer-toolbar').click();
|
||||
|
||||
cy.get('@table').find('[data-idx="2"] .edit-grid-row').click();
|
||||
cy.get('.grid-row-open').as('table-form');
|
||||
cy.get_field("phone").as('phone-field');
|
||||
cy.get('@phone-field').focus().clear().wait(500).blur();
|
||||
cy.get('@phone-field').should("not.have.class", "has-error");
|
||||
cy.get('@table-form').find('.grid-footer-toolbar').click();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
23
cypress/integration/grid_configuration.js
Normal file
23
cypress/integration/grid_configuration.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
context('Grid Configuration', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/doctype/User');
|
||||
});
|
||||
it('Set user wise grid settings', () => {
|
||||
cy.wait(100);
|
||||
cy.get('.frappe-control[data-fieldname="fields"]').as('table');
|
||||
cy.get('@table').find('.icon-sm').click();
|
||||
cy.wait(100);
|
||||
cy.get('.frappe-control[data-fieldname="fields_html"]').as('modal');
|
||||
cy.get('@modal').find('.add-new-fields').click();
|
||||
cy.wait(100);
|
||||
cy.get('[type="checkbox"][data-unit="read_only"]').check();
|
||||
cy.findByRole('button', {name: 'Add'}).click();
|
||||
cy.wait(100);
|
||||
cy.get('[data-fieldname="options"]').invoke('attr', 'value', '1');
|
||||
cy.get('.form-control.column-width[data-fieldname="options"]').trigger('change');
|
||||
cy.findByRole('button', {name: 'Update'}).click();
|
||||
cy.wait(200);
|
||||
cy.get('[title="Read Only"').should('be.visible');
|
||||
});
|
||||
});
|
||||
40
cypress/integration/grid_keyboard_shortcut.js
Normal file
40
cypress/integration/grid_keyboard_shortcut.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
context('Grid Keyboard Shortcut', () => {
|
||||
let total_count = 0;
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
beforeEach(() => {
|
||||
cy.reload();
|
||||
cy.visit('/app/contact/new-contact-1');
|
||||
cy.get('.frappe-control[data-fieldname="email_ids"]').find(".grid-add-row").click();
|
||||
});
|
||||
it('Insert new row at the end', () => {
|
||||
cy.add_new_row_in_grid('{ctrl}{shift}{downarrow}', (cy, total_count) => {
|
||||
cy.get('[data-name="new-contact-email-1"]').should('have.attr', 'data-idx', `${total_count+1}`);
|
||||
}, total_count);
|
||||
});
|
||||
it('Insert new row at the top', () => {
|
||||
cy.add_new_row_in_grid('{ctrl}{shift}{uparrow}', (cy) => {
|
||||
cy.get('[data-name="new-contact-email-1"]').should('have.attr', 'data-idx', '2');
|
||||
});
|
||||
});
|
||||
it('Insert new row below', () => {
|
||||
cy.add_new_row_in_grid('{ctrl}{downarrow}', (cy) => {
|
||||
cy.get('[data-name="new-contact-email-1"]').should('have.attr', 'data-idx', '1');
|
||||
});
|
||||
});
|
||||
it('Insert new row above', () => {
|
||||
cy.add_new_row_in_grid('{ctrl}{uparrow}', (cy) => {
|
||||
cy.get('[data-name="new-contact-email-1"]').should('have.attr', 'data-idx', '2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('add_new_row_in_grid', (shortcut_keys, callbackFn, total_count) => {
|
||||
cy.get('.frappe-control[data-fieldname="email_ids"]').as('table');
|
||||
cy.get('@table').find('.grid-body [data-fieldname="email_id"]').first().click();
|
||||
cy.get('@table').find('.grid-body [data-fieldname="email_id"]')
|
||||
.first().type(shortcut_keys);
|
||||
|
||||
callbackFn(cy, total_count);
|
||||
});
|
||||
|
|
@ -13,7 +13,7 @@ context('Grid Pagination', () => {
|
|||
it('creates pages for child table', () => {
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
cy.get('@table').find('.current-page-number').should('contain', '1');
|
||||
cy.get('@table').find('.current-page-number').should('have.value', '1');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '20');
|
||||
cy.get('@table').find('.grid-body .grid-row').should('have.length', 50);
|
||||
});
|
||||
|
|
@ -21,25 +21,46 @@ context('Grid Pagination', () => {
|
|||
cy.visit('/app/contact/Test Contact');
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
cy.get('@table').find('.next-page').click();
|
||||
cy.get('@table').find('.current-page-number').should('contain', '2');
|
||||
cy.get('@table').find('.current-page-number').should('have.value', '2');
|
||||
cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '51');
|
||||
cy.get('@table').find('.prev-page').click();
|
||||
cy.get('@table').find('.current-page-number').should('contain', '1');
|
||||
cy.get('@table').find('.current-page-number').should('have.value', '1');
|
||||
cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '1');
|
||||
});
|
||||
it('adds and deletes rows and changes page', () => {
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('@table').findByRole('button', {name: 'Add Row'}).click();
|
||||
cy.get('@table').find('.grid-body .row-index').should('contain', 1001);
|
||||
cy.get('@table').find('.current-page-number').should('contain', '21');
|
||||
cy.get('@table').find('.current-page-number').should('have.value', '21');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '21');
|
||||
cy.get('@table').find('.grid-body .grid-row .grid-row-check').click({ force: true });
|
||||
cy.get('@table').find('button.grid-remove-rows').click();
|
||||
cy.get('@table').findByRole('button', {name: 'Delete'}).click();
|
||||
cy.get('@table').find('.grid-body .row-index').last().should('contain', 1000);
|
||||
cy.get('@table').find('.current-page-number').should('contain', '20');
|
||||
cy.get('@table').find('.current-page-number').should('have.value', '20');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '20');
|
||||
});
|
||||
it('go to specific page, use up and down arrow, type characters, 0 page and more than existing page', () => {
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
cy.get('@table').find('.current-page-number').focus().clear().type('17').blur();
|
||||
cy.get('@table').find('.grid-body .row-index').should('contain', 801);
|
||||
|
||||
cy.get('@table').find('.current-page-number').focus().type('{uparrow}{uparrow}');
|
||||
cy.get('@table').find('.current-page-number').should('have.value', '19');
|
||||
|
||||
cy.get('@table').find('.current-page-number').focus().type('{downarrow}{downarrow}');
|
||||
cy.get('@table').find('.current-page-number').should('have.value', '17');
|
||||
|
||||
cy.get('@table').find('.current-page-number').focus().clear().type('700').blur();
|
||||
cy.get('@table').find('.current-page-number').should('have.value', '20');
|
||||
|
||||
cy.get('@table').find('.current-page-number').focus().clear().type('0').blur();
|
||||
cy.get('@table').find('.current-page-number').should('have.value', '1');
|
||||
|
||||
cy.get('@table').find('.current-page-number').focus().clear().type('abc').blur();
|
||||
cy.get('@table').find('.current-page-number').should('have.value', '1');
|
||||
});
|
||||
// it('deletes all rows', ()=> {
|
||||
// cy.visit('/app/contact/Test Contact');
|
||||
// cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
|
|
|
|||
107
cypress/integration/grid_search.js
Normal file
107
cypress/integration/grid_search.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import doctype_with_child_table from '../fixtures/doctype_with_child_table';
|
||||
import child_table_doctype from '../fixtures/child_table_doctype';
|
||||
import child_table_doctype_1 from '../fixtures/child_table_doctype_1';
|
||||
const doctype_with_child_table_name = doctype_with_child_table.name;
|
||||
|
||||
context('Grid Search', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
cy.insert_doc('DocType', child_table_doctype, true);
|
||||
cy.insert_doc('DocType', child_table_doctype_1, true);
|
||||
cy.insert_doc('DocType', doctype_with_child_table, true);
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.xcall("frappe.tests.ui_test_helpers.insert_doctype_with_child_table_record", {
|
||||
name: doctype_with_child_table_name
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Test search row visibility', () => {
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
frappe.model.user_settings.save('Doctype With Child Table', 'GridView', {
|
||||
'Child Table Doctype 1': [
|
||||
{'fieldname': 'data', 'columns': 2},
|
||||
{'fieldname': 'barcode', 'columns': 1},
|
||||
{'fieldname': 'check', 'columns': 1},
|
||||
{'fieldname': 'rating', 'columns': 2},
|
||||
{'fieldname': 'duration', 'columns': 2},
|
||||
{'fieldname': 'date', 'columns': 2}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
cy.visit(`/app/doctype-with-child-table/Test Grid Search`);
|
||||
|
||||
cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table');
|
||||
cy.get('@table').find('.grid-row-check:last').click();
|
||||
cy.get('@table').find('.grid-footer').contains('Delete').click();
|
||||
cy.get('.grid-heading-row .grid-row .search').should('not.exist');
|
||||
});
|
||||
|
||||
it('test search field for different fieldtypes', () => {
|
||||
cy.visit(`/app/doctype-with-child-table/Test Grid Search`);
|
||||
|
||||
cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table');
|
||||
|
||||
// Index Column
|
||||
cy.get('@table').find('.grid-heading-row .row-index.search input').type('3');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 2);
|
||||
cy.get('@table').find('.grid-heading-row .row-index.search input').clear();
|
||||
|
||||
// Data Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').type('Data');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 1);
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').clear();
|
||||
|
||||
// Barcode Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').type('092');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 4);
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').clear();
|
||||
|
||||
// Check Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').type('1');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 9);
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').clear();
|
||||
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').type('0');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 11);
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').clear();
|
||||
|
||||
// Rating Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Rating"]').type('3');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 3);
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Rating"]').clear();
|
||||
|
||||
// Duration Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').type('3d');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 3);
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').clear();
|
||||
|
||||
// Date Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').type('2022');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 4);
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').clear();
|
||||
});
|
||||
|
||||
it('test with multiple filter', () => {
|
||||
cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table');
|
||||
|
||||
// Data Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').type('a');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 10);
|
||||
|
||||
// Barcode Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').type('0');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 8);
|
||||
|
||||
// Duration Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').type('d');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 5);
|
||||
|
||||
// Date Column
|
||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').type('02-');
|
||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 2);
|
||||
});
|
||||
});
|
||||
38
cypress/integration/list_paging.js
Normal file
38
cypress/integration/list_paging.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
context('List Paging', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.call("frappe.tests.ui_test_helpers.create_multiple_todo_records");
|
||||
});
|
||||
});
|
||||
|
||||
it('test load more with count selection buttons', () => {
|
||||
cy.visit('/app/todo/view/report');
|
||||
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '20 of');
|
||||
cy.get('.list-paging-area .btn-more').click();
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '40 of');
|
||||
cy.get('.list-paging-area .btn-more').click();
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '60 of');
|
||||
|
||||
cy.get('.list-paging-area .btn-group .btn-paging[data-value="100"]').click();
|
||||
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '100 of');
|
||||
cy.get('.list-paging-area .btn-more').click();
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '200 of');
|
||||
cy.get('.list-paging-area .btn-more').click();
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '300 of');
|
||||
|
||||
// check if refresh works after load more
|
||||
cy.get('.page-head .standard-actions [data-original-title="Refresh"]').click();
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '300 of');
|
||||
|
||||
cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click();
|
||||
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '500 of');
|
||||
cy.get('.list-paging-area .btn-more').click();
|
||||
|
||||
cy.get('.list-paging-area .list-count').should('contain.text', '1000 of');
|
||||
});
|
||||
});
|
||||
|
|
@ -6,12 +6,24 @@ context('List View', () => {
|
|||
return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow");
|
||||
});
|
||||
});
|
||||
|
||||
it('Keep checkbox checked after Refresh', () => {
|
||||
cy.go_to_list('ToDo');
|
||||
cy.get('.list-row-container .list-row-checkbox').click({ multiple: true, force: true });
|
||||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible');
|
||||
cy.intercept('/api/method/frappe.desk.reportview.get').as('list-refresh');
|
||||
cy.wait(3000); // wait before you hit another refresh
|
||||
cy.get('button[data-original-title="Refresh"]').click();
|
||||
cy.wait('@list-refresh');
|
||||
cy.get('.list-row-container .list-row-checkbox:checked').should('be.visible');
|
||||
});
|
||||
|
||||
it('enables "Actions" button', () => {
|
||||
const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete'];
|
||||
const actions = ['Approve', 'Reject', 'Edit', 'Export', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete'];
|
||||
cy.go_to_list('ToDo');
|
||||
cy.get('.list-row-container:contains("Pending") .list-row-checkbox').click({ multiple: true, force: true });
|
||||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click();
|
||||
cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 8).each((el, index) => {
|
||||
cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 9).each((el, index) => {
|
||||
cy.wrap(el).contains(actions[index]);
|
||||
}).then((elements) => {
|
||||
cy.intercept({
|
||||
|
|
@ -24,10 +36,11 @@ context('List View', () => {
|
|||
}).as('real-time-update');
|
||||
cy.wrap(elements).contains('Approve').click();
|
||||
cy.wait(['@bulk-approval', '@real-time-update']);
|
||||
cy.hide_dialog();
|
||||
cy.wait(300);
|
||||
cy.get_open_dialog().find('.btn-modal-close').click();
|
||||
cy.reload();
|
||||
cy.clear_filters();
|
||||
cy.get('.list-row-container:visible').should('contain', 'Approved');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ context('List View Settings', () => {
|
|||
cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click();
|
||||
cy.get('.modal-dialog').should('contain', 'DocType Settings');
|
||||
|
||||
cy.get('input[data-fieldname="disable_count"]').check({ force: true });
|
||||
cy.get('input[data-fieldname="disable_sidebar_stats"]').check({ force: true });
|
||||
cy.get('button').filter(':visible').contains('Save').click();
|
||||
cy.findByLabelText('Disable Count').check({ force: true });
|
||||
cy.findByLabelText('Disable Sidebar Stats').check({ force: true });
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
|
||||
cy.reload({ force: true });
|
||||
|
||||
|
|
@ -29,8 +29,8 @@ context('List View Settings', () => {
|
|||
cy.get('.menu-btn-group button').click({ force: true });
|
||||
cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click();
|
||||
cy.get('.modal-dialog').should('contain', 'DocType Settings');
|
||||
cy.get('input[data-fieldname="disable_count"]').uncheck({ force: true });
|
||||
cy.get('input[data-fieldname="disable_sidebar_stats"]').uncheck({ force: true });
|
||||
cy.get('button').filter(':visible').contains('Save').click();
|
||||
cy.findByLabelText('Disable Count').uncheck({ force: true });
|
||||
cy.findByLabelText('Disable Sidebar Stats').uncheck({ force: true });
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ context('Login', () => {
|
|||
|
||||
it('validates password', () => {
|
||||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.findByRole('button', {name: 'Login'}).click();
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
});
|
||||
|
||||
it('validates email', () => {
|
||||
cy.get('#login_password').type('qwe');
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.findByRole('button', {name: 'Login'}).click();
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
});
|
||||
|
||||
|
|
@ -25,8 +25,8 @@ context('Login', () => {
|
|||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('#login_password').type('qwer');
|
||||
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.get('.btn-login:visible').contains('Invalid Login. Try again.');
|
||||
cy.findByRole('button', {name: 'Login'}).click();
|
||||
cy.findByRole('button', {name: 'Invalid Login. Try again.'}).should('exist');
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
});
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ context('Login', () => {
|
|||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('#login_password').type(Cypress.config('adminPassword'));
|
||||
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.findByRole('button', {name: 'Login'}).click();
|
||||
cy.location('pathname').should('eq', '/app');
|
||||
cy.window().its('frappe.session.user').should('eq', 'Administrator');
|
||||
});
|
||||
|
|
@ -60,7 +60,7 @@ context('Login', () => {
|
|||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('#login_password').type(Cypress.config('adminPassword'));
|
||||
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.findByRole('button', {name: 'Login'}).click();
|
||||
|
||||
// verify redirected location and url params after login
|
||||
cy.url().should('include', '/me?' + payload.toString().replace('+', '%20'));
|
||||
|
|
|
|||
99
cypress/integration/multi_select_dialog.js
Normal file
99
cypress/integration/multi_select_dialog.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
context('MultiSelectDialog', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app');
|
||||
const contact_template = {
|
||||
"doctype": "Contact",
|
||||
"first_name": "Test",
|
||||
"status": "Passive",
|
||||
"email_ids": [
|
||||
{
|
||||
"doctype": "Contact Email",
|
||||
"email_id": "test@example.com",
|
||||
"is_primary": 0
|
||||
}
|
||||
]
|
||||
};
|
||||
const promises = Array.from({length: 25})
|
||||
.map(() => cy.insert_doc('Contact', contact_template, true));
|
||||
Promise.all(promises);
|
||||
});
|
||||
|
||||
function open_multi_select_dialog() {
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
new frappe.ui.form.MultiSelectDialog({
|
||||
doctype: "Contact",
|
||||
target: {},
|
||||
setters: {
|
||||
status: null,
|
||||
gender: null
|
||||
},
|
||||
add_filters_group: 1,
|
||||
allow_child_item_selection: 1,
|
||||
child_fieldname: "email_ids",
|
||||
child_columns: ["email_id", "is_primary"]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('checks multi select dialog api works', () => {
|
||||
open_multi_select_dialog();
|
||||
cy.get_open_dialog().should('contain', 'Select Contacts');
|
||||
});
|
||||
|
||||
it('checks for filters', () => {
|
||||
['search_term', 'status', 'gender'].forEach(fieldname => {
|
||||
cy.get_open_dialog().get(`.frappe-control[data-fieldname="${fieldname}"]`).should('exist');
|
||||
});
|
||||
|
||||
// add_filters_group: 1 should add a filter group
|
||||
cy.get_open_dialog().get(`.frappe-control[data-fieldname="filter_area"]`).should('exist');
|
||||
|
||||
});
|
||||
|
||||
it('checks for child item selection', () => {
|
||||
cy.get_open_dialog()
|
||||
.get(`.dt-row-header`).should('not.exist');
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.frappe-control[data-fieldname="allow_child_item_selection"]`)
|
||||
.find('input[data-fieldname="allow_child_item_selection"]')
|
||||
.should('exist')
|
||||
.click({force: true});
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.frappe-control[data-fieldname="child_selection_area"]`)
|
||||
.should('exist');
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.dt-row-header`).should('contain', 'Contact');
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.dt-row-header`).should('contain', 'Email Id');
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.dt-row-header`).should('contain', 'Is Primary');
|
||||
});
|
||||
|
||||
it('tests more button', () => {
|
||||
cy.get_open_dialog()
|
||||
.get(`.frappe-control[data-fieldname="more_child_btn"]`)
|
||||
.should('exist')
|
||||
.as('more-btn');
|
||||
|
||||
cy.get_open_dialog().get('.datatable .dt-scrollable .dt-row').should(($rows) => {
|
||||
expect($rows).to.have.length(20);
|
||||
});
|
||||
|
||||
cy.intercept('POST', 'api/method/frappe.client.get_list').as('get-more-records');
|
||||
cy.get('@more-btn').find('button').click({force: true});
|
||||
cy.wait('@get-more-records');
|
||||
|
||||
cy.get_open_dialog().get('.datatable .dt-scrollable .dt-row').should(($rows) => {
|
||||
if ($rows.length <= 20) {
|
||||
throw new Error("More button doesn't work");
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
25
cypress/integration/navigation.js
Normal file
25
cypress/integration/navigation.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
context('Navigation', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
it('Navigate to route with hash in document name', () => {
|
||||
cy.insert_doc('ToDo', {'__newname': 'ABC#123', 'description': 'Test this', 'ignore_duplicate': true});
|
||||
cy.visit('/app/todo/ABC#123');
|
||||
cy.title().should('eq', 'Test this - ABC#123');
|
||||
cy.get_field('description', 'Text Editor').contains('Test this');
|
||||
cy.go('back');
|
||||
cy.title().should('eq', 'Website');
|
||||
});
|
||||
|
||||
it.only('Navigate to previous page after login', () => {
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.page-head').findByTitle('To Do').should('be.visible');
|
||||
cy.request('/api/method/logout');
|
||||
cy.reload().as('reload');
|
||||
cy.get('@reload').get('.page-card .btn-primary').contains('Login').click();
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
cy.login();
|
||||
cy.visit('/app');
|
||||
cy.location('pathname').should('eq', '/app/todo');
|
||||
});
|
||||
});
|
||||
22
cypress/integration/number_card.js
Normal file
22
cypress/integration/number_card.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
context('Number Card', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
it('Check filter populate for child table doctype', () => {
|
||||
cy.visit('/app/number-card/new-number-card-1');
|
||||
cy.get('[data-fieldname="parent_document_type"]').should('have.css', 'display', 'none');
|
||||
|
||||
cy.get_field('document_type', 'Link');
|
||||
cy.fill_field('document_type', 'Workspace Link', 'Link').focus().blur();
|
||||
cy.get_field('document_type', 'Link').should('have.value', 'Workspace Link');
|
||||
|
||||
cy.fill_field('label', 'Test Number Card', 'Data');
|
||||
|
||||
cy.get('[data-fieldname="filters_json"]').click().wait(200);
|
||||
cy.get('.modal-body .filter-action-buttons .add-filter').click();
|
||||
cy.get('.modal-body .fieldname-select-area').click();
|
||||
cy.get('.modal-actions .btn-modal-close').click();
|
||||
});
|
||||
});
|
||||
|
|
@ -2,32 +2,62 @@ context('Query Report', () => {
|
|||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
cy.insert_doc('Report', {
|
||||
'report_name': 'Test ToDo Report',
|
||||
'ref_doctype': 'ToDo',
|
||||
'report_type': 'Query Report',
|
||||
'query': 'select * from tabToDo'
|
||||
}, true).as('doc');
|
||||
cy.create_records({
|
||||
doctype: 'ToDo',
|
||||
description: 'this is a test todo for query report'
|
||||
}).as('todos');
|
||||
});
|
||||
|
||||
it('add custom column in report', () => {
|
||||
cy.visit('/app/query-report/Permitted Documents For User');
|
||||
|
||||
cy.get('.page-form.flex', { timeout: 60000 }).should('have.length', 1).then(() => {
|
||||
cy.get('#page-query-report input[data-fieldname="user"]').as('input');
|
||||
cy.get('@input').focus().type('test@erpnext.com', { delay: 100 }).blur();
|
||||
cy.get('#page-query-report input[data-fieldname="user"]').as('input-user');
|
||||
cy.get('@input-user').focus().type('test@erpnext.com', { delay: 100 }).blur();
|
||||
cy.wait(300);
|
||||
cy.get('#page-query-report input[data-fieldname="doctype"]').as('input-test');
|
||||
cy.get('@input-test').focus().type('Role', { delay: 100 }).blur();
|
||||
cy.get('#page-query-report input[data-fieldname="doctype"]').as('input-role');
|
||||
cy.get('@input-role').focus().type('Role', { delay: 100 }).blur();
|
||||
|
||||
cy.get('.datatable').should('exist');
|
||||
cy.get('.menu-btn-group button').click({ force: true });
|
||||
cy.get('.dropdown-menu li').contains('Add Column').click({ force: true });
|
||||
cy.get('.modal-dialog').should('contain', 'Add Column');
|
||||
cy.get('#page-query-report .page-actions .menu-btn-group button').click({ force: true });
|
||||
cy.get('#page-query-report .menu-btn-group .dropdown-menu').contains('Add Column').click({ force: true });
|
||||
cy.get_open_dialog().get('.modal-title').should('contain', 'Add Column');
|
||||
cy.get('select[data-fieldname="doctype"]').select("Role", { force: true });
|
||||
cy.get('select[data-fieldname="field"]').select("Role Name", { force: true });
|
||||
cy.get('select[data-fieldname="insert_after"]').select("Name", { force: true });
|
||||
cy.get('button').contains('Submit').click({ force: true });
|
||||
cy.get('.menu-btn-group button').click({ force: true });
|
||||
cy.get('.dropdown-menu li').contains('Save').click({ force: true });
|
||||
cy.get('.modal-dialog').should('contain', 'Save Report');
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Submit'}).click({ force: true });
|
||||
cy.get('#page-query-report .page-actions .menu-btn-group button').click({ force: true });
|
||||
cy.get('#page-query-report .menu-btn-group .dropdown-menu').contains('Save').click({ timeout: 100, force: true });
|
||||
cy.get_open_dialog().get('.modal-title').should('contain', 'Save Report');
|
||||
|
||||
cy.get('input[data-fieldname="report_name"]').type("Test Report", { delay: 100, force: true });
|
||||
cy.get('button').contains('Submit').click({ timeout: 1000, force: true });
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Submit'}).click({ timeout: 1000, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
let save_report_and_open = (report, update_name) => {
|
||||
cy.get('#page-query-report .page-actions .menu-btn-group button').click({ force: true });
|
||||
cy.get('#page-query-report .menu-btn-group .dropdown-menu').contains('Save').click({ timeout: 100, force: true });
|
||||
cy.get_open_dialog().get('.modal-title').should('contain', 'Save Report');
|
||||
|
||||
cy.get('input[data-fieldname="report_name"]').type(update_name, { delay: 100, force: true });
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Submit'}).click({ timeout: 1000, force: true });
|
||||
|
||||
cy.visit('/app/query-report/'+report);
|
||||
cy.get('.datatable').should('exist');
|
||||
};
|
||||
|
||||
it('test multi level query report', () => {
|
||||
cy.visit('/app/query-report/Test ToDo Report');
|
||||
cy.get('.datatable').should('exist');
|
||||
|
||||
save_report_and_open('Test ToDo Report 1', ' 1');
|
||||
save_report_and_open('Test ToDo Report 11', '1');
|
||||
});
|
||||
});
|
||||
|
|
@ -3,70 +3,64 @@ context('Recorder', () => {
|
|||
cy.login();
|
||||
});
|
||||
|
||||
it('Navigate to Recorder', () => {
|
||||
cy.visit('/app');
|
||||
cy.awesomebar('recorder');
|
||||
cy.get('h3').should('contain', 'Recorder');
|
||||
cy.url().should('include', '/recorder/detail');
|
||||
beforeEach(() => {
|
||||
cy.visit('/app/recorder');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
// reset recorder
|
||||
return frappe.xcall("frappe.recorder.stop").then(() => {
|
||||
return frappe.xcall("frappe.recorder.delete");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Recorder Empty State', () => {
|
||||
cy.visit('/app/recorder');
|
||||
cy.get('.title-text').should('contain', 'Recorder');
|
||||
cy.get('.page-head').findByTitle('Recorder').should('exist');
|
||||
|
||||
cy.get('.indicator-pill').should('contain', 'Inactive').should('have.class', 'red');
|
||||
|
||||
cy.get('.primary-action').should('contain', 'Start');
|
||||
cy.get('.btn-secondary').should('contain', 'Clear');
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Start'}).should('exist');
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Clear'}).should('exist');
|
||||
|
||||
cy.get('.msg-box').should('contain', 'Inactive');
|
||||
cy.get('.msg-box .btn-primary').should('contain', 'Start Recording');
|
||||
cy.get('.msg-box').should('contain', 'Recorder is Inactive');
|
||||
cy.get('.msg-box').findByRole('button', {name: 'Start Recording'}).should('exist');
|
||||
});
|
||||
|
||||
it('Recorder Start', () => {
|
||||
cy.visit('/app/recorder');
|
||||
cy.get('.primary-action').should('contain', 'Start').click();
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Start'}).click();
|
||||
cy.get('.indicator-pill').should('contain', 'Active').should('have.class', 'green');
|
||||
|
||||
cy.get('.msg-box').should('contain', 'No Requests');
|
||||
cy.get('.msg-box').should('contain', 'No Requests found');
|
||||
|
||||
cy.visit('/app/List/DocType/List');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.wait('@list_refresh');
|
||||
|
||||
cy.get('.title-text').should('contain', 'DocType');
|
||||
cy.get('.page-head').findByTitle('DocType').should('exist');
|
||||
cy.get('.list-count').should('contain', '20 of ');
|
||||
|
||||
cy.visit('/app/recorder');
|
||||
cy.get('.title-text').should('contain', 'Recorder');
|
||||
cy.get('.result-list').should('contain', '/api/method/frappe.desk.reportview.get');
|
||||
|
||||
cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
|
||||
cy.wait(500);
|
||||
cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
|
||||
cy.get('.msg-box').should('contain', 'Inactive');
|
||||
cy.get('.page-head').findByTitle('Recorder').should('exist');
|
||||
cy.get('.frappe-list .result-list').should('contain', '/api/method/frappe.desk.reportview.get');
|
||||
});
|
||||
|
||||
it('Recorder View Request', () => {
|
||||
cy.visit('/app/recorder');
|
||||
cy.get('.primary-action').should('contain', 'Start').click();
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Start'}).click();
|
||||
|
||||
cy.visit('/app/List/DocType/List');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.wait('@list_refresh');
|
||||
|
||||
cy.get('.title-text').should('contain', 'DocType');
|
||||
cy.get('.page-head').findByTitle('DocType').should('exist');
|
||||
cy.get('.list-count').should('contain', '20 of ');
|
||||
|
||||
cy.visit('/app/recorder');
|
||||
|
||||
cy.get('.list-row-container span').contains('/api/method/frappe').click();
|
||||
cy.get('.frappe-list .list-row-container span')
|
||||
.contains('/api/method/frappe')
|
||||
.should('be.visible')
|
||||
.click({force: true});
|
||||
|
||||
cy.url().should('include', '/recorder/request');
|
||||
cy.get('form').should('contain', '/api/method/frappe');
|
||||
|
||||
cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
|
||||
cy.wait(200);
|
||||
cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,47 +1,47 @@
|
|||
context('Relative Timeframe', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
|
||||
});
|
||||
});
|
||||
it('sets relative timespan filter for last week and filters list', () => {
|
||||
cy.visit('/app/List/ToDo/List');
|
||||
cy.clear_filters();
|
||||
cy.get('.list-row:contains("this is fourth todo")').should('exist');
|
||||
cy.add_filter();
|
||||
cy.get('.fieldname-select-area').should('exist');
|
||||
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
|
||||
cy.get('select.condition.form-control').select("Timespan");
|
||||
cy.get('.filter-field select.input-with-feedback.form-control').select("last week");
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.get('.filter-popover .apply-filters').click({ force: true });
|
||||
cy.wait('@list_refresh');
|
||||
cy.get('.list-row-container').its('length').should('eq', 1);
|
||||
cy.get('.list-row-container').should('contain', 'this is second todo');
|
||||
cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
|
||||
.as('save_user_settings');
|
||||
cy.clear_filters();
|
||||
cy.wait('@save_user_settings');
|
||||
});
|
||||
it('sets relative timespan filter for next week and filters list', () => {
|
||||
cy.visit('/app/List/ToDo/List');
|
||||
cy.clear_filters();
|
||||
cy.get('.list-row:contains("this is fourth todo")').should('exist');
|
||||
cy.add_filter();
|
||||
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
|
||||
cy.get('select.condition.form-control').select("Timespan");
|
||||
cy.get('.filter-field select.input-with-feedback.form-control').select("next week");
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.get('.filter-popover .apply-filters').click({ force: true });
|
||||
cy.wait('@list_refresh');
|
||||
cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
|
||||
.as('save_user_settings');
|
||||
cy.clear_filters();
|
||||
cy.wait('@save_user_settings');
|
||||
});
|
||||
});
|
||||
// TODO: Enable this again
|
||||
// currently this is flaky possibly because of different timezone in CI
|
||||
|
||||
// context('Relative Timeframe', () => {
|
||||
// before(() => {
|
||||
// cy.login();
|
||||
// cy.visit('/app/website');
|
||||
// cy.window().its('frappe').then(frappe => {
|
||||
// frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
|
||||
// });
|
||||
// });
|
||||
// it('sets relative timespan filter for last week and filters list', () => {
|
||||
// cy.visit('/app/List/ToDo/List');
|
||||
// cy.clear_filters();
|
||||
// cy.get('.list-row:contains("this is fourth todo")').should('exist');
|
||||
// cy.add_filter();
|
||||
// cy.get('.fieldname-select-area').should('exist');
|
||||
// cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
|
||||
// cy.get('select.condition.form-control').select("Timespan");
|
||||
// cy.get('.filter-field select.input-with-feedback.form-control').select("last week");
|
||||
// cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
// cy.get('.filter-popover .apply-filters').click({ force: true });
|
||||
// cy.wait('@list_refresh');
|
||||
// cy.get('.list-row-container').its('length').should('eq', 1);
|
||||
// cy.get('.list-row-container').should('contain', 'this is second todo');
|
||||
// cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
|
||||
// .as('save_user_settings');
|
||||
// cy.clear_filters();
|
||||
// cy.wait('@save_user_settings');
|
||||
// });
|
||||
// it('sets relative timespan filter for next week and filters list', () => {
|
||||
// cy.visit('/app/List/ToDo/List');
|
||||
// cy.clear_filters();
|
||||
// cy.get('.list-row:contains("this is fourth todo")').should('exist');
|
||||
// cy.add_filter();
|
||||
// cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
|
||||
// cy.get('select.condition.form-control').select("Timespan");
|
||||
// cy.get('.filter-field select.input-with-feedback.form-control').select("next week");
|
||||
// cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
// cy.get('.filter-popover .apply-filters').click({ force: true });
|
||||
// cy.wait('@list_refresh');
|
||||
// cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
|
||||
// .as('save_user_settings');
|
||||
// cy.clear_filters();
|
||||
// cy.wait('@save_user_settings');
|
||||
// });
|
||||
// });
|
||||
|
|
|
|||
|
|
@ -11,31 +11,33 @@ context('Report View', () => {
|
|||
'title': 'Doc 1',
|
||||
'description': 'Random Text',
|
||||
'enabled': 0,
|
||||
// submit document
|
||||
'docstatus': 1
|
||||
}, true).as('doc');
|
||||
'docstatus': 1 // submit document
|
||||
}, true);
|
||||
});
|
||||
|
||||
it('Field with enabled allow_on_submit should be editable.', () => {
|
||||
cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update');
|
||||
cy.visit(`/app/List/${doctype_name}/Report`);
|
||||
|
||||
// check status column added from docstatus
|
||||
cy.get('.dt-row-0 > .dt-cell--col-3').should('contain', 'Submitted');
|
||||
let cell = cy.get('.dt-row-0 > .dt-cell--col-4');
|
||||
|
||||
// select the cell
|
||||
cell.dblclick();
|
||||
cell.find('input[data-fieldname="enabled"]').check({ force: true });
|
||||
cy.get('.dt-row-0 > .dt-cell--col-5').click();
|
||||
cell.get('.dt-cell__edit--col-4').findByRole('checkbox').check({ force: true });
|
||||
cy.get('.dt-row-0 > .dt-cell--col-3').click(); // click outside
|
||||
|
||||
cy.wait('@value-update');
|
||||
cy.get('@doc').then(doc => {
|
||||
cy.call('frappe.client.get_value', {
|
||||
doctype: doc.doctype,
|
||||
filters: {
|
||||
name: doc.name,
|
||||
},
|
||||
fieldname: 'enabled'
|
||||
}).then(r => {
|
||||
expect(r.message.enabled).to.equals(1);
|
||||
});
|
||||
|
||||
cy.call('frappe.client.get_value', {
|
||||
doctype: doctype_name,
|
||||
filters: {
|
||||
title: 'Doc 1',
|
||||
},
|
||||
fieldname: 'enabled'
|
||||
}).then(r => {
|
||||
expect(r.message.enabled).to.equals(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
55
cypress/integration/sidebar.js
Normal file
55
cypress/integration/sidebar.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
context('Sidebar', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/doctype');
|
||||
});
|
||||
|
||||
it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => {
|
||||
cy.click_sidebar_button("Assigned To");
|
||||
|
||||
//To check if no filter is available in "Assigned To" dropdown
|
||||
cy.get('.empty-state').should('contain', 'No filters found');
|
||||
|
||||
cy.click_sidebar_button("Created By");
|
||||
|
||||
//To check if "Created By" dropdown contains filter
|
||||
cy.get('.group-by-item > .dropdown-item').should('contain', 'Me');
|
||||
|
||||
//Assigning a doctype to a user
|
||||
cy.visit('/app/doctype/ToDo');
|
||||
cy.get('.form-assignments > .flex > .text-muted').click();
|
||||
cy.get_field('assign_to_me', 'Check').click();
|
||||
cy.get('.modal-footer > .standard-actions > .btn-primary').click();
|
||||
cy.visit('/app/doctype');
|
||||
cy.click_sidebar_button("Assigned To");
|
||||
|
||||
//To check if filter is added in "Assigned To" dropdown after assignment
|
||||
cy.get('.group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item').should('contain', '1');
|
||||
|
||||
//To check if there is no filter added to the listview
|
||||
cy.get('.filter-selector > .btn').should('contain', 'Filter');
|
||||
|
||||
//To add a filter to display data into the listview
|
||||
cy.get('.group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item').click();
|
||||
|
||||
//To check if filter is applied
|
||||
cy.click_filter_button().should('contain', '1 filter');
|
||||
cy.get('.fieldname-select-area > .awesomplete > .form-control').should('have.value', 'Assigned To');
|
||||
cy.get('.condition').should('have.value', 'like');
|
||||
cy.get('.filter-field > .form-group > .input-with-feedback').should('have.value', '%Administrator%');
|
||||
cy.click_filter_button();
|
||||
|
||||
//To remove the applied filter
|
||||
cy.clear_filters();
|
||||
|
||||
//To remove the assignment
|
||||
cy.visit('/app/doctype/ToDo');
|
||||
cy.get('.assignments > .avatar-group > .avatar > .avatar-frame').click();
|
||||
cy.get('.remove-btn').click({force: true});
|
||||
cy.hide_dialog();
|
||||
cy.visit('/app/doctype');
|
||||
cy.click_sidebar_button("Assigned To");
|
||||
cy.get('.empty-state').should('contain', 'No filters found');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
context('Table MultiSelect', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
|
|
@ -8,7 +8,8 @@ context('Table MultiSelect', () => {
|
|||
it('select value from multiselect dropdown', () => {
|
||||
cy.new_form('Assignment Rule');
|
||||
cy.fill_field('__newname', name);
|
||||
cy.fill_field('document_type', 'ToDo');
|
||||
cy.fill_field('document_type', 'Blog Post');
|
||||
cy.get('.section-head').contains('Assignment Rules').scrollIntoView();
|
||||
cy.fill_field('assign_condition', 'status=="Open"', 'Code');
|
||||
cy.get('input[data-fieldname="users"]').focus().as('input');
|
||||
cy.get('input[data-fieldname="users"] + ul').should('be.visible');
|
||||
|
|
|
|||
89
cypress/integration/timeline.js
Normal file
89
cypress/integration/timeline.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import custom_submittable_doctype from '../fixtures/custom_submittable_doctype';
|
||||
|
||||
context('Timeline', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo', () => {
|
||||
//Adding new ToDo
|
||||
cy.visit('/app/todo/new-todo-1');
|
||||
cy.get('[data-fieldname="description"] .ql-editor.ql-blank').type('Test ToDo', {force: true}).wait(200);
|
||||
cy.get('.page-head .page-actions').findByRole('button', {name: 'Save'}).click();
|
||||
|
||||
cy.visit('/app/todo');
|
||||
cy.click_listview_row_item(0);
|
||||
|
||||
//To check if the comment box is initially empty and tying some text into it
|
||||
cy.get('[data-fieldname="comment"] .ql-editor').should('contain', '').type('Testing Timeline');
|
||||
|
||||
//Adding new comment
|
||||
cy.get('.comment-box').findByRole('button', {name: 'Comment'}).click();
|
||||
|
||||
//To check if the commented text is visible in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Testing Timeline');
|
||||
|
||||
//Editing comment
|
||||
cy.click_timeline_action_btn("Edit");
|
||||
cy.get('.timeline-content [data-fieldname="comment"] .ql-editor').first().type(' 123');
|
||||
cy.click_timeline_action_btn("Save");
|
||||
|
||||
//To check if the edited comment text is visible in timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Testing Timeline 123');
|
||||
|
||||
//Discarding comment
|
||||
cy.click_timeline_action_btn("Edit");
|
||||
cy.click_timeline_action_btn("Dismiss");
|
||||
|
||||
//To check if after discarding the timeline content is same as previous
|
||||
cy.get('.timeline-content').should('contain', 'Testing Timeline 123');
|
||||
|
||||
//Deleting the added comment
|
||||
cy.get('.timeline-message-box .more-actions > .action-btn').click(); //Menu button in timeline item
|
||||
cy.get('.timeline-message-box .more-actions .dropdown-item').contains('Delete').click({ force: true });
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Yes'}).click({ force: true });
|
||||
|
||||
cy.get('.timeline-content').should('not.contain', 'Testing Timeline 123');
|
||||
});
|
||||
|
||||
it('Timeline should have submit and cancel activity information', () => {
|
||||
cy.visit('/app/doctype');
|
||||
|
||||
//Creating custom doctype
|
||||
cy.insert_doc('DocType', custom_submittable_doctype, true);
|
||||
|
||||
cy.visit('/app/custom-submittable-doctype');
|
||||
cy.click_listview_primary_button('Add Custom Submittable DocType');
|
||||
|
||||
//Adding a new entry for the created custom doctype
|
||||
cy.fill_field('title', 'Test');
|
||||
cy.click_modal_primary_button('Save');
|
||||
cy.click_modal_primary_button('Submit');
|
||||
|
||||
cy.visit('/app/custom-submittable-doctype');
|
||||
cy.click_listview_row_item(0);
|
||||
|
||||
//To check if the submission of the documemt is visible in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Administrator submitted this document');
|
||||
cy.get('[id="page-Custom Submittable DocType"] .page-actions').findByRole('button', {name: 'Cancel'}).click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Yes'}).click();
|
||||
|
||||
//To check if the cancellation of the documemt is visible in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Administrator cancelled this document');
|
||||
|
||||
//Deleting the document
|
||||
cy.visit('/app/custom-submittable-doctype');
|
||||
cy.select_listview_row_checkbox(0);
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.page-actions .actions-btn-group [data-label="Delete"]').click();
|
||||
cy.click_modal_primary_button('Yes');
|
||||
|
||||
//Deleting the custom doctype
|
||||
cy.visit('/app/doctype');
|
||||
cy.select_listview_row_checkbox(0);
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.page-actions .actions-btn-group [data-label="Delete"]').click();
|
||||
cy.click_modal_primary_button('Yes');
|
||||
});
|
||||
});
|
||||
76
cypress/integration/timeline_email.js
Normal file
76
cypress/integration/timeline_email.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
context('Timeline Email', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/todo');
|
||||
});
|
||||
|
||||
it('Adding new ToDo', () => {
|
||||
cy.click_listview_primary_button('Add ToDo');
|
||||
cy.get('.custom-actions:visible > .btn').contains("Edit in full page").click({delay: 500});
|
||||
cy.fill_field("description", "Test ToDo", "Text Editor");
|
||||
cy.wait(500);
|
||||
cy.get('.primary-action').contains('Save').click({force: true});
|
||||
cy.wait(700);
|
||||
});
|
||||
|
||||
it('Adding email and verifying timeline content for email attachment', () => {
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.list-row > .level-left > .list-subject').eq(0).click();
|
||||
|
||||
//Creating a new email
|
||||
cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click();
|
||||
cy.fill_field('recipients', 'test@example.com', 'MultiSelect');
|
||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-body > :nth-child(1) > .form-layout > .form-page > :nth-child(3) > .section-body > .form-column > form > [data-fieldtype="Text Editor"] > .form-group > .control-input-wrapper > .control-input > .ql-container > .ql-editor').type('Test Mail');
|
||||
|
||||
//Adding attachment to the email
|
||||
cy.get('.add-more-attachments > .btn').click();
|
||||
cy.get('.mt-2 > .btn > .mt-1').eq(2).click();
|
||||
cy.get('.input-group > .form-control').type('https://wallpaperplay.com/walls/full/8/2/b/72402.jpg');
|
||||
cy.get('.btn-primary').contains('Upload').click();
|
||||
|
||||
//Sending the email
|
||||
cy.click_modal_primary_button('Send', {delay: 500});
|
||||
|
||||
//To check if the sent mail content is shown in the timeline content
|
||||
cy.get('[data-doctype="Communication"] > .timeline-content').should('contain', 'Test Mail');
|
||||
|
||||
//To check if the attachment of email is shown in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Added 72402.jpg');
|
||||
|
||||
//Deleting the sent email
|
||||
cy.get('[title="Open Communication"] > .icon').first().click({force: true});
|
||||
cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .btn').click();
|
||||
cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .dropdown-menu > li > .grey-link').eq(9).click();
|
||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').click();
|
||||
});
|
||||
|
||||
it('Deleting attachment and ToDo', () => {
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click();
|
||||
|
||||
//Removing the added attachment
|
||||
cy.get('.attachment-row > .data-pill > .remove-btn > .icon').click();
|
||||
cy.wait(500);
|
||||
cy.get('.modal-footer:visible > .standard-actions > .btn-primary').contains('Yes').click();
|
||||
|
||||
//To check if the removed attachment is shown in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Removed 72402.jpg');
|
||||
cy.wait(500);
|
||||
|
||||
//To check if the discard button functionality in email is working correctly
|
||||
cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click();
|
||||
cy.fill_field('recipients', 'test@example.com', 'MultiSelect');
|
||||
cy.get('.modal-footer > .standard-actions > .btn-secondary').contains('Discard').click();
|
||||
cy.wait(500);
|
||||
cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click();
|
||||
cy.wait(500);
|
||||
cy.get_field('recipients', 'MultiSelect').should('have.text', '');
|
||||
cy.get('.modal-header:visible > .modal-actions > .btn-modal-close > .icon').click();
|
||||
|
||||
//Deleting the added ToDo
|
||||
cy.get('.menu-btn-group:visible > .btn').click();
|
||||
cy.get('.menu-btn-group:visible > .dropdown-menu > li > .dropdown-item').contains('Delete').click();
|
||||
cy.get('.modal-footer:visible > .standard-actions > .btn-primary').click();
|
||||
});
|
||||
});
|
||||
43
cypress/integration/url_data_field.js
Normal file
43
cypress/integration/url_data_field.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import data_field_validation_doctype from '../fixtures/data_field_validation_doctype';
|
||||
|
||||
const doctype_name = data_field_validation_doctype.name;
|
||||
|
||||
context('URL Data Field Input', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
return cy.insert_doc('DocType', data_field_validation_doctype, true);
|
||||
});
|
||||
|
||||
|
||||
describe('URL Data Field Input ', () => {
|
||||
it('should not show URL link button without focus', () => {
|
||||
cy.new_form(doctype_name);
|
||||
cy.get_field('url').clear().type('https://frappe.io');
|
||||
cy.get_field('url').blur().wait(500);
|
||||
cy.get('.link-btn').should('not.be.visible');
|
||||
});
|
||||
|
||||
it('should show URL link button on focus', () => {
|
||||
cy.get_field('url').focus().wait(500);
|
||||
cy.get('.link-btn').should('be.visible');
|
||||
});
|
||||
|
||||
it('should not show URL link button for invalid URL', () => {
|
||||
cy.get_field('url').clear().type('fuzzbuzz');
|
||||
cy.get('.link-btn').should('not.be.visible');
|
||||
});
|
||||
|
||||
it('should have valid URL link with target _blank', () => {
|
||||
cy.get_field('url').clear().type('https://frappe.io');
|
||||
cy.get('.link-btn .btn-open').should('have.attr', 'href', 'https://frappe.io');
|
||||
cy.get('.link-btn .btn-open').should('have.attr', 'target', '_blank');
|
||||
});
|
||||
|
||||
it('should inject anchor tag in read-only URL data field', () => {
|
||||
cy.get('[data-fieldname="read_only_url"]')
|
||||
.find('a')
|
||||
.should('have.attr', 'target', '_blank');
|
||||
});
|
||||
});
|
||||
});
|
||||
29
cypress/integration/web_form.js
Normal file
29
cypress/integration/web_form.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
context('Web Form', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('Navigate and Submit a WebForm', () => {
|
||||
cy.visit('/update-profile');
|
||||
cy.get_field('last_name', 'Data').type('_Test User', {force: true}).wait(200);
|
||||
cy.get('.web-form-actions .btn-primary').click();
|
||||
cy.wait(500);
|
||||
cy.get('.modal.show > .modal-dialog').should('be.visible');
|
||||
});
|
||||
|
||||
it('Navigate and Submit a MultiStep WebForm', () => {
|
||||
cy.call('frappe.tests.ui_test_helpers.update_webform_to_multistep').then(() => {
|
||||
cy.visit('/update-profile-duplicate');
|
||||
cy.get_field('last_name', 'Data').type('_Test User', {force: true}).wait(200);
|
||||
cy.get('.btn-next').should('be.visible');
|
||||
cy.get('.web-form-footer .btn-primary').should('not.be.visible');
|
||||
cy.get('.btn-next').click();
|
||||
cy.get('.btn-previous').should('be.visible');
|
||||
cy.get('.btn-next').should('not.be.visible');
|
||||
cy.get('.web-form-footer .btn-primary').should('be.visible');
|
||||
cy.get('.web-form-actions .btn-primary').click();
|
||||
cy.wait(500);
|
||||
cy.get('.modal.show > .modal-dialog').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
88
cypress/integration/workspace.js
Normal file
88
cypress/integration/workspace.js
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
context('Workspace 2.0', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
it('Navigate to page from sidebar', () => {
|
||||
cy.visit('/app/build');
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.sidebar-item-container[item-name="Settings"]').first().click();
|
||||
cy.location('pathname').should('eq', '/app/settings');
|
||||
});
|
||||
|
||||
it('Create Private Page', () => {
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.custom-actions button[data-label="Create%20Workspace"]').click();
|
||||
cy.fill_field('title', 'Test Private Page', 'Data');
|
||||
cy.fill_field('icon', 'edit', 'Icon');
|
||||
cy.get_open_dialog().find('.modal-header').click();
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
|
||||
// check if sidebar item is added in pubic section
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0');
|
||||
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
|
||||
cy.wait(300);
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0');
|
||||
|
||||
cy.wait(500);
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.standard-actions .btn-secondary[data-label=Edit]').click();
|
||||
});
|
||||
|
||||
it('Add New Block', () => {
|
||||
cy.get('.ce-block').click().type('{enter}');
|
||||
cy.get('.block-list-container .block-list-item').contains('Heading').click();
|
||||
cy.get(":focus").type('Header');
|
||||
cy.get(".ce-block:last").find('.ce-header').should('exist');
|
||||
|
||||
cy.get('.ce-block:last').click().type('{enter}');
|
||||
cy.get('.block-list-container .block-list-item').contains('Text').click();
|
||||
cy.get(":focus").type('Paragraph text');
|
||||
cy.get(".ce-block:last").find('.ce-paragraph').should('exist');
|
||||
});
|
||||
|
||||
it('Delete A Block', () => {
|
||||
cy.get(":focus").click();
|
||||
cy.get('.paragraph-control .setting-btn').click();
|
||||
cy.get('.paragraph-control .dropdown-item').contains('Delete').click();
|
||||
cy.get(".ce-block:last").find('.ce-paragraph').should('not.exist');
|
||||
});
|
||||
|
||||
it('Shrink and Expand A Block', () => {
|
||||
cy.get(":focus").click();
|
||||
cy.get('.ce-block:last .setting-btn').click();
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Shrink').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-11');
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Shrink').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-10');
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Shrink').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-9');
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Expand').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-10');
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Expand').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-11');
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Expand').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-12');
|
||||
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
|
||||
});
|
||||
|
||||
it('Delete Private Page', () => {
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.standard-actions .btn-secondary[data-label=Edit]').click();
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]')
|
||||
.find('.sidebar-item-control .setting-btn').click();
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]')
|
||||
.find('.dropdown-item[title="Delete Workspace"]').click({force: true});
|
||||
cy.wait(300);
|
||||
cy.get('.modal-footer > .standard-actions > .btn-modal-primary:visible').first().click();
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('not.exist');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
module.exports = () => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
};
|
||||
module.exports = (on, config) => {
|
||||
require('@cypress/code-coverage/task')(on, config);
|
||||
return config;
|
||||
};
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import 'cypress-file-upload';
|
||||
import '@testing-library/cypress/add-commands';
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
|
|
@ -29,7 +30,7 @@ Cypress.Commands.add('login', (email, password) => {
|
|||
email = 'Administrator';
|
||||
}
|
||||
if (!password) {
|
||||
password = Cypress.config('adminPassword');
|
||||
password = Cypress.env('adminPassword');
|
||||
}
|
||||
cy.request({
|
||||
url: '/api/method/login',
|
||||
|
|
@ -109,34 +110,6 @@ Cypress.Commands.add('get_doc', (doctype, name) => {
|
|||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => {
|
||||
return cy
|
||||
.window()
|
||||
.its('frappe.csrf_token')
|
||||
.then(csrf_token => {
|
||||
return cy
|
||||
.request({
|
||||
method: 'POST',
|
||||
url: `/api/resource/${doctype}`,
|
||||
body: args,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Frappe-CSRF-Token': csrf_token
|
||||
},
|
||||
failOnStatusCode: !ignore_duplicate
|
||||
})
|
||||
.then(res => {
|
||||
let status_codes = [200];
|
||||
if (ignore_duplicate) {
|
||||
status_codes.push(409);
|
||||
}
|
||||
expect(res.status).to.be.oneOf(status_codes);
|
||||
return res.body;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('remove_doc', (doctype, name) => {
|
||||
return cy
|
||||
.window()
|
||||
|
|
@ -160,7 +133,7 @@ Cypress.Commands.add('remove_doc', (doctype, name) => {
|
|||
|
||||
Cypress.Commands.add('create_records', doc => {
|
||||
return cy
|
||||
.call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc})
|
||||
.call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc: JSON.stringify(doc)})
|
||||
.then(r => r.message);
|
||||
});
|
||||
|
||||
|
|
@ -186,22 +159,23 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
|
|||
if (fieldtype === 'Select') {
|
||||
cy.get('@input').select(value);
|
||||
} else {
|
||||
cy.get('@input').type(value, {waitForAnimations: false, force: true});
|
||||
cy.get('@input').type(value, {waitForAnimations: false, force: true, delay: 100});
|
||||
}
|
||||
return cy.get('@input');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
|
||||
let selector = `.form-control[data-fieldname="${fieldname}"]`;
|
||||
let field_element = fieldtype === 'Select' ? 'select': 'input';
|
||||
let selector = `[data-fieldname="${fieldname}"] ${field_element}:visible`;
|
||||
|
||||
if (fieldtype === 'Text Editor') {
|
||||
selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`;
|
||||
selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]:visible`;
|
||||
}
|
||||
if (fieldtype === 'Code') {
|
||||
selector = `[data-fieldname="${fieldname}"] .ace_text-input`;
|
||||
}
|
||||
|
||||
return cy.get(selector);
|
||||
return cy.get(selector).first();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => {
|
||||
|
|
@ -240,7 +214,7 @@ Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fie
|
|||
});
|
||||
|
||||
Cypress.Commands.add('awesomebar', text => {
|
||||
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100});
|
||||
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 700});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('new_form', doctype => {
|
||||
|
|
@ -251,7 +225,8 @@ Cypress.Commands.add('new_form', doctype => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add('go_to_list', doctype => {
|
||||
cy.visit(`/app/list/${doctype}/list`);
|
||||
let dt_in_route = doctype.toLowerCase().replace(/ /g, '-');
|
||||
cy.visit(`/app/${dt_in_route}`);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('clear_cache', () => {
|
||||
|
|
@ -315,7 +290,11 @@ Cypress.Commands.add('add_filter', () => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add('clear_filters', () => {
|
||||
cy.get('.filter-section .filter-button').click();
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.model.utils.user_settings.save'
|
||||
}).as('filter-saved');
|
||||
cy.get('.filter-section .filter-button').click({force: true});
|
||||
cy.wait(300);
|
||||
cy.get('.filter-popover').should('exist');
|
||||
cy.get('.filter-popover').find('.clear-filters').click();
|
||||
|
|
@ -323,4 +302,33 @@ Cypress.Commands.add('clear_filters', () => {
|
|||
cy.window().its('cur_list').then(cur_list => {
|
||||
cur_list && cur_list.filter_area && cur_list.filter_area.clear();
|
||||
});
|
||||
cy.wait('@filter-saved');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_modal_primary_button', (btn_name) => {
|
||||
cy.get('.modal-footer > .standard-actions > .btn-primary').contains(btn_name).trigger('click', {force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_sidebar_button', (btn_name) => {
|
||||
cy.get('.list-group-by-fields .list-link > a').contains(btn_name).click({force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_listview_row_item', (row_no) => {
|
||||
cy.get('.list-row > .level-left > .list-subject > .level-item > .ellipsis').eq(row_no).click({force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_filter_button', () => {
|
||||
cy.get('.filter-selector > .btn').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_listview_primary_button', (btn_name) => {
|
||||
cy.get('.primary-action').contains(btn_name).click({force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_timeline_action_btn', (btn_name) => {
|
||||
cy.get('.timeline-message-box .actions .action-btn').contains(btn_name).click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('select_listview_row_checkbox', (row_no) => {
|
||||
cy.get('.frappe-list .select-like > .list-row-checkbox').eq(row_no).click();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
import '@cypress/code-coverage/support';
|
||||
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
|
|
|
|||
4
dev-requirements.txt
Normal file
4
dev-requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
coverage==5.5
|
||||
Faker~=8.1.0
|
||||
pyngrok~=5.0.5
|
||||
unittest-xml-reporting~=3.0.4
|
||||
38
esbuild/build-cleanup.js
Normal file
38
esbuild/build-cleanup.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/* eslint-disable no-console */
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const glob = require("fast-glob");
|
||||
|
||||
module.exports = {
|
||||
name: 'build_cleanup',
|
||||
setup(build) {
|
||||
build.onEnd(result => {
|
||||
if (result.errors.length) return;
|
||||
clean_dist_files(Object.keys(result.metafile.outputs));
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function clean_dist_files(new_files) {
|
||||
new_files.forEach(
|
||||
file => {
|
||||
if (file.endsWith(".map")) return;
|
||||
|
||||
const pattern = file.split(".").slice(0, -2).join(".") + "*";
|
||||
glob.sync(pattern).forEach(
|
||||
file_to_delete => {
|
||||
if (file_to_delete.startsWith(file)) return;
|
||||
|
||||
fs.unlink(path.resolve(file_to_delete), err => {
|
||||
if (!err) return;
|
||||
|
||||
console.error(
|
||||
`Error deleting ${file.split(path.sep).pop()}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
528
esbuild/esbuild.js
Normal file
528
esbuild/esbuild.js
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
/* eslint-disable no-console */
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const glob = require("fast-glob");
|
||||
const esbuild = require("esbuild");
|
||||
const vue = require("esbuild-vue");
|
||||
const yargs = require("yargs");
|
||||
const cliui = require("cliui")();
|
||||
const chalk = require("chalk");
|
||||
const html_plugin = require("./frappe-html");
|
||||
const rtlcss = require('rtlcss');
|
||||
const postCssPlugin = require("@frappe/esbuild-plugin-postcss2").default;
|
||||
const ignore_assets = require("./ignore-assets");
|
||||
const sass_options = require("./sass_options");
|
||||
const build_cleanup_plugin = require("./build-cleanup");
|
||||
|
||||
const {
|
||||
app_list,
|
||||
assets_path,
|
||||
apps_path,
|
||||
sites_path,
|
||||
get_app_path,
|
||||
get_public_path,
|
||||
log,
|
||||
log_warn,
|
||||
log_error,
|
||||
bench_path,
|
||||
get_redis_subscriber
|
||||
} = require("./utils");
|
||||
|
||||
const argv = yargs
|
||||
.usage("Usage: node esbuild [options]")
|
||||
.option("apps", {
|
||||
type: "string",
|
||||
description: "Run build for specific apps"
|
||||
})
|
||||
.option("skip_frappe", {
|
||||
type: "boolean",
|
||||
description: "Skip building frappe assets"
|
||||
})
|
||||
.option("files", {
|
||||
type: "string",
|
||||
description: "Run build for specified bundles"
|
||||
})
|
||||
.option("watch", {
|
||||
type: "boolean",
|
||||
description: "Run in watch mode and rebuild on file changes"
|
||||
})
|
||||
.option("live-reload", {
|
||||
type: "boolean",
|
||||
description: `Automatically reload Desk when assets are rebuilt.
|
||||
Can only be used with the --watch flag.`
|
||||
})
|
||||
.option("production", {
|
||||
type: "boolean",
|
||||
description: "Run build in production mode"
|
||||
})
|
||||
.option("run-build-command", {
|
||||
type: "boolean",
|
||||
description: "Run build command for apps"
|
||||
})
|
||||
.example(
|
||||
"node esbuild --apps frappe,erpnext",
|
||||
"Run build only for frappe and erpnext"
|
||||
)
|
||||
.example(
|
||||
"node esbuild --files frappe/website.bundle.js,frappe/desk.bundle.js",
|
||||
"Run build only for specified bundles"
|
||||
)
|
||||
.version(false).argv;
|
||||
|
||||
const APPS = (!argv.apps ? app_list : argv.apps.split(",")).filter(
|
||||
app => !(argv.skip_frappe && app == "frappe")
|
||||
);
|
||||
const FILES_TO_BUILD = argv.files ? argv.files.split(",") : [];
|
||||
const WATCH_MODE = Boolean(argv.watch);
|
||||
const PRODUCTION = Boolean(argv.production);
|
||||
const RUN_BUILD_COMMAND = !WATCH_MODE && Boolean(argv["run-build-command"]);
|
||||
|
||||
const TOTAL_BUILD_TIME = `${chalk.black.bgGreen(" DONE ")} Total Build Time`;
|
||||
const NODE_PATHS = [].concat(
|
||||
// node_modules of apps directly importable
|
||||
app_list
|
||||
.map(app => path.resolve(get_app_path(app), "../node_modules"))
|
||||
.filter(fs.existsSync),
|
||||
// import js file of any app if you provide the full path
|
||||
app_list
|
||||
.map(app => path.resolve(get_app_path(app), ".."))
|
||||
.filter(fs.existsSync)
|
||||
);
|
||||
|
||||
execute()
|
||||
.then(() => RUN_BUILD_COMMAND && run_build_command_for_apps(APPS))
|
||||
.catch(e => console.error(e));
|
||||
|
||||
if (WATCH_MODE) {
|
||||
// listen for open files in editor event
|
||||
open_in_editor();
|
||||
}
|
||||
|
||||
async function execute() {
|
||||
console.time(TOTAL_BUILD_TIME);
|
||||
|
||||
let results;
|
||||
try {
|
||||
results = await build_assets_for_apps(APPS, FILES_TO_BUILD);
|
||||
} catch (e) {
|
||||
log_error("There were some problems during build");
|
||||
log();
|
||||
log(chalk.dim(e.stack));
|
||||
if (process.env.CI) {
|
||||
process.kill(process.pid);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!WATCH_MODE) {
|
||||
log_built_assets(results);
|
||||
console.timeEnd(TOTAL_BUILD_TIME);
|
||||
log();
|
||||
} else {
|
||||
log("Watching for changes...");
|
||||
}
|
||||
for (const result of results) {
|
||||
await write_assets_json(result.metafile);
|
||||
}
|
||||
}
|
||||
|
||||
function build_assets_for_apps(apps, files) {
|
||||
let { include_patterns, ignore_patterns } = files.length
|
||||
? get_files_to_build(files)
|
||||
: get_all_files_to_build(apps);
|
||||
|
||||
return glob(include_patterns, { ignore: ignore_patterns }).then(files => {
|
||||
let output_path = assets_path;
|
||||
|
||||
let file_map = {};
|
||||
let style_file_map = {};
|
||||
let rtl_style_file_map = {};
|
||||
for (let file of files) {
|
||||
let relative_app_path = path.relative(apps_path, file);
|
||||
let app = relative_app_path.split(path.sep)[0];
|
||||
|
||||
let extension = path.extname(file);
|
||||
let output_name = path.basename(file, extension);
|
||||
if (
|
||||
[".css", ".scss", ".less", ".sass", ".styl"].includes(extension)
|
||||
) {
|
||||
output_name = path.join("css", output_name);
|
||||
} else if ([".js", ".ts"].includes(extension)) {
|
||||
output_name = path.join("js", output_name);
|
||||
}
|
||||
output_name = path.join(app, "dist", output_name);
|
||||
|
||||
if (Object.keys(file_map).includes(output_name) || Object.keys(style_file_map).includes(output_name)) {
|
||||
log_warn(
|
||||
`Duplicate output file ${output_name} generated from ${file}`
|
||||
);
|
||||
}
|
||||
if ([".css", ".scss", ".less", ".sass", ".styl"].includes(extension)) {
|
||||
style_file_map[output_name] = file;
|
||||
rtl_style_file_map[output_name.replace('/css/', '/css-rtl/')] = file;
|
||||
} else {
|
||||
file_map[output_name] = file;
|
||||
}
|
||||
}
|
||||
let build = build_files({
|
||||
files: file_map,
|
||||
outdir: output_path
|
||||
});
|
||||
let style_build = build_style_files({
|
||||
files: style_file_map,
|
||||
outdir: output_path
|
||||
});
|
||||
let rtl_style_build = build_style_files({
|
||||
files: rtl_style_file_map,
|
||||
outdir: output_path,
|
||||
rtl_style: true
|
||||
});
|
||||
return Promise.all([build, style_build, rtl_style_build]);
|
||||
});
|
||||
}
|
||||
|
||||
function get_all_files_to_build(apps) {
|
||||
let include_patterns = [];
|
||||
let ignore_patterns = [];
|
||||
|
||||
for (let app of apps) {
|
||||
let public_path = get_public_path(app);
|
||||
include_patterns.push(
|
||||
path.resolve(
|
||||
public_path,
|
||||
"**",
|
||||
"*.bundle.{js,ts,css,sass,scss,less,styl}"
|
||||
)
|
||||
);
|
||||
ignore_patterns.push(
|
||||
path.resolve(public_path, "node_modules"),
|
||||
path.resolve(public_path, "dist")
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
include_patterns,
|
||||
ignore_patterns
|
||||
};
|
||||
}
|
||||
|
||||
function get_files_to_build(files) {
|
||||
// files: ['frappe/website.bundle.js', 'erpnext/main.bundle.js']
|
||||
let include_patterns = [];
|
||||
let ignore_patterns = [];
|
||||
|
||||
for (let file of files) {
|
||||
let [app, bundle] = file.split("/");
|
||||
let public_path = get_public_path(app);
|
||||
include_patterns.push(path.resolve(public_path, "**", bundle));
|
||||
ignore_patterns.push(
|
||||
path.resolve(public_path, "node_modules"),
|
||||
path.resolve(public_path, "dist")
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
include_patterns,
|
||||
ignore_patterns
|
||||
};
|
||||
}
|
||||
|
||||
function build_files({ files, outdir }) {
|
||||
let build_plugins = [
|
||||
html_plugin,
|
||||
build_cleanup_plugin,
|
||||
vue(),
|
||||
];
|
||||
return esbuild.build(get_build_options(files, outdir, build_plugins));
|
||||
}
|
||||
|
||||
function build_style_files({ files, outdir, rtl_style = false }) {
|
||||
let plugins = [];
|
||||
if (rtl_style) {
|
||||
plugins.push(rtlcss);
|
||||
}
|
||||
|
||||
let build_plugins = [
|
||||
ignore_assets,
|
||||
build_cleanup_plugin,
|
||||
postCssPlugin({
|
||||
plugins: plugins,
|
||||
sassOptions: sass_options
|
||||
})
|
||||
];
|
||||
|
||||
plugins.push(require("autoprefixer"));
|
||||
return esbuild.build(get_build_options(files, outdir, build_plugins));
|
||||
}
|
||||
|
||||
function get_build_options(files, outdir, plugins) {
|
||||
return {
|
||||
entryPoints: files,
|
||||
entryNames: "[dir]/[name].[hash]",
|
||||
outdir,
|
||||
sourcemap: true,
|
||||
bundle: true,
|
||||
metafile: true,
|
||||
minify: PRODUCTION,
|
||||
nodePaths: NODE_PATHS,
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
PRODUCTION ? "production" : "development"
|
||||
)
|
||||
},
|
||||
plugins: plugins,
|
||||
watch: get_watch_config()
|
||||
};
|
||||
}
|
||||
|
||||
function get_watch_config() {
|
||||
if (WATCH_MODE) {
|
||||
return {
|
||||
async onRebuild(error, result) {
|
||||
if (error) {
|
||||
log_error("There was an error during rebuilding changes.");
|
||||
log();
|
||||
log(chalk.dim(error.stack));
|
||||
notify_redis({ error });
|
||||
} else {
|
||||
let {
|
||||
new_assets_json,
|
||||
prev_assets_json
|
||||
} = await write_assets_json(result.metafile);
|
||||
|
||||
let changed_files;
|
||||
if (prev_assets_json) {
|
||||
changed_files = get_rebuilt_assets(
|
||||
prev_assets_json,
|
||||
new_assets_json
|
||||
);
|
||||
|
||||
let timestamp = new Date().toLocaleTimeString();
|
||||
let message = `${timestamp}: Compiled ${changed_files.length} files...`;
|
||||
log(chalk.yellow(message));
|
||||
for (let filepath of changed_files) {
|
||||
let filename = path.basename(filepath);
|
||||
log(" " + filename);
|
||||
}
|
||||
log();
|
||||
}
|
||||
notify_redis({ success: true, changed_files });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function log_built_assets(results) {
|
||||
let outputs = {};
|
||||
for (const result of results) {
|
||||
outputs = Object.assign(outputs, result.metafile.outputs);
|
||||
}
|
||||
let column_widths = [60, 20];
|
||||
cliui.div(
|
||||
{
|
||||
text: chalk.cyan.bold("File"),
|
||||
width: column_widths[0]
|
||||
},
|
||||
{
|
||||
text: chalk.cyan.bold("Size"),
|
||||
width: column_widths[1]
|
||||
}
|
||||
);
|
||||
cliui.div("");
|
||||
|
||||
let output_by_dist_path = {};
|
||||
for (let outfile in outputs) {
|
||||
if (outfile.endsWith(".map")) continue;
|
||||
let data = outputs[outfile];
|
||||
outfile = path.resolve(outfile);
|
||||
outfile = path.relative(assets_path, outfile);
|
||||
let filename = path.basename(outfile);
|
||||
let dist_path = outfile.replace(filename, "");
|
||||
output_by_dist_path[dist_path] = output_by_dist_path[dist_path] || [];
|
||||
output_by_dist_path[dist_path].push({
|
||||
name: filename,
|
||||
size: (data.bytes / 1000).toFixed(2) + " Kb"
|
||||
});
|
||||
}
|
||||
|
||||
for (let dist_path in output_by_dist_path) {
|
||||
let files = output_by_dist_path[dist_path];
|
||||
cliui.div({
|
||||
text: dist_path,
|
||||
width: column_widths[0]
|
||||
});
|
||||
|
||||
for (let i in files) {
|
||||
let file = files[i];
|
||||
let branch = "";
|
||||
if (i < files.length - 1) {
|
||||
branch = "├─ ";
|
||||
} else {
|
||||
branch = "└─ ";
|
||||
}
|
||||
let color = file.name.endsWith(".js") ? "green" : "blue";
|
||||
cliui.div(
|
||||
{
|
||||
text: branch + chalk[color]("" + file.name),
|
||||
width: column_widths[0]
|
||||
},
|
||||
{
|
||||
text: file.size,
|
||||
width: column_widths[1]
|
||||
}
|
||||
);
|
||||
}
|
||||
cliui.div("");
|
||||
}
|
||||
log(cliui.toString());
|
||||
}
|
||||
|
||||
// to store previous build's assets.json for comparison
|
||||
let prev_assets_json;
|
||||
let curr_assets_json;
|
||||
|
||||
async function write_assets_json(metafile) {
|
||||
let rtl = false;
|
||||
prev_assets_json = curr_assets_json;
|
||||
let out = {};
|
||||
for (let output in metafile.outputs) {
|
||||
let info = metafile.outputs[output];
|
||||
let asset_path = "/" + path.relative(sites_path, output);
|
||||
if (info.entryPoint) {
|
||||
let key = path.basename(info.entryPoint);
|
||||
if (key.endsWith('.css') && asset_path.includes('/css-rtl/')) {
|
||||
rtl = true;
|
||||
key = `rtl_${key}`;
|
||||
}
|
||||
out[key] = asset_path;
|
||||
}
|
||||
}
|
||||
|
||||
let assets_json_path = path.resolve(assets_path, `assets${rtl?'-rtl':''}.json`);
|
||||
let assets_json;
|
||||
try {
|
||||
assets_json = await fs.promises.readFile(assets_json_path, "utf-8");
|
||||
} catch (error) {
|
||||
assets_json = "{}";
|
||||
}
|
||||
assets_json = JSON.parse(assets_json);
|
||||
// update with new values
|
||||
let new_assets_json = Object.assign({}, assets_json, out);
|
||||
curr_assets_json = new_assets_json;
|
||||
|
||||
await fs.promises.writeFile(
|
||||
assets_json_path,
|
||||
JSON.stringify(new_assets_json, null, 4)
|
||||
);
|
||||
await update_assets_json_in_cache();
|
||||
return {
|
||||
new_assets_json,
|
||||
prev_assets_json
|
||||
};
|
||||
}
|
||||
|
||||
function update_assets_json_in_cache() {
|
||||
// update assets_json cache in redis, so that it can be read directly by python
|
||||
return new Promise(resolve => {
|
||||
let client = get_redis_subscriber("redis_cache");
|
||||
// handle error event to avoid printing stack traces
|
||||
client.on("error", _ => {
|
||||
log_warn("Cannot connect to redis_cache to update assets_json");
|
||||
});
|
||||
client.del("assets_json", err => {
|
||||
client.unref();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function run_build_command_for_apps(apps) {
|
||||
let cwd = process.cwd();
|
||||
let { execSync } = require("child_process");
|
||||
|
||||
for (let app of apps) {
|
||||
if (app === "frappe") continue;
|
||||
|
||||
let root_app_path = path.resolve(get_app_path(app), "..");
|
||||
let package_json = path.resolve(root_app_path, "package.json");
|
||||
if (fs.existsSync(package_json)) {
|
||||
let { scripts } = require(package_json);
|
||||
if (scripts && scripts.build) {
|
||||
log("\nRunning build command for", chalk.bold(app));
|
||||
process.chdir(root_app_path);
|
||||
execSync("yarn build", { encoding: "utf8", stdio: "inherit" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.chdir(cwd);
|
||||
}
|
||||
|
||||
async function notify_redis({ error, success, changed_files }) {
|
||||
// notify redis which in turns tells socketio to publish this to browser
|
||||
let subscriber = get_redis_subscriber("redis_socketio");
|
||||
subscriber.on("error", _ => {
|
||||
log_warn("Cannot connect to redis_socketio for browser events");
|
||||
});
|
||||
|
||||
let payload = null;
|
||||
if (error) {
|
||||
let formatted = await esbuild.formatMessages(error.errors, {
|
||||
kind: "error",
|
||||
terminalWidth: 100
|
||||
});
|
||||
let stack = error.stack.replace(new RegExp(bench_path, "g"), "");
|
||||
payload = {
|
||||
error,
|
||||
formatted,
|
||||
stack
|
||||
};
|
||||
}
|
||||
if (success) {
|
||||
payload = {
|
||||
success: true,
|
||||
changed_files,
|
||||
live_reload: argv["live-reload"]
|
||||
};
|
||||
}
|
||||
|
||||
subscriber.publish(
|
||||
"events",
|
||||
JSON.stringify({
|
||||
event: "build_event",
|
||||
message: payload
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function open_in_editor() {
|
||||
let subscriber = get_redis_subscriber("redis_socketio");
|
||||
subscriber.on("error", _ => {
|
||||
log_warn("Cannot connect to redis_socketio for open_in_editor events");
|
||||
});
|
||||
subscriber.on("message", (event, file) => {
|
||||
if (event === "open_in_editor") {
|
||||
file = JSON.parse(file);
|
||||
let file_path = path.resolve(file.file);
|
||||
log("Opening file in editor:", file_path);
|
||||
let launch = require("launch-editor");
|
||||
launch(`${file_path}:${file.line}:${file.column}`);
|
||||
}
|
||||
});
|
||||
subscriber.subscribe("open_in_editor");
|
||||
}
|
||||
|
||||
function get_rebuilt_assets(prev_assets, new_assets) {
|
||||
let added_files = [];
|
||||
let old_files = Object.values(prev_assets);
|
||||
let new_files = Object.values(new_assets);
|
||||
|
||||
for (let filepath of new_files) {
|
||||
if (!old_files.includes(filepath)) {
|
||||
added_files.push(filepath);
|
||||
}
|
||||
}
|
||||
return added_files;
|
||||
}
|
||||
44
esbuild/frappe-html.js
Normal file
44
esbuild/frappe-html.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
module.exports = {
|
||||
name: "frappe-html",
|
||||
setup(build) {
|
||||
let path = require("path");
|
||||
let fs = require("fs/promises");
|
||||
|
||||
build.onResolve({ filter: /\.html$/ }, args => {
|
||||
return {
|
||||
path: path.join(args.resolveDir, args.path),
|
||||
namespace: "frappe-html"
|
||||
};
|
||||
});
|
||||
|
||||
build.onLoad({ filter: /.*/, namespace: "frappe-html" }, args => {
|
||||
let filepath = args.path;
|
||||
let filename = path.basename(filepath).split(".")[0];
|
||||
|
||||
return fs
|
||||
.readFile(filepath, "utf-8")
|
||||
.then(content => {
|
||||
content = scrub_html_template(content);
|
||||
return {
|
||||
contents: `\n\tfrappe.templates['${filename}'] = \`${content}\`;\n`,
|
||||
watchFiles: [filepath]
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
return {
|
||||
contents: "",
|
||||
warnings: [
|
||||
{
|
||||
text: `There was an error importing ${filepath}`
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function scrub_html_template(content) {
|
||||
content = content.replace(/`/g, "\\`");
|
||||
return content;
|
||||
}
|
||||
11
esbuild/ignore-assets.js
Normal file
11
esbuild/ignore-assets.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
name: "frappe-ignore-asset",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^\/assets\// }, args => {
|
||||
return {
|
||||
path: args.path,
|
||||
external: true
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
1
esbuild/index.js
Normal file
1
esbuild/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
require("./esbuild");
|
||||
29
esbuild/sass_options.js
Normal file
29
esbuild/sass_options.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
let path = require("path");
|
||||
let { get_app_path, app_list } = require("./utils");
|
||||
|
||||
let node_modules_path = path.resolve(
|
||||
get_app_path("frappe"),
|
||||
"..",
|
||||
"node_modules"
|
||||
);
|
||||
let app_paths = app_list
|
||||
.map(get_app_path)
|
||||
.map(app_path => path.resolve(app_path, ".."));
|
||||
|
||||
module.exports = {
|
||||
includePaths: [node_modules_path, ...app_paths],
|
||||
importer: function(url) {
|
||||
if (url.startsWith("~")) {
|
||||
// strip ~ so that it can resolve from node_modules
|
||||
url = url.slice(1);
|
||||
}
|
||||
if (url.endsWith(".css")) {
|
||||
// strip .css from end of path
|
||||
url = url.slice(0, -4);
|
||||
}
|
||||
// normal file, let it go
|
||||
return {
|
||||
file: url
|
||||
};
|
||||
}
|
||||
};
|
||||
145
esbuild/utils.js
Normal file
145
esbuild/utils.js
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const chalk = require("chalk");
|
||||
|
||||
const frappe_path = path.resolve(__dirname, "..");
|
||||
const bench_path = path.resolve(frappe_path, "..", "..");
|
||||
const sites_path = path.resolve(bench_path, "sites");
|
||||
const apps_path = path.resolve(bench_path, "apps");
|
||||
const assets_path = path.resolve(sites_path, "assets");
|
||||
const app_list = get_apps_list();
|
||||
|
||||
const app_paths = app_list.reduce((out, app) => {
|
||||
out[app] = path.resolve(apps_path, app, app);
|
||||
return out;
|
||||
}, {});
|
||||
const public_paths = app_list.reduce((out, app) => {
|
||||
out[app] = path.resolve(app_paths[app], "public");
|
||||
return out;
|
||||
}, {});
|
||||
const public_js_paths = app_list.reduce((out, app) => {
|
||||
out[app] = path.resolve(app_paths[app], "public/js");
|
||||
return out;
|
||||
}, {});
|
||||
|
||||
const bundle_map = app_list.reduce((out, app) => {
|
||||
const public_js_path = public_js_paths[app];
|
||||
if (fs.existsSync(public_js_path)) {
|
||||
const all_files = fs.readdirSync(public_js_path);
|
||||
const js_files = all_files.filter(file => file.endsWith(".js"));
|
||||
|
||||
for (let js_file of js_files) {
|
||||
const filename = path.basename(js_file).split(".")[0];
|
||||
out[path.join(app, "js", filename)] = path.resolve(
|
||||
public_js_path,
|
||||
js_file
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}, {});
|
||||
|
||||
const get_public_path = app => public_paths[app];
|
||||
|
||||
const get_build_json_path = app =>
|
||||
path.resolve(get_public_path(app), "build.json");
|
||||
|
||||
function get_build_json(app) {
|
||||
try {
|
||||
return require(get_build_json_path(app));
|
||||
} catch (e) {
|
||||
// build.json does not exist
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function delete_file(path) {
|
||||
if (fs.existsSync(path)) {
|
||||
fs.unlinkSync(path);
|
||||
}
|
||||
}
|
||||
|
||||
function run_serially(tasks) {
|
||||
let result = Promise.resolve();
|
||||
tasks.forEach(task => {
|
||||
if (task) {
|
||||
result = result.then ? result.then(task) : Promise.resolve();
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
const get_app_path = app => app_paths[app];
|
||||
|
||||
function get_apps_list() {
|
||||
return fs
|
||||
.readFileSync(path.resolve(sites_path, "apps.txt"), {
|
||||
encoding: "utf-8"
|
||||
})
|
||||
.split("\n")
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function get_cli_arg(name) {
|
||||
let args = process.argv.slice(2);
|
||||
let arg = `--${name}`;
|
||||
let index = args.indexOf(arg);
|
||||
|
||||
let value = null;
|
||||
if (index != -1) {
|
||||
value = true;
|
||||
}
|
||||
if (value && args[index + 1]) {
|
||||
value = args[index + 1];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function log_error(message, badge = "ERROR") {
|
||||
badge = chalk.white.bgRed(` ${badge} `);
|
||||
console.error(`${badge} ${message}`); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
function log_warn(message, badge = "WARN") {
|
||||
badge = chalk.black.bgYellowBright(` ${badge} `);
|
||||
console.warn(`${badge} ${message}`); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
function log(...args) {
|
||||
console.log(...args); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
function get_redis_subscriber(kind) {
|
||||
// get redis subscriber that aborts after 10 connection attempts
|
||||
let { get_redis_subscriber: get_redis } = require("../node_utils");
|
||||
return get_redis(kind, {
|
||||
retry_strategy: function(options) {
|
||||
// abort after 10 connection attempts
|
||||
if (options.attempt > 10) {
|
||||
return undefined;
|
||||
}
|
||||
return Math.min(options.attempt * 100, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
app_list,
|
||||
bench_path,
|
||||
assets_path,
|
||||
sites_path,
|
||||
apps_path,
|
||||
bundle_map,
|
||||
get_public_path,
|
||||
get_build_json_path,
|
||||
get_build_json,
|
||||
get_app_path,
|
||||
delete_file,
|
||||
run_serially,
|
||||
get_cli_arg,
|
||||
log,
|
||||
log_warn,
|
||||
log_error,
|
||||
get_redis_subscriber
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
# License: MIT. See LICENSE
|
||||
"""
|
||||
Frappe - Low Code Open Source Framework in Python and JS
|
||||
|
||||
|
|
@ -10,26 +10,35 @@ be used to build database driven apps.
|
|||
|
||||
Read the documentation: https://frappeframework.com/docs
|
||||
"""
|
||||
from __future__ import unicode_literals, print_function
|
||||
import os, warnings
|
||||
|
||||
STANDARD_USERS = ('Guest', 'Administrator')
|
||||
|
||||
_dev_server = os.environ.get('DEV_SERVER', False)
|
||||
|
||||
if _dev_server:
|
||||
warnings.simplefilter('always', DeprecationWarning)
|
||||
warnings.simplefilter('always', PendingDeprecationWarning)
|
||||
|
||||
from six import iteritems, binary_type, text_type, string_types, PY2
|
||||
from werkzeug.local import Local, release_local
|
||||
import os, sys, importlib, inspect, json
|
||||
from past.builtins import cmp
|
||||
import sys, importlib, inspect, json
|
||||
import typing
|
||||
import click
|
||||
from faker import Faker
|
||||
|
||||
# public
|
||||
# Local application imports
|
||||
from .exceptions import *
|
||||
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
|
||||
from .utils.lazy_loader import lazy_import
|
||||
|
||||
# Harmless for Python 3
|
||||
# For Python 2 set default encoding to utf-8
|
||||
if PY2:
|
||||
reload(sys)
|
||||
sys.setdefaultencoding("utf-8")
|
||||
from frappe.query_builder import (
|
||||
get_query_builder,
|
||||
patch_query_execute,
|
||||
patch_query_aggregation,
|
||||
)
|
||||
from frappe.utils.data import cstr
|
||||
|
||||
__version__ = '14.0.0-dev'
|
||||
|
||||
__version__ = '13.0.0-dev'
|
||||
__title__ = "Frappe Framework"
|
||||
|
||||
local = Local()
|
||||
|
|
@ -39,7 +48,8 @@ class _dict(dict):
|
|||
"""dict like object that exposes keys as attributes"""
|
||||
def __getattr__(self, key):
|
||||
ret = self.get(key)
|
||||
if not ret and key.startswith("__"):
|
||||
# "__deepcopy__" exception added to fix frappe#14833 via DFP
|
||||
if not ret and key.startswith("__") and key != "__deepcopy__":
|
||||
raise AttributeError()
|
||||
return ret
|
||||
def __setattr__(self, key, value):
|
||||
|
|
@ -91,14 +101,14 @@ def _(msg, lang=None, context=None):
|
|||
|
||||
def as_unicode(text, encoding='utf-8'):
|
||||
'''Convert to unicode if required'''
|
||||
if isinstance(text, text_type):
|
||||
if isinstance(text, str):
|
||||
return text
|
||||
elif text==None:
|
||||
elif text is None:
|
||||
return ''
|
||||
elif isinstance(text, binary_type):
|
||||
return text_type(text, encoding)
|
||||
elif isinstance(text, bytes):
|
||||
return str(text, encoding)
|
||||
else:
|
||||
return text_type(text)
|
||||
return str(text)
|
||||
|
||||
def get_lang_dict(fortype, name=None):
|
||||
"""Returns the translated language dict for the given type and name.
|
||||
|
|
@ -114,7 +124,9 @@ def set_user_lang(user, user_language=None):
|
|||
local.lang = get_user_lang(user)
|
||||
|
||||
# local-globals
|
||||
|
||||
db = local("db")
|
||||
qb = local("qb")
|
||||
conf = local("conf")
|
||||
form = form_dict = local("form_dict")
|
||||
request = local("request")
|
||||
|
|
@ -129,6 +141,21 @@ message_log = local("message_log")
|
|||
|
||||
lang = local("lang")
|
||||
|
||||
# This if block is never executed when running the code. It is only used for
|
||||
# telling static code analyzer where to find dynamically defined attributes.
|
||||
if typing.TYPE_CHECKING:
|
||||
from frappe.utils.redis_wrapper import RedisWrapper
|
||||
|
||||
from frappe.database.mariadb.database import MariaDBDatabase
|
||||
from frappe.database.postgres.database import PostgresDatabase
|
||||
from frappe.query_builder.builder import MariaDB, Postgres
|
||||
|
||||
db: typing.Union[MariaDBDatabase, PostgresDatabase]
|
||||
qb: typing.Union[MariaDB, Postgres]
|
||||
|
||||
|
||||
# end: static analysis hack
|
||||
|
||||
def init(site, sites_path=None, new_site=False):
|
||||
"""Initialize frappe for the current site. Reset thread locals `frappe.local`"""
|
||||
if getattr(local, "initialised", None):
|
||||
|
|
@ -188,36 +215,44 @@ def init(site, sites_path=None, new_site=False):
|
|||
local.cache = {}
|
||||
local.document_cache = {}
|
||||
local.meta_cache = {}
|
||||
local.autoincremented_status_map = {site: -1}
|
||||
local.form_dict = _dict()
|
||||
local.session = _dict()
|
||||
local.dev_server = os.environ.get('DEV_SERVER', False)
|
||||
local.dev_server = _dev_server
|
||||
local.qb = get_query_builder(local.conf.db_type or "mariadb")
|
||||
|
||||
setup_module_map()
|
||||
patch_query_execute()
|
||||
patch_query_aggregation()
|
||||
|
||||
local.initialised = True
|
||||
|
||||
def connect(site=None, db_name=None):
|
||||
def connect(site=None, db_name=None, set_admin_as_user=True):
|
||||
"""Connect to site database instance.
|
||||
|
||||
:param site: If site is given, calls `frappe.init`.
|
||||
:param db_name: Optional. Will use from `site_config.json`."""
|
||||
:param db_name: Optional. Will use from `site_config.json`.
|
||||
:param set_admin_as_user: Set Administrator as current user.
|
||||
"""
|
||||
from frappe.database import get_db
|
||||
if site:
|
||||
init(site)
|
||||
|
||||
local.db = get_db(user=db_name or local.conf.db_name)
|
||||
set_user("Administrator")
|
||||
if set_admin_as_user:
|
||||
set_user("Administrator")
|
||||
|
||||
def connect_replica():
|
||||
from frappe.database import get_db
|
||||
user = local.conf.db_name
|
||||
password = local.conf.db_password
|
||||
port = local.conf.replica_db_port
|
||||
|
||||
if local.conf.different_credentials_for_replica:
|
||||
user = local.conf.replica_db_name
|
||||
password = local.conf.replica_db_password
|
||||
|
||||
local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password)
|
||||
local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password, port=port)
|
||||
|
||||
# swap db connections
|
||||
local.primary_db = local.db
|
||||
|
|
@ -264,7 +299,7 @@ def get_conf(site=None):
|
|||
|
||||
class init_site:
|
||||
def __init__(self, site=None):
|
||||
'''If site==None, initialize it for empty site ('') to load common_site_config.json'''
|
||||
'''If site is None, initialize it for empty site ('') to load common_site_config.json'''
|
||||
self.site = site or ''
|
||||
|
||||
def __enter__(self):
|
||||
|
|
@ -281,9 +316,8 @@ def destroy():
|
|||
|
||||
release_local(local)
|
||||
|
||||
# memcache
|
||||
redis_server = None
|
||||
def cache():
|
||||
def cache() -> "RedisWrapper":
|
||||
"""Returns redis connection."""
|
||||
global redis_server
|
||||
if not redis_server:
|
||||
|
|
@ -326,7 +360,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False,
|
|||
response JSON and shown in a pop-up / modal.
|
||||
|
||||
:param msg: Message.
|
||||
:param title: [optional] Message title.
|
||||
:param title: [optional] Message title. Default: "Message".
|
||||
:param raise_exception: [optional] Raise given exception and show message.
|
||||
:param as_table: [optional] If `msg` is a list of lists, render as HTML table.
|
||||
:param as_list: [optional] If `msg` is a list, render as un-ordered list.
|
||||
|
|
@ -363,8 +397,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False,
|
|||
if flags.print_messages and out.message:
|
||||
print(f"Message: {strip_html_tags(out.message)}")
|
||||
|
||||
if title:
|
||||
out.title = title
|
||||
out.title = title or _("Message", context="Default title of the message dialog")
|
||||
|
||||
if not indicator and raise_exception:
|
||||
indicator = 'red'
|
||||
|
|
@ -416,7 +449,7 @@ def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None,
|
|||
msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide, as_list=as_list)
|
||||
|
||||
def emit_js(js, user=False, **kwargs):
|
||||
if user == False:
|
||||
if user is False:
|
||||
user = session.user
|
||||
publish_realtime('eval_js', js, user=user, **kwargs)
|
||||
|
||||
|
|
@ -466,11 +499,11 @@ def get_request_header(key, default=None):
|
|||
:param default: Default value."""
|
||||
return request.headers.get(key, default)
|
||||
|
||||
def sendmail(recipients=[], sender="", subject="No Subject", message="No Message",
|
||||
def sendmail(recipients=None, sender="", subject="No Subject", message="No Message",
|
||||
as_markdown=False, delayed=True, reference_doctype=None, reference_name=None,
|
||||
unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
|
||||
attachments=None, content=None, doctype=None, name=None, reply_to=None,
|
||||
cc=[], bcc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None,
|
||||
unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, add_unsubscribe_link=1,
|
||||
attachments=None, content=None, doctype=None, name=None, reply_to=None, queue_separately=False,
|
||||
cc=None, bcc=None, message_id=None, in_reply_to=None, send_after=None, expose_recipients=None,
|
||||
send_priority=1, communication=None, retry=1, now=None, read_receipt=None, is_notification=False,
|
||||
inline_images=None, template=None, args=None, header=None, print_letterhead=False, with_container=False):
|
||||
"""Send email using user's default **Email Account** or global default **Email Account**.
|
||||
|
|
@ -500,6 +533,14 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
|
|||
:param header: Append header in email
|
||||
:param with_container: Wraps email inside a styled container
|
||||
"""
|
||||
|
||||
if recipients is None:
|
||||
recipients = []
|
||||
if cc is None:
|
||||
cc = []
|
||||
if bcc is None:
|
||||
bcc = []
|
||||
|
||||
text_content = None
|
||||
if template:
|
||||
message, text_content = get_email_from_template(template, args)
|
||||
|
|
@ -513,16 +554,20 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
|
|||
if not delayed:
|
||||
now = True
|
||||
|
||||
from frappe.email import queue
|
||||
queue.send(recipients=recipients, sender=sender,
|
||||
from frappe.email.doctype.email_queue.email_queue import QueueBuilder
|
||||
builder = QueueBuilder(recipients=recipients, sender=sender,
|
||||
subject=subject, message=message, text_content=text_content,
|
||||
reference_doctype = doctype or reference_doctype, reference_name = name or reference_name,
|
||||
reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, add_unsubscribe_link=add_unsubscribe_link,
|
||||
unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message,
|
||||
attachments=attachments, reply_to=reply_to, cc=cc, bcc=bcc, message_id=message_id, in_reply_to=in_reply_to,
|
||||
send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority,
|
||||
communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification,
|
||||
send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, queue_separately=queue_separately,
|
||||
communication=communication, read_receipt=read_receipt, is_notification=is_notification,
|
||||
inline_images=inline_images, header=header, print_letterhead=print_letterhead, with_container=with_container)
|
||||
|
||||
# build email queue and send the email if send_now is True.
|
||||
builder.process(send_now=now)
|
||||
|
||||
|
||||
whitelisted = []
|
||||
guest_methods = []
|
||||
xss_safe_methods = []
|
||||
|
|
@ -548,8 +593,15 @@ def whitelist(allow_guest=False, xss_safe=False, methods=None):
|
|||
|
||||
def innerfn(fn):
|
||||
global whitelisted, guest_methods, xss_safe_methods, allowed_http_methods_for_whitelisted_func
|
||||
whitelisted.append(fn)
|
||||
|
||||
# get function from the unbound / bound method
|
||||
# this is needed because functions can be compared, but not methods
|
||||
method = None
|
||||
if hasattr(fn, '__func__'):
|
||||
method = fn
|
||||
fn = method.__func__
|
||||
|
||||
whitelisted.append(fn)
|
||||
allowed_http_methods_for_whitelisted_func[fn] = methods
|
||||
|
||||
if allow_guest:
|
||||
|
|
@ -558,10 +610,24 @@ def whitelist(allow_guest=False, xss_safe=False, methods=None):
|
|||
if xss_safe:
|
||||
xss_safe_methods.append(fn)
|
||||
|
||||
return fn
|
||||
return method or fn
|
||||
|
||||
return innerfn
|
||||
|
||||
def is_whitelisted(method):
|
||||
from frappe.utils import sanitize_html
|
||||
|
||||
is_guest = session['user'] == 'Guest'
|
||||
if method not in whitelisted or is_guest and method not in guest_methods:
|
||||
throw(_("Not permitted"), PermissionError)
|
||||
|
||||
if is_guest and method not in xss_safe_methods:
|
||||
# strictly sanitize form_dict
|
||||
# escapes html characters like <> except for predefined tags like a, b, ul etc.
|
||||
for key, value in form_dict.items():
|
||||
if isinstance(value, str):
|
||||
form_dict[key] = sanitize_html(value)
|
||||
|
||||
def read_only():
|
||||
def innfn(fn):
|
||||
def wrapper_fn(*args, **kwargs):
|
||||
|
|
@ -570,8 +636,6 @@ def read_only():
|
|||
|
||||
try:
|
||||
retval = fn(*args, **get_newargs(fn, kwargs))
|
||||
except:
|
||||
raise
|
||||
finally:
|
||||
if local and hasattr(local, 'primary_db'):
|
||||
local.db.close()
|
||||
|
|
@ -581,6 +645,29 @@ def read_only():
|
|||
return wrapper_fn
|
||||
return innfn
|
||||
|
||||
def write_only():
|
||||
# if replica connection exists, we have to replace it momentarily with the primary connection
|
||||
def innfn(fn):
|
||||
def wrapper_fn(*args, **kwargs):
|
||||
primary_db = getattr(local, "primary_db", None)
|
||||
replica_db = getattr(local, "replica_db", None)
|
||||
in_read_only = getattr(local, "db", None) != primary_db
|
||||
|
||||
# switch to primary connection
|
||||
if in_read_only and primary_db:
|
||||
local.db = local.primary_db
|
||||
|
||||
try:
|
||||
retval = fn(*args, **get_newargs(fn, kwargs))
|
||||
finally:
|
||||
# switch back to replica connection
|
||||
if in_read_only and replica_db:
|
||||
local.db = replica_db
|
||||
|
||||
return retval
|
||||
return wrapper_fn
|
||||
return innfn
|
||||
|
||||
def only_for(roles, message=False):
|
||||
"""Raise `frappe.PermissionError` if the user does not have any of the given **Roles**.
|
||||
|
||||
|
|
@ -651,23 +738,34 @@ def only_has_select_perm(doctype, user=None, ignore_permissions=False):
|
|||
else:
|
||||
return False
|
||||
|
||||
def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False):
|
||||
def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False, parent_doctype=None):
|
||||
"""Raises `frappe.PermissionError` if not permitted.
|
||||
|
||||
:param doctype: DocType for which permission is to be check.
|
||||
:param ptype: Permission type (`read`, `write`, `create`, `submit`, `cancel`, `amend`). Default: `read`.
|
||||
:param doc: [optional] Checks User permissions for given doc.
|
||||
:param user: [optional] Check for given user. Default: current user."""
|
||||
:param user: [optional] Check for given user. Default: current user.
|
||||
:param parent_doctype: Required when checking permission for a child DocType (unless doc is specified)."""
|
||||
import frappe.permissions
|
||||
|
||||
if not doctype and doc:
|
||||
doctype = doc.doctype
|
||||
|
||||
import frappe.permissions
|
||||
out = frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user, raise_exception=throw)
|
||||
out = frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user,
|
||||
raise_exception=throw, parent_doctype=parent_doctype)
|
||||
|
||||
if throw and not out:
|
||||
if doc:
|
||||
frappe.throw(_("No permission for {0}").format(doc.doctype + " " + doc.name))
|
||||
else:
|
||||
frappe.throw(_("No permission for {0}").format(doctype))
|
||||
# mimics frappe.throw
|
||||
document_label = f"{doc.doctype} {doc.name}" if doc else doctype
|
||||
msgprint(
|
||||
_("No permission for {0}").format(document_label),
|
||||
raise_exception=ValidationError,
|
||||
title=None,
|
||||
indicator='red',
|
||||
is_minimizable=None,
|
||||
wide=None,
|
||||
as_list=False
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
|
@ -683,7 +781,7 @@ def has_website_permission(doc=None, ptype='read', user=None, verbose=False, doc
|
|||
user = session.user
|
||||
|
||||
if doc:
|
||||
if isinstance(doc, string_types):
|
||||
if isinstance(doc, str):
|
||||
doc = get_doc(doctype, doc)
|
||||
|
||||
doctype = doc.doctype
|
||||
|
|
@ -712,7 +810,9 @@ def has_website_permission(doc=None, ptype='read', user=None, verbose=False, doc
|
|||
def is_table(doctype):
|
||||
"""Returns True if `istable` property (indicating child Table) is set for given DocType."""
|
||||
def get_tables():
|
||||
return db.sql_list("select name from tabDocType where istable=1")
|
||||
return db.get_values(
|
||||
"DocType", filters={"istable": 1}, order_by=None, pluck=True
|
||||
)
|
||||
|
||||
tables = cache().get_value("is_table", get_tables)
|
||||
return doctype in tables
|
||||
|
|
@ -752,8 +852,7 @@ def set_value(doctype, docname, fieldname, value=None):
|
|||
return frappe.client.set_value(doctype, docname, fieldname, value)
|
||||
|
||||
def get_cached_doc(*args, **kwargs):
|
||||
if args and len(args) > 1 and isinstance(args[1], text_type):
|
||||
key = get_document_cache_key(args[0], args[1])
|
||||
if key := can_cache_doc(args):
|
||||
# local cache
|
||||
doc = local.document_cache.get(key)
|
||||
if doc:
|
||||
|
|
@ -771,8 +870,24 @@ def get_cached_doc(*args, **kwargs):
|
|||
|
||||
return doc
|
||||
|
||||
def can_cache_doc(args):
|
||||
"""
|
||||
Determine if document should be cached based on get_doc params.
|
||||
Returns cache key if doc can be cached, None otherwise.
|
||||
"""
|
||||
|
||||
if not args:
|
||||
return
|
||||
|
||||
doctype = args[0]
|
||||
name = doctype if len(args) == 1 else args[1]
|
||||
|
||||
# Only cache if both doctype and name are strings
|
||||
if isinstance(doctype, str) and isinstance(name, str):
|
||||
return get_document_cache_key(doctype, name)
|
||||
|
||||
def get_document_cache_key(doctype, name):
|
||||
return '{0}::{1}'.format(doctype, name)
|
||||
return f'{doctype}::{name}'
|
||||
|
||||
def clear_document_cache(doctype, name):
|
||||
cache().hdel("last_modified", doctype)
|
||||
|
|
@ -783,7 +898,7 @@ def clear_document_cache(doctype, name):
|
|||
|
||||
def get_cached_value(doctype, name, fieldname, as_dict=False):
|
||||
doc = get_cached_doc(doctype, name)
|
||||
if isinstance(fieldname, string_types):
|
||||
if isinstance(fieldname, str):
|
||||
if as_dict:
|
||||
throw('Cannot make dict for single fieldname')
|
||||
return doc.get(fieldname)
|
||||
|
|
@ -813,8 +928,7 @@ def get_doc(*args, **kwargs):
|
|||
doc = frappe.model.document.get_doc(*args, **kwargs)
|
||||
|
||||
# set in cache
|
||||
if args and len(args) > 1:
|
||||
key = get_document_cache_key(args[0], args[1])
|
||||
if key := can_cache_doc(args):
|
||||
local.document_cache[key] = doc
|
||||
cache().hset('document_cache', key, doc.as_dict())
|
||||
|
||||
|
|
@ -847,8 +961,8 @@ def get_meta_module(doctype):
|
|||
import frappe.modules
|
||||
return frappe.modules.load_doctype_module(doctype)
|
||||
|
||||
def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None,
|
||||
for_reload=False, ignore_permissions=False, flags=None, ignore_on_trash=False, ignore_missing=True):
|
||||
def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reload=False,
|
||||
ignore_permissions=False, flags=None, ignore_on_trash=False, ignore_missing=True, delete_permanently=False):
|
||||
"""Delete a document. Calls `frappe.model.delete_doc.delete_doc`.
|
||||
|
||||
:param doctype: DocType of document to be delete.
|
||||
|
|
@ -856,10 +970,11 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None,
|
|||
:param force: Allow even if document is linked. Warning: This may lead to data integrity errors.
|
||||
:param ignore_doctypes: Ignore if child table is one of these.
|
||||
:param for_reload: Call `before_reload` trigger before deleting.
|
||||
:param ignore_permissions: Ignore user permissions."""
|
||||
:param ignore_permissions: Ignore user permissions.
|
||||
:param delete_permanently: Do not create a Deleted Document for the document."""
|
||||
import frappe.model.delete_doc
|
||||
frappe.model.delete_doc.delete_doc(doctype, name, force, ignore_doctypes, for_reload,
|
||||
ignore_permissions, flags, ignore_on_trash, ignore_missing)
|
||||
ignore_permissions, flags, ignore_on_trash, ignore_missing, delete_permanently)
|
||||
|
||||
def delete_doc_if_exists(doctype, name, force=0):
|
||||
"""Delete document if exists."""
|
||||
|
|
@ -902,7 +1017,7 @@ def get_module(modulename):
|
|||
|
||||
def scrub(txt):
|
||||
"""Returns sluggified string. e.g. `Sales Order` becomes `sales_order`."""
|
||||
return txt.replace(' ', '_').replace('-', '_').lower()
|
||||
return cstr(txt).replace(' ', '_').replace('-', '_').lower()
|
||||
|
||||
def unscrub(txt):
|
||||
"""Returns titlified string. e.g. `sales_order` becomes `Sales Order`."""
|
||||
|
|
@ -936,7 +1051,7 @@ def get_pymodule_path(modulename, *joins):
|
|||
:param *joins: Join additional path elements using `os.path.join`."""
|
||||
if not "public" in joins:
|
||||
joins = [scrub(part) for part in joins]
|
||||
return os.path.join(os.path.dirname(get_module(scrub(modulename)).__file__), *joins)
|
||||
return os.path.join(os.path.dirname(get_module(scrub(modulename)).__file__ or ''), *joins)
|
||||
|
||||
def get_module_list(app_name):
|
||||
"""Get list of modules for given all via `app/modules.txt`."""
|
||||
|
|
@ -988,7 +1103,7 @@ def get_doc_hooks():
|
|||
if not hasattr(local, 'doc_events_hooks'):
|
||||
hooks = get_hooks('doc_events', {})
|
||||
out = {}
|
||||
for key, value in iteritems(hooks):
|
||||
for key, value in hooks.items():
|
||||
if isinstance(key, tuple):
|
||||
for doctype in key:
|
||||
append_hook(out, doctype, value)
|
||||
|
|
@ -1070,9 +1185,7 @@ def setup_module_map():
|
|||
|
||||
if not (local.app_modules and local.module_app):
|
||||
local.module_app, local.app_modules = {}, {}
|
||||
for app in get_all_apps(True):
|
||||
if app == "webnotes":
|
||||
app = "frappe"
|
||||
for app in get_all_apps(with_internal_apps=True):
|
||||
local.app_modules.setdefault(app, [])
|
||||
for module in get_module_list(app):
|
||||
module = scrub(module)
|
||||
|
|
@ -1105,7 +1218,7 @@ def get_file_json(path):
|
|||
|
||||
def read_file(path, raise_not_found=False):
|
||||
"""Open a file and return its content as Unicode."""
|
||||
if isinstance(path, text_type):
|
||||
if isinstance(path, str):
|
||||
path = path.encode("utf-8")
|
||||
|
||||
if os.path.exists(path):
|
||||
|
|
@ -1119,7 +1232,7 @@ def read_file(path, raise_not_found=False):
|
|||
def get_attr(method_string):
|
||||
"""Get python method object from its name."""
|
||||
app_name = method_string.split(".")[0]
|
||||
if not local.flags.in_install and app_name not in get_installed_apps():
|
||||
if not local.flags.in_uninstall and not local.flags.in_install and app_name not in get_installed_apps():
|
||||
throw(_("App {0} is not installed").format(app_name), AppNotInstalledError)
|
||||
|
||||
modulename = '.'.join(method_string.split('.')[:-1])
|
||||
|
|
@ -1128,7 +1241,7 @@ def get_attr(method_string):
|
|||
|
||||
def call(fn, *args, **kwargs):
|
||||
"""Call a function and match arguments."""
|
||||
if isinstance(fn, string_types):
|
||||
if isinstance(fn, str):
|
||||
fn = get_attr(fn)
|
||||
|
||||
newargs = get_newargs(fn, kwargs)
|
||||
|
|
@ -1139,13 +1252,9 @@ def get_newargs(fn, kwargs):
|
|||
if hasattr(fn, 'fnargs'):
|
||||
fnargs = fn.fnargs
|
||||
else:
|
||||
try:
|
||||
fnargs, varargs, varkw, defaults = inspect.getargspec(fn)
|
||||
except ValueError:
|
||||
fnargs = inspect.getfullargspec(fn).args
|
||||
varargs = inspect.getfullargspec(fn).varargs
|
||||
varkw = inspect.getfullargspec(fn).varkw
|
||||
defaults = inspect.getfullargspec(fn).defaults
|
||||
fnargs = inspect.getfullargspec(fn).args
|
||||
fnargs.extend(inspect.getfullargspec(fn).kwonlyargs)
|
||||
varkw = inspect.getfullargspec(fn).varkw
|
||||
|
||||
newargs = {}
|
||||
for a in kwargs:
|
||||
|
|
@ -1195,10 +1304,10 @@ def make_property_setter(args, ignore_validate=False, validate_fields_for_doctyp
|
|||
ps.validate_fieldtype_change()
|
||||
ps.insert()
|
||||
|
||||
def import_doc(path, ignore_links=False, ignore_insert=False, insert=False):
|
||||
def import_doc(path):
|
||||
"""Import a file using Data Import."""
|
||||
from frappe.core.doctype.data_import.data_import import import_doc
|
||||
import_doc(path, ignore_links=ignore_links, ignore_insert=ignore_insert, insert=insert)
|
||||
import_doc(path)
|
||||
|
||||
def copy_doc(doc, ignore_no_copy=True):
|
||||
""" No_copy fields also get copied."""
|
||||
|
|
@ -1370,7 +1479,7 @@ def get_list(doctype, *args, **kwargs):
|
|||
frappe.get_list("ToDo", fields="*", filters = {"description": ("like", "test%")})
|
||||
"""
|
||||
import frappe.model.db_query
|
||||
return frappe.model.db_query.DatabaseQuery(doctype).execute(None, *args, **kwargs)
|
||||
return frappe.model.db_query.DatabaseQuery(doctype).execute(*args, **kwargs)
|
||||
|
||||
def get_all(doctype, *args, **kwargs):
|
||||
"""List database query via `frappe.model.db_query`. Will **not** check for permissions.
|
||||
|
|
@ -1415,7 +1524,10 @@ def get_value(*args, **kwargs):
|
|||
|
||||
def as_json(obj, indent=1):
|
||||
from frappe.utils.response import json_handler
|
||||
return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler, separators=(',', ': '))
|
||||
try:
|
||||
return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler, separators=(',', ': '))
|
||||
except TypeError:
|
||||
return json.dumps(obj, indent=indent, default=json_handler, separators=(',', ': '))
|
||||
|
||||
def are_emails_muted():
|
||||
from frappe.utils import cint
|
||||
|
|
@ -1447,8 +1559,8 @@ def format(*args, **kwargs):
|
|||
import frappe.utils.formatters
|
||||
return frappe.utils.formatters.format_value(*args, **kwargs)
|
||||
|
||||
def get_print(doctype=None, name=None, print_format=None, style=None,
|
||||
html=None, as_pdf=False, doc=None, output=None, no_letterhead=0, password=None):
|
||||
def get_print(doctype=None, name=None, print_format=None, style=None, html=None,
|
||||
as_pdf=False, doc=None, output=None, no_letterhead=0, password=None, pdf_options=None):
|
||||
"""Get Print Format for given document.
|
||||
|
||||
:param doctype: DocType of document.
|
||||
|
|
@ -1457,7 +1569,7 @@ def get_print(doctype=None, name=None, print_format=None, style=None,
|
|||
:param style: Print Format style.
|
||||
:param as_pdf: Return as PDF. Default False.
|
||||
:param password: Password to encrypt the pdf with. Default None"""
|
||||
from frappe.website.render import build_page
|
||||
from frappe.website.serve import get_response_content
|
||||
from frappe.utils.pdf import get_pdf
|
||||
|
||||
local.form_dict.doctype = doctype
|
||||
|
|
@ -1467,15 +1579,15 @@ def get_print(doctype=None, name=None, print_format=None, style=None,
|
|||
local.form_dict.doc = doc
|
||||
local.form_dict.no_letterhead = no_letterhead
|
||||
|
||||
options = None
|
||||
pdf_options = pdf_options or {}
|
||||
if password:
|
||||
options = {'password': password}
|
||||
pdf_options['password'] = password
|
||||
|
||||
if not html:
|
||||
html = build_page("printview")
|
||||
html = get_response_content("printview")
|
||||
|
||||
if as_pdf:
|
||||
return get_pdf(html, output = output, options = options)
|
||||
return get_pdf(html, options=pdf_options, output=output)
|
||||
else:
|
||||
return html
|
||||
|
||||
|
|
@ -1566,7 +1678,7 @@ def local_cache(namespace, key, generator, regenerate_if_none=False):
|
|||
if key not in local.cache[namespace]:
|
||||
local.cache[namespace][key] = generator()
|
||||
|
||||
elif local.cache[namespace][key]==None and regenerate_if_none:
|
||||
elif local.cache[namespace][key] is None and regenerate_if_none:
|
||||
# if key exists but the previous result was None
|
||||
local.cache[namespace][key] = generator()
|
||||
|
||||
|
|
@ -1587,6 +1699,12 @@ def enqueue(*args, **kwargs):
|
|||
import frappe.utils.background_jobs
|
||||
return frappe.utils.background_jobs.enqueue(*args, **kwargs)
|
||||
|
||||
def task(**task_kwargs):
|
||||
def decorator_task(f):
|
||||
f.enqueue = lambda **fun_kwargs: enqueue(f, **task_kwargs, **fun_kwargs)
|
||||
return f
|
||||
return decorator_task
|
||||
|
||||
def enqueue_doc(*args, **kwargs):
|
||||
'''
|
||||
Enqueue method to be executed using a background worker
|
||||
|
|
@ -1643,7 +1761,7 @@ def get_desk_link(doctype, name):
|
|||
)
|
||||
|
||||
def bold(text):
|
||||
return '<b>{0}</b>'.format(text)
|
||||
return '<strong>{0}</strong>'.format(text)
|
||||
|
||||
def safe_eval(code, eval_globals=None, eval_locals=None):
|
||||
'''A safer `eval`'''
|
||||
|
|
@ -1654,6 +1772,23 @@ def safe_eval(code, eval_globals=None, eval_locals=None):
|
|||
"round": round
|
||||
}
|
||||
|
||||
UNSAFE_ATTRIBUTES = {
|
||||
# Generator Attributes
|
||||
"gi_frame", "gi_code",
|
||||
# Coroutine Attributes
|
||||
"cr_frame", "cr_code", "cr_origin",
|
||||
# Async Generator Attributes
|
||||
"ag_code", "ag_frame",
|
||||
# Traceback Attributes
|
||||
"tb_frame", "tb_next",
|
||||
# Format Attributes
|
||||
"format", "format_map",
|
||||
}
|
||||
|
||||
for attribute in UNSAFE_ATTRIBUTES:
|
||||
if attribute in code:
|
||||
throw('Illegal rule {0}. Cannot use "{1}"'.format(bold(code), attribute))
|
||||
|
||||
if '__' in code:
|
||||
throw('Illegal rule {0}. Cannot use "__"'.format(bold(code)))
|
||||
|
||||
|
|
@ -1699,7 +1834,7 @@ def get_version(doctype, name, limit=None, head=False, raise_err=True):
|
|||
'limit': limit
|
||||
}, as_list=1)
|
||||
|
||||
from frappe.chat.util import squashify, dictify, safe_json_loads
|
||||
from frappe.utils import squashify, dictify, safe_json_loads
|
||||
|
||||
versions = []
|
||||
|
||||
|
|
@ -1747,16 +1882,17 @@ def parse_json(val):
|
|||
return parse_json(val)
|
||||
|
||||
def mock(type, size=1, locale='en'):
|
||||
import faker
|
||||
results = []
|
||||
faker = Faker(locale)
|
||||
if not type in dir(faker):
|
||||
fake = faker.Faker(locale)
|
||||
if type not in dir(fake):
|
||||
raise ValueError('Not a valid mock type.')
|
||||
else:
|
||||
for i in range(size):
|
||||
data = getattr(faker, type)()
|
||||
data = getattr(fake, type)()
|
||||
results.append(data)
|
||||
|
||||
from frappe.chat.util import squashify
|
||||
from frappe.utils import squashify
|
||||
return squashify(results)
|
||||
|
||||
def validate_and_sanitize_search_inputs(fn):
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue