Merge branch 'develop' into user-permission-not-saved-fix

This commit is contained in:
Suraj Shetty 2022-03-17 15:41:07 +05:30 committed by GitHub
commit 1d03424e26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
1998 changed files with 65265 additions and 155568 deletions

View file

@ -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

View file

@ -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
View 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
View 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

View file

@ -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,

View file

@ -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",

View file

@ -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
View 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
View 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

View file

@ -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,

View file

@ -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
View 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
View file

@ -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
View file

@ -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
View 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

View file

@ -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"}'

View file

@ -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
View 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

View 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

View file

@ -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

View file

@ -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

View 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

View 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

View file

@ -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
View 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
View file

@ -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

View file

@ -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
View 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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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
View 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

View file

@ -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"]
}

View 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
};

View 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
};

View 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
};

View 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
};

View 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
};

View 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
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

View file

@ -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);
});
});
});
});

View file

@ -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');

View 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();
});
});
});

View file

@ -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"]')

View file

@ -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');
});

View 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"
}
]
}
];
}
});

View 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');
});
});

View file

@ -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", ""
);
});
});

View file

@ -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);
});
});

View 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();
});
});
});

View 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();
});
});

View 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');
});
});

View 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');
});
});
});

View file

@ -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', {

View 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');
// });
// });
// });

View file

@ -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');

View 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);
});

View file

@ -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');
});
});

View 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();
});
});

View 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();
});
});

View file

@ -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');
});
});

View 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");
});
});

View 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');
});
});

View 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();
});
});
});

View 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');
});
});

View 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);
});

View file

@ -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');

View 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);
});
});

View 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');
});
});

View file

@ -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');
});
});
});

View file

@ -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();
});
});

View file

@ -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'));

View 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");
}
});
});
});

View 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');
});
});

View 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();
});
});

View file

@ -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');
});
});

View file

@ -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();
});
});
});

View file

@ -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');
// });
// });

View file

@ -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);
});
});
});
});

View 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');
});
});

View file

@ -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');

View 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');
});
});

View 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();
});
});

View 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');
});
});
});

View 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');
});
});
});

View 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');
});
});

View file

@ -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;
};

View file

@ -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();
});

View file

@ -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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1 @@
require("./esbuild");

29
esbuild/sass_options.js Normal file
View 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
View 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
};

View file

@ -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