Merge branch 'lts-3.16' of https://github.com/ToolJet/ToolJet into fix/input-component-issues

This commit is contained in:
Nishidh Jain 2025-11-03 14:16:10 +05:30
commit 5c16fa22d7
225 changed files with 9470 additions and 4117 deletions

View file

@ -119,7 +119,7 @@ jobs:
TJDB_SQL_MODE_DISABLE: ${{ secrets.CLOUD_TJDB_SQL_MODE_DISABLE }}
TOOLJET_SERVER_URL: ${{ secrets.CLOUD_TOOLJET_SERVER_URL }}
TOOLJET_EDITION: cloud
WEBSITE_SIGNUP_URL: https://website-stage.tooljet.ai/ai-create-account
WEBSITE_SIGNUP_URL: https://website-stage.tooljet.ai/signup
- name: 🚀 Deploy to Netlify
run: |
@ -138,5 +138,52 @@ jobs:
SERVER_IP: ${{ secrets.CLOUD_SERVER_IP }}
TJDB_SQL_MODE_DISABLE: ${{ secrets.CLOUD_TJDB_SQL_MODE_DISABLE }}
TOOLJET_SERVER_URL: ${{ secrets.CLOUD_TOOLJET_SERVER_URL }}
WEBSITE_SIGNUP_URL: https://website-stage.tooljet.ai/ai-create-account
WEBSITE_SIGNUP_URL: https://website-stage.tooljet.ai/signup
TOOLJET_EDITION: cloud
Purge_Cloudflare_Cache:
needs: deploy
runs-on: ubuntu-latest
steps:
- name: ✅ Check user authorization
run: |
allowed_user1=${{ secrets.ALLOWED_USER1_USERNAME }}
allowed_user2=${{ secrets.ALLOWED_USER2_USERNAME }}
allowed_user3=${{ secrets.ALLOWED_USER3_USERNAME }}
if [[ "${{ github.actor }}" != "$allowed_user1" && \
"${{ github.actor }}" != "$allowed_user2" && \
"${{ github.actor }}" != "$allowed_user3" ]]; then
echo "❌ User '${{ github.actor }}' is not authorized to trigger this workflow."
exit 1
else
echo "✅ User '${{ github.actor }}' is authorized."
fi
- name: 🧹 Purge Cloudflare Cache
continue-on-error: true
run: |
echo "🔄 Purging Cloudflare cache for specific URLs..."
response=$(curl -s -w "\n%{http_code}" -X POST \
"https://api.cloudflare.com/client/v4/zones/${{ secrets.CLOUDFLARE_ZONE_ID_PROD }}/purge_cache" \
-H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN_PROD }}" \
-H "Content-Type: application/json" \
--data '{
"files": [
"${{ secrets.CLOUDFLARE_CONFIG_URL_STAGE }}",
"${{ secrets.CLOUDFLARE_METADATA_URL_STAGE }}"
]
}')
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" = "200" ]; then
echo "✅ Cloudflare cache purged successfully for specified URLs"
echo "$body"
else
echo "⚠️ Cloudflare cache purge failed with status code: $http_code"
echo "$body"
exit 1
fi

View file

@ -123,7 +123,7 @@ jobs:
- name: 🚀 Deploy to Netlify
run: |
npm install -g netlify-cli@17.10.1
npm install -g netlify-cli
netlify deploy --prod --dir=frontend/build --auth=$NETLIFY_AUTH_TOKEN --site=${{ secrets.CLOUD_PROD_NETLIFY_SITE_ID }}
working-directory: repo
env:
@ -138,5 +138,52 @@ jobs:
SERVER_IP: ${{ secrets.CLOUD_PROD_CLOUD_SERVER_IP }}
TJDB_SQL_MODE_DISABLE: ${{ secrets.CLOUD_PROD_TJDB_SQL_MODE_DISABLE }}
TOOLJET_SERVER_URL: ${{ secrets.CLOUD_PROD_TOOLJET_SERVER_URL }}
WEBSITE_SIGNUP_URL: https://tooljet.ai/ai-create-account
WEBSITE_SIGNUP_URL: https://www.tooljet.ai/create-account
TOOLJET_EDITION: cloud
Purge_Cloudflare_Cache:
needs: deploy
runs-on: ubuntu-latest
steps:
- name: ✅ Check user authorization
run: |
allowed_user1=${{ secrets.ALLOWED_USER1_USERNAME }}
allowed_user2=${{ secrets.ALLOWED_USER2_USERNAME }}
allowed_user3=${{ secrets.ALLOWED_USER3_USERNAME }}
if [[ "${{ github.actor }}" != "$allowed_user1" && \
"${{ github.actor }}" != "$allowed_user2" && \
"${{ github.actor }}" != "$allowed_user3" ]]; then
echo "❌ User '${{ github.actor }}' is not authorized to trigger this workflow."
exit 1
else
echo "✅ User '${{ github.actor }}' is authorized."
fi
- name: 🧹 Purge Cloudflare Cache
continue-on-error: true
run: |
echo "🔄 Purging Cloudflare cache for specific URLs..."
response=$(curl -s -w "\n%{http_code}" -X POST \
"https://api.cloudflare.com/client/v4/zones/${{ secrets.CLOUDFLARE_ZONE_ID_PROD }}/purge_cache" \
-H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN_PROD }}" \
-H "Content-Type: application/json" \
--data '{
"files": [
"${{ secrets.CLOUDFLARE_CONFIG_URL_PROD }}",
"${{ secrets.CLOUDFLARE_METADATA_URL_PROD }}"
]
}')
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" = "200" ]; then
echo "✅ Cloudflare cache purged successfully for specified URLs"
echo "$body"
else
echo "⚠️ Cloudflare cache purge failed with status code: $http_code"
echo "$body"
exit 1
fi

View file

@ -37,6 +37,29 @@ jobs:
echo "Labels: ${{ toJSON(github.event.pull_request.labels.*.name) }}"
echo "Matrix edition: ${{ matrix.edition }}"
- name: Free up disk space
run: |
echo "Available disk space before cleanup:"
df -h
# Remove unnecessary packages
sudo apt-get remove -y '^aspnetcore-.*' '^dotnet-.*' '^llvm-.*' '^php.*' '^mongodb-.*' '^mysql-.*' azure-cli google-cloud-sdk hhvm google-chrome-stable firefox powershell mono-devel || true
sudo apt-get autoremove -y
sudo apt-get clean
# Remove large directories
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/share/boost
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
# Clean Docker
docker system prune -af --volumes
echo "Available disk space after cleanup:"
df -h
- name: Checkout
uses: actions/checkout@v3
with:
@ -56,32 +79,47 @@ jobs:
- name: Build CE Docker image
if: matrix.edition == 'ce'
uses: docker/build-push-action@v4
with:
context: .
file: docker/ce-production.Dockerfile
push: true
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "Building CE Docker image..."
docker buildx build \
--platform=linux/amd64 \
-f docker/ce-production.Dockerfile \
-t tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce \
--no-cache \
--load \
.
echo "Pushing CE Docker image..."
docker push tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce
echo "Cleaning up build cache..."
docker builder prune -af
echo "Disk space after build:"
df -h
- name: Build EE Docker image
if: matrix.edition == 'ee'
uses: docker/build-push-action@v4
with:
context: .
build-args: |
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}
BRANCH_NAME=${{ github.event.pull_request.head.ref }}
file: cypress-tests/cypress-lts.Dockerfile
push: true
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "Building EE Docker image..."
docker buildx build \
--platform=linux/amd64 \
-f cypress-tests/cypress-lts.Dockerfile \
--build-arg CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }} \
--build-arg BRANCH_NAME=${{ github.event.pull_request.head.ref }} \
-t tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee \
--no-cache \
--load \
.
echo "Pushing EE Docker image..."
docker push tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee
echo "Cleaning up build cache..."
docker builder prune -af
echo "Disk space after build:"
df -h
- name: Set up environment variables
run: |
@ -119,7 +157,10 @@ jobs:
echo "LICENSE_KEY=${{ secrets.RENDER_LICENSE_KEY }}" >> .env
- name: clean up old docker containers
run: docker system prune -af
run: |
docker system prune -af --volumes
echo "Disk space after Docker cleanup:"
df -h
- name: Pulling the docker-compose file
run: curl -LO https://tooljet-test.s3.us-west-1.amazonaws.com/docker-compose.yaml && mkdir postgres_data
@ -182,6 +223,112 @@ jobs:
echo "✅ Server is ready!"
- name: Test database connection
run: |
# Wait for database to be ready
echo "Testing database connection..."
docker exec Tooljet-postgres psql -U postgres -d tooljet_development -c "SELECT current_database();"
- name: Create delete_user procedure
run: |
echo "Creating delete_user stored procedure..."
docker exec Tooljet-postgres psql -U postgres -d tooljet_development -c "
CREATE OR REPLACE PROCEDURE delete_user(p_email TEXT)
LANGUAGE plpgsql
AS \$\$
DECLARE
v_user_id UUID;
v_organization_ids UUID[];
v_organizations_to_delete UUID[];
v_log_message TEXT;
BEGIN
-- Log start of procedure
RAISE NOTICE 'Starting delete_user procedure for email: %', p_email;
-- Get user_id from email
SELECT id INTO v_user_id
FROM users
WHERE email = p_email;
IF v_user_id IS NULL THEN
RAISE EXCEPTION 'User with email % not found', p_email;
END IF;
RAISE NOTICE 'User found with id: %', v_user_id;
-- Get organization_ids for the user
SELECT ARRAY_AGG(organization_id) INTO v_organization_ids
FROM organization_users
WHERE user_id = v_user_id;
RAISE NOTICE 'Found % organizations for user', COALESCE(ARRAY_LENGTH(v_organization_ids, 1), 0);
-- Get organizations with exactly one user (to be deleted)
SELECT ARRAY_AGG(organization_id) INTO v_organizations_to_delete
FROM (
SELECT organization_id
FROM organization_users
WHERE organization_id = ANY(v_organization_ids)
GROUP BY organization_id
HAVING COUNT(*) = 1
) subquery;
-- Add organizations where the user is the owner
v_organizations_to_delete := v_organizations_to_delete;
RAISE NOTICE 'Found % organizations to delete', COALESCE(ARRAY_LENGTH(v_organizations_to_delete, 1), 0);
-- Delete apps in organizations to be deleted
WITH deleted_apps AS (
DELETE FROM apps
WHERE organization_id = ANY(v_organizations_to_delete)
RETURNING id
)
SELECT 'Deleted ' || COUNT(*) || ' apps' INTO v_log_message FROM deleted_apps;
RAISE NOTICE '%', v_log_message;
-- Delete data_sources in organizations to be deleted
WITH deleted_data_sources AS (
DELETE FROM data_sources
WHERE organization_id = ANY(v_organizations_to_delete)
RETURNING id
)
SELECT 'Deleted ' || COUNT(*) || ' data sources' INTO v_log_message FROM deleted_data_sources;
RAISE NOTICE '%', v_log_message;
-- Delete audit_logs for organizations to be deleted and for the user
WITH deleted_audit_logs AS (
DELETE FROM audit_logs
WHERE organization_id = ANY(v_organization_ids)
OR user_id = v_user_id
RETURNING id
)
SELECT 'Deleted ' || COUNT(*) || ' audit logs' INTO v_log_message FROM deleted_audit_logs;
RAISE NOTICE '%', v_log_message;
-- Delete organizations
WITH deleted_organizations AS (
DELETE FROM organizations
WHERE id = ANY(v_organizations_to_delete)
RETURNING id
)
SELECT 'Deleted ' || COUNT(*) || ' organizations' INTO v_log_message FROM deleted_organizations;
RAISE NOTICE '%', v_log_message;
-- Finally, delete the user
DELETE FROM users
WHERE id = v_user_id;
RAISE NOTICE 'Deleted user with id: %', v_user_id;
RAISE NOTICE 'User deletion procedure completed successfully for email: %', p_email;
END;
\$\$;"
echo "✅ delete_user procedure created successfully"
- name: Create Cypress environment file
id: create-json-tj
uses: jsdaniell/create-json@1.1.2
@ -250,32 +397,47 @@ jobs:
- name: Build CE Docker image
if: matrix.edition == 'ce'
uses: docker/build-push-action@v4
with:
context: .
file: docker/ce-production.Dockerfile
push: true
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "Building CE Docker image..."
docker buildx build \
--platform=linux/amd64 \
-f docker/ce-production.Dockerfile \
-t tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce \
--no-cache \
--load \
.
echo "Pushing CE Docker image..."
docker push tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce
echo "Cleaning up build cache..."
docker builder prune -af
echo "Disk space after build:"
df -h
- name: Build EE Docker image
if: matrix.edition == 'ee'
uses: docker/build-push-action@v4
with:
context: .
build-args: |
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}
BRANCH_NAME=${{ github.event.pull_request.head.ref }}
file: cypress-tests/cypress-lts.Dockerfile
push: true
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "Building EE Docker image..."
docker buildx build \
--platform=linux/amd64 \
-f cypress-tests/cypress-lts.Dockerfile \
--build-arg CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }} \
--build-arg BRANCH_NAME=${{ github.event.pull_request.head.ref }} \
-t tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee \
--no-cache \
--load \
.
echo "Pushing EE Docker image..."
docker push tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee
echo "Cleaning up build cache..."
docker builder prune -af
echo "Disk space after build:"
df -h
- name: Set up environment variables
run: |
@ -315,7 +477,10 @@ jobs:
echo "LICENSE_KEY=${{ secrets.RENDER_LICENSE_KEY }}" >> .env
- name: clean up old docker containers
run: docker system prune -af
run: |
docker system prune -af --volumes
echo "Disk space after Docker cleanup:"
df -h
- name: Pulling the docker-compose file
run: curl -LO https://tooljet-test.s3.us-west-1.amazonaws.com/docker-compose.yaml && mkdir postgres_data
@ -436,32 +601,47 @@ jobs:
- name: Build CE Docker image
if: matrix.edition == 'ce'
uses: docker/build-push-action@v4
with:
context: .
file: docker/ce-production.Dockerfile
push: true
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "Building CE Docker image..."
docker buildx build \
--platform=linux/amd64 \
-f docker/ce-production.Dockerfile \
-t tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce \
--no-cache \
--load \
.
echo "Pushing CE Docker image..."
docker push tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce
echo "Cleaning up build cache..."
docker builder prune -af
echo "Disk space after build:"
df -h
- name: Build EE Docker image
if: matrix.edition == 'ee'
uses: docker/build-push-action@v4
with:
context: .
build-args: |
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}
BRANCH_NAME=${{ github.event.pull_request.head.ref }}
file: cypress-tests/cypress-lts.Dockerfile
push: true
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "Building EE Docker image..."
docker buildx build \
--platform=linux/amd64 \
-f cypress-tests/cypress-lts.Dockerfile \
--build-arg CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }} \
--build-arg BRANCH_NAME=${{ github.event.pull_request.head.ref }} \
-t tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee \
--no-cache \
--load \
.
echo "Pushing EE Docker image..."
docker push tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee
echo "Cleaning up build cache..."
docker builder prune -af
echo "Disk space after build:"
df -h
- name: Set up environment variables
run: |
@ -501,7 +681,10 @@ jobs:
echo "LICENSE_KEY=${{ secrets.RENDER_LICENSE_KEY }}" >> .env
- name: clean up old docker containers
run: docker system prune -af
run: |
docker system prune -af --volumes
echo "Disk space after Docker cleanup:"
df -h
- name: Pulling the docker-compose file
run: curl -LO https://tooljet-test.s3.us-west-1.amazonaws.com/docker-compose.yaml && mkdir postgres_data
@ -635,32 +818,47 @@ jobs:
- name: Build CE Docker image
if: matrix.edition == 'ce'
uses: docker/build-push-action@v4
with:
context: .
file: docker/ce-production.Dockerfile
push: true
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "Building CE Docker image..."
docker buildx build \
--platform=linux/amd64 \
-f docker/ce-production.Dockerfile \
-t tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce \
--no-cache \
--load \
.
echo "Pushing CE Docker image..."
docker push tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce
echo "Cleaning up build cache..."
docker builder prune -af
echo "Disk space after build:"
df -h
- name: Build EE Docker image
if: matrix.edition == 'ee'
uses: docker/build-push-action@v4
with:
context: .
build-args: |
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}
BRANCH_NAME=${{ github.event.pull_request.head.ref }}
file: cypress-tests/cypress-lts.Dockerfile
push: true
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "Building EE Docker image..."
docker buildx build \
--platform=linux/amd64 \
-f cypress-tests/cypress-lts.Dockerfile \
--build-arg CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }} \
--build-arg BRANCH_NAME=${{ github.event.pull_request.head.ref }} \
-t tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee \
--no-cache \
--load \
.
echo "Pushing EE Docker image..."
docker push tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee
echo "Cleaning up build cache..."
docker builder prune -af
echo "Disk space after build:"
df -h
- name: Set up environment variables
run: |
@ -699,7 +897,10 @@ jobs:
echo "LICENSE_KEY=${{ secrets.RENDER_LICENSE_KEY }}" >> .env
- name: clean up old docker containers
run: docker system prune -af
run: |
docker system prune -af --volumes
echo "Disk space after Docker cleanup:"
df -h
- name: Pulling the docker-compose file
run: curl -LO https://tooljet-test.s3.us-west-1.amazonaws.com/docker-compose.yaml && mkdir postgres_data

View file

@ -4,18 +4,18 @@ on:
workflow_dispatch:
inputs:
branch_name:
description: 'Git branch to build from'
description: "Git branch to build from"
required: true
default: 'main'
default: "lts-3.16"
dockerfile_path:
description: 'Path to Dockerfile'
description: "Path to Dockerfile"
required: true
default: './docker/LTS/cloud/cloud-server.Dockerfile'
default: "./docker/LTS/cloud/cloud-server.Dockerfile"
type: choice
options:
- ./docker/LTS/cloud/cloud-server.Dockerfile
docker_tag:
description: 'Docker tag suffix (e.g., cloud-staging-v14)'
description: "Docker tag suffix (e.g., cloud-staging-v14)"
required: true
jobs:
@ -82,7 +82,7 @@ jobs:
BRANCH_NAME=${{ github.event.inputs.branch_name }}
- name: Show the full Docker tag
run: |
run: |
echo "✅ Docker image tagged as: $IMAGE_TAG"
# Deploy to AKS
@ -144,6 +144,7 @@ jobs:
- name: 📥 Manual Git checkout with submodules
run: |
set -e
BRANCH="${{ github.event.inputs.branch_name }}"
REPO="https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/${{ github.repository }}"
@ -155,6 +156,11 @@ jobs:
git clone --recurse-submodules --depth=1 --branch "$BRANCH" "$REPO" repo
cd repo
echo "🔎 Main repo: verifying checkout"
MAIN_CURRENT=$(git rev-parse --abbrev-ref HEAD)
echo "✅ Main repo: successfully checked out branch $MAIN_CURRENT"
echo "📍 Main repo: current commit $(git rev-parse --short HEAD): $(git log -1 --pretty=%s)"
echo "🔁 Updating submodules"
git submodule update --init --recursive
@ -162,13 +168,44 @@ jobs:
BRANCH="$BRANCH" git submodule foreach --recursive bash -c '
name="$sm_path"
echo "↪ $name: checking out branch $BRANCH"
echo ""
echo "Entering '\''$name'\''"
echo "↪ $name: trying to checkout branch '\''$BRANCH'\''"
if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null; then
git fetch origin "$BRANCH:$BRANCH"
git checkout "$BRANCH"
git fetch origin "$BRANCH:$BRANCH" || {
echo "❌ $name: fetch failed for $BRANCH"
exit 1
}
PREV=$(git rev-parse --short HEAD || echo "unknown")
git checkout "$BRANCH" || {
echo "❌ $name: checkout failed for $BRANCH"
exit 1
}
echo "Previous HEAD position was $PREV: $(git log -1 --pretty=%s || echo 'unknown')"
echo "✅ $name: checked out branch $BRANCH"
else
echo "⚠️ Branch not found, falling back to main"
git checkout main && git pull origin main
echo "⚠️ $name: branch '$BRANCH' not found on origin. Falling back to 'lts-3.16'"
PREV=$(git rev-parse --short HEAD || echo "unknown")
git fetch origin lts-3.16:lts-3.16 || {
echo "❌ $name: fetch failed for lts-3.16"
exit 1
}
git checkout lts-3.16 || {
echo "❌ $name: fallback to lts-3.16 failed"
exit 1
}
echo "Previous HEAD position was $PREV: $(git log -1 --pretty=%s || echo 'unknown')"
echo "✅ $name: now on branch lts-3.16"
fi
CURRENT=$(git rev-parse --abbrev-ref HEAD)
echo "🔎 $name: current branch = $CURRENT"
if [ "$CURRENT" != "$BRANCH" ] && [ "$CURRENT" != "lts-3.16" ]; then
echo "❌ $name: unexpected branch state — wanted '$BRANCH' or fallback 'lts-3.16', got '$CURRENT'"
exit 1
fi
'
@ -181,7 +218,7 @@ jobs:
run: npm install
working-directory: repo
- name: 🛠️ Build project
- name: 🛠️ Build the project
run: npm run build:plugins:prod && npm run build:frontend
working-directory: repo
env:

View file

@ -4,18 +4,19 @@ on:
workflow_dispatch:
inputs:
branch_name:
description: 'Git branch to build from'
description: "Git branch to build from"
required: true
default: 'main'
default: "main"
dockerfile_path:
description: 'Path to Dockerfile'
description: "Path to Dockerfile"
required: true
type: choice
options:
- ./docker/LTS/ee/ee-production.Dockerfile
- ./docker/pre-release/ee/ee-production.Dockerfile
- ./docker/LTS/cloud/cloud-server.Dockerfile
docker_tag:
description: 'Docker tag suffix (e.g., pre-release-14)'
description: "Docker tag suffix (e.g., pre-release-14)"
required: true
jobs:

View file

@ -4,21 +4,21 @@ on:
workflow_dispatch:
inputs:
branch_name:
description: 'Git branch to build from'
description: "Git branch to build from"
required: true
default: 'main'
default: "main"
dockerfile_path:
description: 'Select Dockerfile'
description: "Select Dockerfile"
required: true
type: choice
options:
- ./docker/LTS/ee/ee-production.Dockerfile
- ./docker/pre-release/ee/ee-production.Dockerfile
docker_tag:
description: 'Docker tag suffix (e.g., pre-release-14, 3.16-lts, etc.)'
description: "Docker tag suffix (e.g., pre-release-14, 3.16-lts, etc.)"
required: true
test_system:
description: 'Select test system'
description: "Select test system"
required: true
type: choice
options:
@ -52,24 +52,27 @@ jobs:
"${{ secrets.ALLOWED_USER10_TEST_SYSTEM }}"
"${{ secrets.ALLOWED_USER11_TEST_SYSTEM }}"
"${{ secrets.ALLOWED_USER12_TEST_SYSTEM }}"
"${{ secrets.ALLOWED_USER13_TEST_SYSTEM }}"
"${{ secrets.ALLOWED_USER14_TEST_SYSTEM }}"
)
current_user="${{ github.actor }}"
authorized=false
for user in "${allowed_users[@]}"; do
if [[ "$current_user" == "$user" ]]; then
authorized=true
break
fi
done
if [[ "$authorized" == "false" ]]; then
echo "❌ User '$current_user' is not authorized to trigger this workflow."
exit 1
else
echo "✅ User '$current_user' is authorized."
fi
- name: Checkout code
uses: actions/checkout@v4
with:
@ -94,6 +97,7 @@ jobs:
else
echo "tag=tooljet/tj-osv:$input_tag" >> $GITHUB_OUTPUT
fi
- name: Build and Push Docker image
uses: docker/build-push-action@v4
with:
@ -105,6 +109,7 @@ jobs:
build-args: |
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}
BRANCH_NAME=${{ github.event.inputs.branch_name }}
- name: Show the full Docker tag
run: echo "✅ Docker image built and pushed:${{ steps.taggen.outputs.tag }}"
@ -116,38 +121,39 @@ jobs:
run: |
test_system="${{ github.event.inputs.test_system }}"
vm_host=$(echo '${{ secrets.VM_HOST_MAP_JSON }}' | jq -r --arg sys "$test_system" '.[$sys]')
if [[ -z "$vm_host" || "$vm_host" == "null" ]]; then
echo "VM mapping not found for $test_system"
exit 1
fi
echo "host=$vm_host" >> $GITHUB_OUTPUT
- name: Deploy to target environment
run: |
echo "$SSH_KEY" > key.pem
chmod 600 key.pem
IMAGE_TAG="${{ steps.taggen.outputs.tag }}"
TARGET_SYSTEM="${{ github.event.inputs.test_system }}"
# Debug: Show what we're deploying
echo "DEBUG: IMAGE_TAG=$IMAGE_TAG"
echo "DEBUG: TARGET_SYSTEM=$TARGET_SYSTEM"
ssh -o StrictHostKeyChecking=no -i key.pem $SSH_USER@${{ steps.vmhost.outputs.host }} << EOF
set -e
IMAGE_TAG="$IMAGE_TAG"
TARGET_SYSTEM="$TARGET_SYSTEM"
cd ~
echo "📁 Finding correct deployment directory"
# Debug: Show variables on remote host
echo "Debug on remote: IMAGE_TAG=\$IMAGE_TAG"
echo "Debug on remote: TARGET_SYSTEM=\$TARGET_SYSTEM"
if [[ "\$TARGET_SYSTEM" == *-3.16-lts ]]; then
echo "Detected LTS system: \$TARGET_SYSTEM"
echo "🔍 Searching for LTS directories..."
@ -178,39 +184,39 @@ jobs:
cd ~
echo "✅ Now in directory: \$(pwd)"
fi
echo "🔐 Docker login"
echo "${{ secrets.DOCKER_PASSWORD }}" | sudo docker login --username "${{ secrets.DOCKER_USERNAME }}" --password-stdin
echo "current image"
cat .env | grep TOOLJET_IMAGE
echo "📦 Reading current TOOLJET_IMAGE from .env"
CURRENT_IMAGE=\$(grep '^TOOLJET_IMAGE=' .env | cut -d '=' -f2- | tr -d '"' | tr -d "'")
echo "Found CURRENT_IMAGE: \$CURRENT_IMAGE"
echo "🛑 Stopping containers"
sudo docker-compose down
echo "📝 Updating .env with new image"
sudo sed -i "s|^TOOLJET_IMAGE=.*|TOOLJET_IMAGE=\$IMAGE_TAG|" .env
echo "📥 Pulling new image: \$IMAGE_TAG"
if [ -z "\$IMAGE_TAG" ]; then
echo "❌ IMAGE_TAG is empty!"
exit 1
fi
sudo docker pull "\$IMAGE_TAG"
echo "🚀 Starting container in background"
sudo docker-compose up -d
# Wait for ToolJet to start and show success message
echo "⏳ Waiting for ToolJet to start (timeout: 300 seconds)..."
SUCCESS_FOUND=false
TIMEOUT=300
ELAPSED=0
while [ \$ELAPSED -lt \$TIMEOUT ]; do
# Check for success message in logs
if sudo docker-compose logs 2>/dev/null | grep -qE "🚀 TOOLJET APPLICATION STARTED SUCCESSFULLY|Ready to use at http://localhost:82 🚀|Ready to use at http://localhost:80"; then
@ -223,7 +229,7 @@ jobs:
sleep 10
ELAPSED=\$((ELAPSED + 10))
done
if [ "\$SUCCESS_FOUND" = false ]; then
echo "❌ Timeout reached without finding success logs"
echo "📄 Showing current logs for troubleshooting..."
@ -244,20 +250,19 @@ jobs:
echo "✅ Rollback completed!"
exit 1
fi
echo "✅ Deployment successful!"
echo "📌 Storing successful deployment info in .env"
sudo sed -i "/^OLD_IMAGE=/d" .env
echo "OLD_IMAGE=\$CURRENT_IMAGE" | sudo tee -a .env
echo "📄 Final application logs:"
sudo docker-compose logs --tail=50
echo "🧹 Pruning old Docker images"
sudo docker image prune -a -f
EOF
env:
SSH_USER: ${{ secrets.AZURE_VM_USER }}
SSH_KEY: ${{ secrets.AZURE_VM_KEY }}

View file

@ -1 +1 @@
3.20.21-lts
3.20.30-lts

View file

@ -19,9 +19,9 @@ module.exports = defineConfig({
trashAssetsBeforeRuns: true,
e2e: {
setupNodeEvents(on, config) {
setupNodeEvents (on, config) {
on("task", {
readPdf(pathToPdf) {
readPdf (pathToPdf) {
return new Promise((resolve) => {
const pdfPath = path.resolve(pathToPdf);
let dataBuffer = fs.readFileSync(pdfPath);
@ -33,7 +33,7 @@ module.exports = defineConfig({
});
on("task", {
readXlsx(filePath) {
readXlsx (filePath) {
return new Promise((resolve, reject) => {
try {
let dataBuffer = fs.readFileSync(filePath);
@ -48,7 +48,7 @@ module.exports = defineConfig({
});
on("task", {
deleteFile(filePath) {
deleteFile (filePath) {
return new Promise((resolve, reject) => {
const fullPath = path.resolve(filePath);
if (fs.existsSync(fullPath)) {
@ -69,7 +69,7 @@ module.exports = defineConfig({
});
on("task", {
deleteFolder(folderName) {
deleteFolder (folderName) {
return new Promise((resolve, reject) => {
if (fs.existsSync(folderName)) {
rmdir(folderName, { maxRetries: 10, recursive: true }, (err) => {
@ -87,7 +87,7 @@ module.exports = defineConfig({
});
on("task", {
dbConnection({ dbconfig, sql }) {
dbConnection ({ dbconfig, sql }) {
const client = new pg.Pool(dbconfig);
return client.query(sql);
},
@ -110,18 +110,20 @@ module.exports = defineConfig({
return config;
},
experimentalRunAllSpecs: true,
experimentalModfyObstructiveThirdPartyCode: true,
baseUrl: "http://localhost:8082",
specPattern: "cypress/e2e/happyPath/**/*.cy.js",
downloadsFolder: "cypress/downloads",
numTestsKeptInMemory: 0,
redirectionLimit: 5,
redirectionLimit: 3,
trashAssetsBeforeRuns: true,
experimentalMemoryManagement: true,
coverage: true,
codeCoverageTasksRegistered: true,
coverage: false,
codeCoverageTasksRegistered: false,
video: false,
videoUploadOnPasses: false,
experimentalStudio: true,
// experimentalStudio: true,
// experimentalPromptCommand: true,
// projectId: "ca6324a0-4210-4f7e-846a-71ca2766ca4",
},
});

View file

@ -1,35 +1,5 @@
const envVar = Cypress.env("environment");
Cypress.Commands.add(
"apiLogin",
(
userEmail = "dev@tooljet.io",
userPassword = "password",
workspaceId = "",
redirection = "/"
) => {
cy.request({
url: `${Cypress.env("server_host")}/api/authenticate/${workspaceId}`,
method: "POST",
body: {
email: userEmail,
password: userPassword,
redirectTo: redirection,
},
})
.its("body")
.then((res) => {
Cypress.env("workspaceId", res.current_organization_id);
Cypress.log({
name: "Api login",
displayName: "LOGIN: ",
message: `: Success`,
});
});
}
);
Cypress.Commands.add("apiCreateGDS", (url, name, kind, options) => {
cy.getCookie("tj_auth_token").then((cookie) => {
cy.request(
@ -64,7 +34,7 @@ Cypress.Commands.add("apiCreateGDS", (url, name, kind, options) => {
});
});
Cypress.Commands.add("apiFetchDataSourcesId", () => {
Cypress.Commands.add("apiFetchDataSourcesIdFromApp", () => {
cy.getAuthHeaders().then((headers) => {
cy.request({
method: "GET",
@ -183,23 +153,6 @@ Cypress.Commands.add(
}
);
Cypress.Commands.add("apiLogout", () => {
cy.getCookie("tj_auth_token").then((cookie) => {
cy.request(
{
method: "GET",
url: `${Cypress.env("server_host")}/api/session/logout`,
headers: {
"Tj-Workspace-Id": Cypress.env("workspaceId"),
Cookie: `tj_auth_token=${cookie.value}`,
},
},
{ log: false }
).then((response) => {
expect(response.status).to.equal(200);
});
});
});
Cypress.Commands.add("apiAddQuery", (queryName, query, dataQueryId) => {
cy.getCookie("tj_auth_token").then((cookie) => {
@ -233,7 +186,7 @@ Cypress.Commands.add("apiAddQuery", (queryName, query, dataQueryId) => {
Cypress.Commands.add(
"apiAddQueryToApp",
({ queryName, options, dsName, dsKind }) => {
({ queryName, options, dataSourceName, dsKind }) => {
cy.getCookie("tj_auth_token", { log: false }).then((cookie) => {
const authToken = cookie?.value;
const workspaceId = Cypress.env("workspaceId");
@ -259,10 +212,10 @@ Cypress.Commands.add(
headers: commonHeaders,
}).then((dsResponse) => {
const dataSource = dsResponse.body.data_sources.find(
(ds) => ds.name === dsName
(ds) => ds.name === dataSourceName
);
const dataSourceID = dataSource.id;
Cypress.env(`${dsName}`, dataSourceID);
Cypress.env(`${dataSourceName}`, dataSourceID);
cy.request({
method: "POST",
@ -378,19 +331,6 @@ Cypress.Commands.add(
}
);
Cypress.Commands.add("apiGetEnvironments", () => {
cy.getAuthHeaders().then((headers) => {
cy.request({
method: "GET",
url: `${Cypress.env("server_host")}/api/app-environments`,
headers: headers,
}).then((response) => {
expect(response.status).to.equal(200);
return response.body.environments;
});
});
});
Cypress.Commands.add("apiMakeAppPublic", (appId = Cypress.env("appId")) => {
cy.getAuthHeaders().then((headers) => {
cy.request({
@ -495,30 +435,17 @@ Cypress.Commands.add("apiGetDataSourceIdByName", (dataSourceName) => {
headers: headers,
}).then((response) => {
expect(response.status).to.equal(200);
const dataSource = response.body.data_sources.find(
(ds) => ds.name === dataSourceName
);
return dataSource.id;
const id = response.body.data_sources.find(ds => ds.name === dataSourceName)?.id;
Cypress.log({
name: "apiGetDataSourceIdByName",
displayName: "Data Source ID",
message: `Data source ID for '${dataSourceName}': ${id}`,
});
return id;
});
});
});
Cypress.Commands.add("getAuthHeaders", (returnCached = false) => {
let headers = {};
if (returnCached) {
return returnCached;
} else {
cy.getCookie("tj_auth_token").then((cookie) => {
headers = {
"Tj-Workspace-Id": Cypress.env("workspaceId"),
Cookie: `tj_auth_token=${cookie.value}`,
};
Cypress.env("authHeaders", headers);
return headers;
});
}
});
Cypress.Commands.add(
"apiUpdateDataSource",
(dataSourceName, envName, updateData) => {

View file

@ -17,7 +17,7 @@ const API_ENDPOINT =
Cypress.Commands.add(
"appUILogin",
(email = "dev@tooljet.io", password = "password") => {
(email = "dev@tooljet.io", password = "password", status = 'success', toast = '') => {
cy.clearAndType(onboardingSelectors.loginEmailInput, email);
cy.clearAndType(onboardingSelectors.loginPasswordInput, password);
cy.get(onboardingSelectors.signInButton).click();
@ -27,7 +27,7 @@ Cypress.Commands.add(
);
Cypress.Commands.add("clearAndType", (selector, text) => {
cy.get(selector).type(`{selectall}{backspace}${text}`);
cy.get(selector).should("be.visible", { timeout: 10000 }).click({ force: true }).type(`{selectall}{backspace}${text}`);
});
Cypress.Commands.add("forceClickOnCanvas", () => {
@ -53,9 +53,10 @@ Cypress.Commands.add("waitForAutoSave", () => {
cy.wait(200);
cy.get(commonSelectors.autoSave, { timeout: 20000 }).should(
"have.text",
commonText.autoSave,
'',
{ timeout: 20000 }
);
).find('svg')
.should('be.visible', { timeout: 20000 });
});
Cypress.Commands.add("createApp", (appName) => {
@ -65,7 +66,7 @@ Cypress.Commands.add("createApp", (appName) => {
: commonSelectors.appCreateButton;
cy.get("body").then(($title) => {
cy.get(getAppButtonSelector($title)).click();
cy.get(getAppButtonSelector($title)).scrollIntoView().click({ force: true });//workaround for cypress dashboard click issue
cy.clearAndType('[data-cy="app-name-input"]', appName);
cy.get('[data-cy="create-app"]').click();
});
@ -77,37 +78,47 @@ Cypress.Commands.add(
"dragAndDropWidget",
(
widgetName,
positionX = 80,
positionY = 80,
positionX = 100,
positionY = 100,
widgetName2 = widgetName,
canvas = commonSelectors.canvas
) => {
const dataTransfer = new DataTransfer();
cy.forceClickOnCanvas();
cy.get("body")
.then(($body) => {
const isSearchVisible = $body
.find(commonSelectors.searchField)
.is(":visible");
cy.get('[data-cy="right-sidebar-plus-button"]').click();
cy.get(commonSelectors.searchField).should('be.visible');
if (!isSearchVisible) {
cy.get('[data-cy="right-sidebar-plus-button"]').click();
}
cy.get(commonSelectors.searchField).first().clear().type(widgetName);
cy.get(commonWidgetSelector.widgetBox(widgetName2)).should('be.visible');
cy.get(commonWidgetSelector.widgetBox(widgetName2))
.trigger('mousedown', { which: 1, button: 0, force: true })
.trigger('dragstart', { dataTransfer, force: true });
cy.get(canvas)
.trigger('dragenter', {
dataTransfer,
clientX: positionX,
clientY: positionY,
force: true
})
.then(() => {
cy.clearAndType(commonSelectors.searchField, widgetName);
.trigger('dragover', {
dataTransfer,
clientX: positionX,
clientY: positionY,
force: true
});
cy.get(commonWidgetSelector.widgetBox(widgetName2)).trigger(
"dragstart",
{ dataTransfer },
{ force: true }
);
cy.get(canvas).trigger("drop", positionX, positionY, {
dataTransfer,
force: true,
});
cy.get(canvas)
.trigger('drop', {
dataTransfer,
clientX: positionX,
clientY: positionY,
force: true
})
.trigger('mouseup', { force: true });
cy.waitForAutoSave();
}
);
@ -224,8 +235,11 @@ Cypress.Commands.add("renameApp", (appName) => {
`{selectAll}{backspace}${appName}`,
{ force: true }
);
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.get(commonSelectors.renameAppButton).should("be.enabled").click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonText.appRenamedToast
);
});
Cypress.Commands.add(
@ -410,7 +424,7 @@ Cypress.Commands.add("getPosition", (componentName) => {
);
});
Cypress.Commands.add("defaultWorkspaceLogin", () => {
Cypress.Commands.add("defaultWorkspaceLogin", (workspaceSlug = 'my-workspace',) => {
// cy.task("dbConnection", {
// dbconfig: Cypress.env("app_db"),
// sql: `
@ -421,10 +435,9 @@ Cypress.Commands.add("defaultWorkspaceLogin", () => {
cy.apiLogin(
"dev@tooljet.io",
"password",
// workspaceId,
// "/my-workspace"
).then(() => {
cy.visit("/");
cy.visit(`/${workspaceSlug}`);
cy.wait(2000);
cy.get(commonSelectors.homePageLogo, { timeout: 10000 });
});
@ -557,7 +570,7 @@ Cypress.Commands.add("installMarketplacePlugin", (pluginName) => {
}
});
function installPlugin (pluginName) {
function installPlugin(pluginName) {
cy.get('[data-cy="-list-item"]').eq(1).click();
cy.wait(1000);
@ -659,4 +672,27 @@ Cypress.Commands.add("runSqlQuery", (query, db = Cypress.env("app_db")) => {
dbconfig: db,
sql: query,
});
});
});
Cypress.Commands.add(
"openWorkflow",
(
slug = "",
workspaceId = Cypress.env("workspaceId"),
workflowId = Cypress.env("workflowId"),
) => {
cy.intercept("GET", "/api/apps/*").as("getWorkflowData");
cy.window({ log: false }).then((win) => {
win.localStorage.setItem("walkthroughCompleted", "true");
});
cy.visit(`/${workspaceId}/apps/${workflowId}/${slug}`);
cy.wait("@getWorkflowData").then((interception) => {
const responseData = interception.response.body;
Cypress.env("editingVersionId", responseData.editing_version.id);
Cypress.env("environmentId", responseData.editorEnvironment.id);
Cypress.env("workflowId", responseData.id);
});
}
);

View file

@ -1,5 +1,66 @@
const envVar = Cypress.env("environment");
Cypress.Commands.add(
"apiLogin",
(
userEmail = "dev@tooljet.io",
userPassword = "password",
workspaceId = "",
redirection = "/"
) => {
cy.request({
url: `${Cypress.env("server_host")}/api/authenticate/${workspaceId}`,
method: "POST",
body: {
email: userEmail,
password: userPassword,
redirectTo: redirection,
},
})
.its("body")
.then((res) => {
Cypress.env("workspaceId", res.current_organization_id);
Cypress.log({
name: "Api login",
displayName: "LOGIN: ",
message: `: Success`,
});
});
}
);
Cypress.Commands.add("apiLogout", () => {
cy.getCookie("tj_auth_token").then((cookie) => {
cy.request(
{
method: "GET",
url: `${Cypress.env("server_host")}/api/session/logout`,
headers: {
"Tj-Workspace-Id": Cypress.env("workspaceId"),
Cookie: `tj_auth_token=${cookie.value}`,
},
},
{ log: false }
).then((response) => {
expect(response.status).to.equal(200);
});
});
});
Cypress.Commands.add("apiGetEnvironments", () => {
cy.getAuthHeaders().then((headers) => {
cy.request({
method: "GET",
url: `${Cypress.env("server_host")}/api/app-environments`,
headers: headers,
}).then((response) => {
expect(response.status).to.equal(200);
return response.body.environments;
});
});
});
Cypress.Commands.add("apiCreateWorkspace", (workspaceName, workspaceSlug) => {
cy.getCookie("tj_auth_token").then((cookie) => {
cy.request(
@ -100,12 +161,61 @@ Cypress.Commands.add("apiUpdateWsConstant", (id, updateValue, envName) => {
url: `${Cypress.env("server_host")}/api/organization-constants/${id}`,
headers: headers,
body: {
value: String(updateValue), // ensure it's a string
value: String(updateValue),
environment_id: envId,
},
}).then((response) => {
expect(response.status).to.equal(200);
response.body; // returns the PATCH response body
response.body;
});
});
});
});
Cypress.Commands.add("apiGetGroupId", (groupName) => {
return cy.getAuthHeaders().then((headers) => {
return cy
.request({
method: "GET",
url: `${Cypress.env("server_host")}/api/v2/group-permissions`,
headers: headers,
log: false,
})
.then((response) => {
expect(response.status).to.equal(200);
const group = response.body.groupPermissions.find(
(g) => g.name === groupName
);
if (!group) throw new Error(`Group with name ${groupName} not found`);
return group.id;
});
});
});
Cypress.Commands.add("apiUpdateUserRole", (email, role) => {
return cy.task("dbConnection", {
dbconfig: Cypress.env("app_db"),
sql: `
SELECT id
FROM users
WHERE email='${email}'
LIMIT 1;
`,
}).then((resp) => {
const userId = resp.rows[0]?.id;
if (!userId) throw new Error(`User with email ${email} not found`);
return userId;
}).then((userId) => {
return cy.getAuthHeaders().then((headers) => {
return cy.request({
method: "PUT",
url: `${Cypress.env("server_host")}/api/v2/group-permissions/role/user`,
headers: headers,
body: {
newRole: role,
userId: userId,
},
}).then((response) => {
expect(response.status).to.equal(200);
});
});
});
@ -116,47 +226,71 @@ Cypress.Commands.add(
(
groupName,
name,
canEdit = false,
canView = true,
hideFromDashboard = false,
resourceType = "app",
permissions = {},
resourcesToAdd = []
) => {
cy.getAuthHeaders().then((headers) => {
// Fetch group permissions
cy.request({
method: "GET",
url: `${Cypress.env("server_host")}/api/v2/group-permissions`,
headers: headers,
log: false,
}).then((response) => {
expect(response.status).to.equal(200);
const group = response.body.groupPermissions.find(
(g) => g.name === groupName
);
if (!group) throw new Error(`Group with name ${groupName} not found`);
cy.apiGetGroupId(groupName).then((groupId) => {
const environment = Cypress.env("environment");
const isEnterprise = environment === "Enterprise";
const groupId = group.id;
const resourceTypeMap = {
app: { type: "app", endpoint: "app" },
workflow: { type: "workflow", endpoint: "data-source" },
datasource: { type: "data_source", endpoint: "data-source" },
};
// Create granular permission
cy.request({
method: "POST",
url: `${Cypress.env("server_host")}/api/v2/group-permissions/granular-permissions`,
headers: headers,
body: {
const { type, endpoint } =
resourceTypeMap[resourceType] || resourceTypeMap.app;
const url = isEnterprise
? `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}/granular-permissions/${endpoint}`
: `${Cypress.env("server_host")}/api/v2/group-permissions/granular-permissions`;
let permissionObject;
if (resourceType === "datasource") {
permissionObject = {
action: {
canUse: permissions.canUse ?? true,
canConfigure: permissions.canConfigure ?? false,
},
resourcesToAdd,
};
} else {
permissionObject = {
canEdit: permissions.canEdit ?? false,
canView: permissions.canView ?? true,
hideFromDashboard: permissions.hideFromDashboard ?? false,
resourcesToAdd,
};
}
const body = isEnterprise
? {
name,
type,
groupId,
isAll: true,
createResourcePermissionObject: permissionObject,
}
: {
name,
type: "app",
groupId,
isAll: true,
createAppsPermissionsObject: {
canEdit,
canView,
hideFromDashboard,
resourcesToAdd,
},
},
createAppsPermissionsObject: permissionObject,
};
cy.request({
method: "POST",
url: url,
headers: headers,
body: body,
log: false,
}).then((res) => {
expect(res.status).to.equal(201);
cy.log(`Created ${resourceType} granular permission: ${name}`);
});
});
});
@ -167,22 +301,7 @@ Cypress.Commands.add(
"apiDeleteGranularPermission",
(groupName, typesToDelete = []) => {
cy.getAuthHeaders().then((headers) => {
// Step 1: Get the group by name
cy.request({
method: "GET",
url: `${Cypress.env("server_host")}/api/v2/group-permissions`,
headers,
log: false,
}).then((response) => {
expect(response.status).to.equal(200);
const group = response.body.groupPermissions.find(
(g) => g.name === groupName
);
if (!group) throw new Error(`Group with name ${groupName} not found`);
const groupId = group.id;
// Step 2: Get all granular permissions for the group
cy.apiGetGroupId(groupName).then((groupId) => {
cy.request({
method: "GET",
url: `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}/granular-permissions`,
@ -192,23 +311,30 @@ Cypress.Commands.add(
expect(granularResponse.status).to.equal(200);
const granularPermissions = granularResponse.body;
// Step 3: Filter if typesToDelete is specified
const permissionsToDelete = typesToDelete.length
? granularPermissions.filter((perm) =>
typesToDelete.includes(perm.type)
)
: granularPermissions;
// Step 4: Delete each granular permission
permissionsToDelete.forEach((permission) => {
const typeEndpointMap = {
app: "app",
workflow: "app",
data_source: "data-source",
};
const endpoint = typeEndpointMap[permission.type] || "app";
cy.request({
method: "DELETE",
url: `${Cypress.env("server_host")}/api/v2/group-permissions/granular-permissions/app/${permission.id}`,
url: `${Cypress.env("server_host")}/api/v2/group-permissions/granular-permissions/${endpoint}/${permission.id}`,
headers,
log: false,
}).then((deleteResponse) => {
expect(deleteResponse.status).to.equal(200);
cy.log(`Deleted granular permission: ${permission.name}`);
cy.log(
`Deleted ${permission.type} granular permission: ${permission.name}`
);
});
});
});
@ -310,14 +436,13 @@ Cypress.Commands.add(
redirectTo = "/",
}) => {
cy.intercept("POST", "/api/oauth/sign-in/*", (req) => {
// Inject missing params if not present
if (!req.body.organizationId) {
req.body.organizationId = organizationId;
}
if (!req.body.redirectTo) {
req.body.redirectTo = redirectTo;
}
req.continue(); // Send the modified request
req.continue();
}).as("oidcSignIn");
cy.getSsoConfigId("openid").then((ssoConfigId) => {
const configIdToUse = ssoConfigId;
@ -375,7 +500,6 @@ Cypress.Commands.add(
const redirectUrl = authResp.headers["location"];
const params = new URL(redirectUrl).searchParams;
const code = params.get("code");
// 6. Exchange code for tokens
cy.request({
method: "POST",
url: `https://${oktaDomain}/oauth2/v1/token`,
@ -567,3 +691,45 @@ Cypress.Commands.add(
});
}
);
Cypress.Commands.add(
"apiUpdateGroupPermission",
(groupName, permissionPayload) => {
return cy.apiGetGroupId(groupName).then((groupId) => {
return cy.getAuthHeaders().then((headers) => {
return cy
.request({
method: "PUT",
url: `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}`,
headers: headers,
body: permissionPayload,
log: false,
})
.then((response) => {
expect(response.status).to.equal(200);
return response.body;
});
});
});
}
);
Cypress.Commands.add("getAuthHeaders", (returnCached = false) => {
let headers = {};
if (returnCached) {
return returnCached;
} else {
cy.getCookie("tj_auth_token").then((cookie) => {
headers = {
"Tj-Workspace-Id": Cypress.env("workspaceId"),
Cookie: `tj_auth_token=${cookie.value}`,
};
Cypress.env("authHeaders", headers);
Cypress.log({
name: "getAuthHeaders",
message: `Auth headers: ${JSON.stringify(headers)}`,
});
return headers;
});
}
});

View file

@ -6,10 +6,10 @@ import { commonText } from "Texts/common";
import { selectAppCardOption } from "Support/utils/common";
import { navigateBackToWorkflowsDashboard } from "Support/utils/workFlows";
Cypress.Commands.add("createWorkflowApp", (wfName) => {
Cypress.Commands.add("createWorkflowApp", (workflowName) => {
cy.get(workflowSelector.globalWorkFlowsIcon).click();
cy.get(workflowSelector.workflowsCreateButton).click();
cy.get(workflowSelector.workFlowNameInputField).type(wfName);
cy.get(workflowSelector.workFlowNameInputField).type(workflowName);
cy.get(workflowSelector.createWorkFlowsButton).click();
});
@ -115,10 +115,10 @@ Cypress.Commands.add(
}
);
Cypress.Commands.add("deleteWorkflow", (wfName) => {
Cypress.Commands.add("deleteWorkflow", (workflowName) => {
cy.intercept("DELETE", "/api/apps/*").as("appDeleted");
navigateBackToWorkflowsDashboard();
cy.get(commonSelectors.appCard(wfName))
cy.get(commonSelectors.appCard(workflowName))
.realHover()
.find(commonSelectors.appCardOptionsButton)
.realHover()
@ -128,9 +128,9 @@ Cypress.Commands.add("deleteWorkflow", (wfName) => {
cy.wait("@appDeleted");
});
Cypress.Commands.add("deleteWorkflowfromDashboard", (wfName) => {
Cypress.Commands.add("deleteWorkflowfromDashboard", (workflowName) => {
cy.intercept("DELETE", "/api/apps/*").as("appDeleted");
cy.get(commonSelectors.appCard(wfName))
cy.get(commonSelectors.appCard(workflowName))
.realHover()
.find(commonSelectors.appCardOptionsButton)
.realHover()
@ -142,13 +142,17 @@ Cypress.Commands.add("deleteWorkflowfromDashboard", (wfName) => {
Cypress.Commands.add(
"exportWorkflowApp",
(wfName, fixtureFile = "cypress/fixtures/exportedApp.json") => {
(workflowName, fixtureFile = "cypress/fixtures/exportedApp.json") => {
navigateBackToWorkflowsDashboard();
selectAppCardOption(
wfName,
commonSelectors.appCardOptions(workflowsText.exportWFOption)
);
cy.get(`[data-cy="${workflowName}-card"]`)
.trigger('mouseover')
.find('[data-cy="app-card-menu-icon"]')
.click({ force: true });
cy.get(commonSelectors.appCardOptions(workflowsText.exportWFOption))
.click();
cy.wait(2000);
cy.exec("ls -t ./cypress/downloads/ | head -1").then((result) => {
@ -158,20 +162,21 @@ Cypress.Commands.add(
cy.writeFile(fixtureFile, json);
});
});
cy.deleteWorkflowfromDashboard(wfName);
cy.deleteWorkflowfromDashboard(workflowName);
}
);
Cypress.Commands.add("addWorkflowInApp", (wfName) => {
Cypress.Commands.add("addWorkflowInApp", (workflowName) => {
cy.get(workflowSelector.showDSPopoverButton).click();
cy.get(workflowSelector.workflowSearchInput).type(
workflowsText.workflowLabel
);
cy.contains(`[id*="react-select-"]`, workflowsText.workflowLabel).click();
cy.get(workflowSelector.queryRenameInput).clear().type(wfName);
cy.get(workflowSelector.queryRenameInput).clear().type(workflowName);
cy.get(workflowSelector.workflowDropdown).parent()
.find('.react-select__control')
.click();
cy.get(workflowSelector.workflowSelectInput).realType(wfName);
cy.get(workflowSelector.workflowSelectOption).contains(wfName).click();
cy.get(workflowSelector.workflowSelectInput).realType(workflowName);
cy.get(workflowSelector.workflowSelectOption).contains(workflowName).click();
});

View file

@ -0,0 +1,102 @@
const envVar = Cypress.env("environment");
Cypress.Commands.add("apiCreateWorkflow", (workflowName, reuseSession = false) => {
cy.getAuthHeaders(reuseSession).then((headers) => {
cy.request({
method: "POST",
url: `${Cypress.env("server_host")}/api/apps`,
headers: headers,
body: {
icon: "sentfast",
name: workflowName,
type: "workflow",
},
}).then((response) => {
expect(response.status).to.equal(201);
const workflowId = response.body?.id || response.allRequestResponses?.[0]?.["Response Body"]?.id;
const userId = response.body?.user_id || response.allRequestResponses?.[0]?.["Response Body"]?.user_id;
Cypress.env("workflowId", workflowId);
Cypress.env("user_id", userId);
Cypress.log({
name: "Workflow create",
displayName: "WORKFLOW CREATED",
message: `: ${response.body.name}`,
});
});
});
});
Cypress.Commands.add(
"openWorkflowByName",
(
workflowName,
workspaceId = Cypress.env("workspaceId"),
componentSelector = "[data-cy='workflow-canvas']",
reuseSession = false
) => {
cy.getAuthHeaders(reuseSession).then((headers) => {
cy.request({
method: "GET",
url: `${Cypress.env("server_host")}/api/apps?page=1&type=workflow&searchKey=${workflowName}`,
headers: headers,
}, { log: false }).then((response) => {
const workflow = response.body.apps?.find(
app => app.name === workflowName || app.slug === workflowName
);
if (workflow) {
Cypress.env("workflowId", workflow.id);
cy.openWorkflow(workflow.slug, workspaceId, workflow.id, componentSelector);
Cypress.log({
name: "Workflow Open",
displayName: "WORKFLOW OPENED",
message: `: ${workflowName}`,
});
} else {
throw new Error(`Workflow "${workflowName}" not found`);
}
});
});
}
);
Cypress.Commands.add("apiDeleteWorkflow", (workflowName, reuseSession = false) => {
cy.getAuthHeaders(reuseSession).then((headers) => {
cy.request({
method: "GET",
url: `${Cypress.env("server_host")}/api/apps?page=1&type=workflow&searchKey=${workflowName}`,
headers: headers,
}, { log: false }).then((response) => {
const workflow = response.body.apps?.find(
app => app.name === workflowName || app.slug === workflowName
);
if (workflow) {
cy.request({
method: "DELETE",
url: `${Cypress.env("server_host")}/api/apps/${workflow.id}`,
headers: headers,
}, { log: false }).then((deleteResponse) => {
expect(deleteResponse.status).to.equal(200);
Cypress.log({
name: "Workflow Delete",
displayName: "WORKFLOW DELETED",
message: `: ${workflowName}`,
});
});
} else {
Cypress.log({
name: "Workflow Not Found",
displayName: "WORKFLOW NOT FOUND",
message: `: ${workflowName}`,
});
}
});
});
});

View file

@ -89,6 +89,7 @@ export const commonSelectors = {
invitedUserEmail: '[data-cy="email-input-value"]',
invitedUseremail: '[data-cy="email-input-input-value"]',
acceptInviteButton: '[data-cy="accept-invite-button"]',
homePageIcon: '[data-cy="icon-home"]',
databaseIcon: '[data-cy="icon-database"]',
profileSettings: '[data-cy="profile-settings"]',
workspaceSettings: '[data-cy="workspace-settings"]',
@ -176,7 +177,9 @@ export const commonSelectors = {
resetPasswordButton: '[data-cy="reset-password-button"]',
resetPasswordPageDescription: '[data-cy="reset-password-page-description"]',
backToLoginButton: '[data-cy="back-to-login"]',
breadcrumbTitle: '[data-cy="app-header-label"]>>',
// breadcrumbTitle: '[data-cy="app-header-label"]>>',
breadcrumbHeaderTitle: '[data-cy="breadcrumb-header-applications"]>>',
breadcrumbTitle: '[data-cy="breadcrumb-header-workspace-settings"]>>',
// breadcrumbPageTitle: '[data-cy="app-header-label"]',
breadcrumbPageTitle: '[data-cy="breadcrumb-page-title"]',
labelFullNameInput: '[data-cy="name-label"]',
@ -190,6 +193,7 @@ export const commonSelectors = {
addNewDataSourceButton: '[data-cy="add-new-data-source-button"]',
saveButton: '[data-cy="save-button"]',
appEditButton: '[data-cy="edit-button"]',
editorAppNameInput: '[data-cy="editor-app-name-input"]',
onboardingRadioButton: (radioButtonText) => {
return `[data-cy="${cyParamName(radioButtonText)}-radio-button"]`;
},
@ -268,6 +272,7 @@ export const commonSelectors = {
chooseFromTemplateButton: '[data-cy="choose-from-template-button"]',
CreateAppFromTemplateButton: '[data-cy="create-new-app-from-template-title"]',
settingsIcon: '[data-cy="settings-icon"]',
previewSettings: '[data-cy="preview-settings"]',
marketplaceOption: '[data-cy="marketplace-option"]',
backToAppOption: '[data-cy="back-to-app-option"]',
databaseOption: '[data-cy="database-option"]',
@ -281,6 +286,8 @@ export const commonSelectors = {
defaultModalTitle: '[data-cy="modal-title"]',
workspaceConstantsIcon: '[data-cy="icon-workspace-constants"]',
confirmationButton: '[data-cy="confirmation-button"]',
modalConfirmButton: '[data-cy="modal-confirm-button"]',
rightSidebarPlusButton: '[data-cy="right-sidebar-plus-button"]',
textField: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-text-field"]`;

View file

@ -55,4 +55,24 @@ export const dashboardSelector = {
slugSuccessLabel: '[data-cy="slug-sucess-label"]',
slugErrorLabel: '[data-cy="slug-error-label"]',
editWorkspaceTitle: '[data-cy="edit-workspace-title"]',
homePagePromptHeader: '[data-cy="home-page-prompt-header"]',
promptInput: '[data-cy="prompt-input"]',
homePageDividerText: '[data-cy="divider-text"]',
appCardWidget: '[data-cy="getstarted-app-widget"]',
templateCardWidget: '[data-cy="getstarted-templates-widget"]',
databaseCardWidget: '[data-cy="getstarted-datasource-widget"]',
workflowCardWidget: '[data-cy="getstarted-workflow-widget"]',
widgetCardName: (cardType) => {
return `[data-cy="getstarted-${cardType}-widget"]`;
},
widgetCardTitle: '[data-cy="widget-card-title"]',
widgetCardDescription: '[data-cy="widget-card-description"]',
homePagePromptTextArea: '[data-cy="prompt-textarea"]',
promptEnterButton: '[data-cy="prompt-enter-button"]',
aiIcon: '[data-cy="ai-icon"]',
homePageIcon: (iconName) => {
return `[data-cy="${iconName}s-icon"]`;
}
};

View file

@ -14,7 +14,7 @@ export const appVersionSelectors = {
versionNameInputField: '[data-cy="version-name-input-field"]',
createVersionFromLabel: '[data-cy="create-version-from-label"]',
createVersionInputField: '[data-cy="create-version-from-input-field"]',
createNewVersionButton: '[data-cy="create-new-version-button"]',
createNewVersionButton: '[data-cy="create-new-version-button"]:last',
appVersionContentList: ".react-select__menu-list",
};
export const exportAppModalSelectors = {

View file

@ -0,0 +1,82 @@
export const cyParamName = (paramName = "") => {
return String(paramName).toLowerCase().replace(/\s+/g, "-");
};
export const cyParamNameCamelCase = (paramName = "") => {
return paramName
.replace(/[^a-zA-Z0-9\s]/g, "")
.toLowerCase()
.replace(/\s+(\w)/g, (_, c) => c.toUpperCase());
};
export const licenseSelectors = {
comparePlansText: '[data-cy="compare-plans-button"]',
listOfItems: (itemName) => {
return `[data-cy="${cyParamName(itemName)}-list-item"]`;
},
tabTitle: (tabName) => {
return `[data-cy="${cyParamNameCamelCase(tabName)}-tab-title"]`;
},
subTab: (subTabName) => {
return `[data-cy="${cyParamName(subTabName)}-sub-tab"]`;
},
numberOfTextLabel: (label) => {
return `[data-cy="number-of-${cyParamName(label)}-label"]`;
},
inputField: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-field"]`;
},
label: (labelName) => {
return `[data-cy="${cyParamName(labelName)}-label"]`;
},
limitInfo: (type) => {
return `[data-cy="${cyParamName(type)}-limit-info"]`;
},
limitHeading: (type) => {
return `[data-cy="${cyParamName(type)}-limit-heading"]`;
},
circularToggleDisabledIcon: '[data-cy="circular-toggle-disabled-icon"]',
licenseBannerHeading: '[data-cy="license-banner-heading"]',
licenseBannerInfo: '[data-cy="license-banner-info"]',
paidFeatureButton: '[data-cy="paid-feature-button"]',
warningIcon: '[data-cy="warning-icon"]',
noDomainLinkedLabel: '[data-cy="no-domain-header"]',
noDomainInfoText: '[data-cy="no-domain-info-text"]',
licenseOption: '[data-cy="license-list-item"]',
licenseKeyOption: '[data-cy="license-key-list-item"]',
limitOption: '[data-cy="limits-list-item"]',
accessOption: '[data-cy="access-list-item"]',
domainOption: '[data-cy="domain-list-item"]',
licenseKeyTitle: '[data-cy="licenseKey-tab-title"]',
licenseLabel: '[data-cy="license-label"]',
updateButton: '[data-cy="update-button"]',
limitsTabTitle: '[data-cy="limits-tab-title"]',
appsTab: '[data-cy="apps-sub-tab"]',
noOfAppsLabel: '[data-cy="number-of-apps-label"]',
noOfAppsfield: '[data-cy="apps-field"]',
workspaceTab: '[data-cy="workspaces-sub-tab"]',
noOfworkspaceLabel: '[data-cy="number-of-workspaces-label"]',
noOfWorkspacefield: '[data-cy="workspaces-field"]',
usersTab: '[data-cy="users-sub-tab"]',
noOfTotalUsersLabel: '[data-cy="number-of-total-users-label"]',
noOfTotalUsersfield: '[data-cy="total-users-field"]',
noOfBuildersLabel: '[data-cy="number-of-builders-label"]',
noOfBuildersfield: '[data-cy="builders-field"]',
noOfEndUsersLabel: '[data-cy="number-of-end-users-label"]',
noOfEndUsersfield: '[data-cy="end-users-field"]',
noOfSuperAdminLabel: '[data-cy="number-of-super-admins-label"]',
noOfSuperAdminfield: '[data-cy="super-admins-field"]',
tablesTab: '[data-cy="tables-sub-tab"]',
noOfTablesLabel: '[data-cy="number-of-tables-label"]',
noOfTablesfield: '[data-cy="tables-field"]',
expiryStatus: '[data-cy="license-expiry-status"]',
licenseTextArea: '[data-cy="license-text-area"]',
paidFeatureButton: '[data-cy="paid-feature-button"]',
accessTabTitle: '[data-cy="access-tab-title"]',
enterpriseGradientIcon: '[data-cy="enterprise-gradient-icon"]',
warningInfoText: '[data-cy="warning-info-text"]',
lockGradientIcon: '[data-cy="lock-gradient"]',
dsGradientIcon: '[data-cy="datasource-gradient"]',
};

View file

@ -4,6 +4,7 @@ export const cyParamName = (paramName = "") => {
export const groupsSelector = {
pageTitle: "[data-cy=user-groups-title]",
createNewGroupButton: "[data-cy=create-new-group-button]",
createGroupButton: "[data-cy=create-group-button]",
tableHeader: "[data-cy=table-header]",
groupName: "[data-cy=group-name]",
addNewGroupModalTitle: '[data-cy="add-new-group-title"]',
@ -41,7 +42,7 @@ export const groupsSelector = {
allAppsRadio: '[data-cy="all-apps-radio"]',
allAppsLabel: '[data-cy="all-apps-label"]',
allAppsHelperText: '[data-cy="this-will-select-all-apps-in-the-workspace-including-any-new-apps-created-info-text"]',
customradio: '[data-cy="custom-radio"]',
customRadio: '[data-cy="custom-radio"]',
customLabel: '[data-cy="custom-label"]',
customHelperText: '[data-cy="custom-info-text"]',
resourcesFolders: "[data-cy=resource-folders]",
@ -50,10 +51,29 @@ export const groupsSelector = {
appsCreateLabel: "[data-cy=app-create-label]",
appsDeleteCheck: "[data-cy=app-delete-checkbox]",
appsDeleteLabel: "[data-cy=app-delete-label]",
appPromoteCheck: '[data-cy="app-promote-checkbox"]',
appPromoteLabel: '[data-cy="app-promote-label"]',
appPromoteHelperText: '[data-cy="app-promote-helper-text"]',
appReleaseCheck: '[data-cy="app-release-checkbox"]',
appReleaseLabel: '[data-cy="app-release-label"]',
appReleaseHelperText: '[data-cy="app-release-helper-text"]',
workflowsCreateCheck: "[data-cy=workflow-create-checkbox]",
workflowsCreateLabel: "[data-cy=workflow-create-label]",
workflowsCreateHelperText: '[data-cy="workflow-create-helper-text"]',
workflowsDeleteCheck: '[data-cy="workflow-delete-checkbox"]',
workflowsDeleteLabel: '[data-cy="workflow-delete-label"]',
workflowsDeleteHelperText: '[data-cy="workflow-delete-helper-text"]',
datasourcesCreateCheck: "[data-cy=datasource-create-checkbox]",
datasourcesCreateLabel: "[data-cy=datasource-create-label]",
datasourcesCreateHelperText: '[data-cy="datasource-create-helper-text"]',
datasourcesDeleteCheck: '[data-cy="datasource-delete-checkbox"]',
datasourcesDeleteLabel: '[data-cy="datasource-delete-label"]',
datasourcesDeleteHelperText: '[data-cy="datasource-delete-helper-text"]',
foldersCreateCheck: "[data-cy=folder-create-checkbox]",
foldersCreateLabel: "[data-cy=folder-create-label]",
foldersHelperText: '[data-cy="folder-helper-text"]',
workspaceVarCheckbox: '[data-cy="env-variable-checkbox"]',
appsText: '[data-cy="apps-text"]',
appEditRadio: '[data-cy="app-edit-radio"]',
appEditLabel: '[data-cy="app-edit-label"]',
@ -64,9 +84,26 @@ export const groupsSelector = {
appHideCheckbox: '[data-cy="app-hide-from-dashboard-radio"]',
appHideHelperText: '[data-cy="app-hide-from-dashboard-helper-text"]',
appHideLabel: '[data-cy="app-hide-from-dashboard-label"]',
workflowsText: '[data-cy="workflows-text"]',
workflowsBuildRadio: '[data-cy="workflow-build-radio"]',
workflowsBuildLabel: '[data-cy="workflow-build-label"]',
workflowsBuildHelperText: '[data-cy="workflow-build-helper-text"]',
workflowsExecuteRadio: '[data-cy="workflow-execute-radio"]',
workflowsExecuteLabel: '[data-cy="workflow-execute-label"]',
workflowsExecuteHelperText: '[data-cy="workflow-execute-helper-text"]',
datasourcesText: '[data-cy="datasources-text"]',
datasourcesConfigureRadio: '[data-cy="datasource-configure-radio"]',
datasourcesConfigureLabel: '[data-cy="datasource-configure-label"]',
datasourcesConfigureHelperText: '[data-cy="datasource-configure-helper-text"]',
datasourcesBuildWithRadio: '[data-cy="datasource-build-with-radio"]',
datasourcesBuildWithLabel: '[data-cy="datasource-build-with-label"]',
datasourcesBuildWithHelperText: '[data-cy="datasource-build-with-helper-text"]',
appHidePermissionModalLabel: '[data-cy="hide-from-dashboard-permission-label"]',
appHidePermissionModalHelperText: '[data-cy="hide-from-dashboard-permission-info-text"]',
addAppButton: '[data-cy="add-apps-buton"]',
addAppsButton: '[data-cy="add-apps-buton"]',
addEditPermissionModal: '[data-cy = "add-edit-permission-modal"]',
addEditPermissionModalTitle: '[data-cy="modal-title"]',
permissionNameLabel: '[data-cy="permission-name-label"]',
@ -103,8 +140,8 @@ export const groupsSelector = {
helperTextAdminPermissions: '[data-cy="helper-text-admin-permissions"]',
updateGroupNameModalTitle: '[data-cy="update-group-title"]',
editGranularPermissionIcon: '[data-cy="edit-permission-button"]',
granularAccessPermission: '[data-cy="granular-access-permission"]',
groupChip: '[data-cy="group-chip"]',
granularAccessPermission: '[data-cy="apps-granular-access"]',
groupChip: (accessType) => `[data-cy="${cyParamName(accessType)}-group-chip"]`,
resourceContainer: '[data-cy="resources-container"]',
groupLink: (groupname) => {
return `[data-cy="${cyParamName(groupname)}-list-item"]`;
@ -150,7 +187,13 @@ export const groupsSelector = {
granularEmptyPageIcon: '[data-cy="empty-page-svg"]',
emptyPagePermissionHelperText: '[data-cy="empty-page-info-text"]',
groupNameUpdateLink: '[data-cy="group-name-update-link"]',
addPermissionButton: '[data-cy="add-permission-button"]',
addAppButton: '[data-cy="add-app-button"]',
addWorkflowButton: '[data-cy="add-workflow-button"]',
addDatasourceButton: '[data-cy="add-data_source-button"]',
buildWorkflowradio: '[data-cy="build-permission-radio"]',
executeWorkflowradio: '[data-cy="execute-permission-radio"]',
configureDatasourceradio: '[data-cy="configure-permission-radio"]',
buildWithDatasourceRadio: '[data-cy="build-with-datasource-radio"]',
};

View file

@ -28,6 +28,7 @@ export const commonText = {
createFolder: "Create folder",
AddedToFolderToast: "Added to folder.",
appCreatedToast: "App created successfully!",
appRenamedToast: "App name has been updated!",
appRemovedFromFolderMessage:
"The app will be removed from this folder, do you want to continue?",
appRemovedFromFolderTaost: "Removed from folder.",
@ -135,6 +136,7 @@ export const commonText = {
breadcrumbGlobalDatasourceTitle: "Global datasources",
breadcrumbDatabaseTitle: "Databse",
breadcrumbApplications: "Applications",
breadcrumbHome: "Home",
breadcrumbSettings: "Settings",
addNewDataSourceButton: "Add new datasource",
@ -261,3 +263,24 @@ export const widgetValue = (widgetName) => {
export const customValidation = (name, message) => {
return ["{{", `components.${name}.value ? true : '${message}'}}`];
};
export const settingsText = {
settingsHeader: "Settings",
allUsersListItem: "All Users",
allWorkspacesListItem: "All workspaces",
manageInstanceSettingsListItem: "Manage instance settings",
whiteLabellingListItem: "White labelling",
instanceLoginListItem: "Instance login",
emailProtocolListItem: "Email protocol (SMTP)",
licenseListItem: "License",
};
export const workspaceSettingsText = {
WorkspaceSettingsHeader: "Workspace settings",
usersListItem: "Users",
groupsListItem: "Groups",
workspaceLoginListItem: "Workspace login",
customStylesListItem: "Custom styles",
configureGitSyncListItem: "Configure git sync",
themesListItem: "Themes",
};

View file

@ -49,4 +49,17 @@ export const dashboardText = {
folderName: (folderName) => {
return folderName;
},
homePageDividerText: "OR START WITH",
homePagePromptHeader: "What do you want to build today?",
appCardTitle: "Create a blank app",
appCardDescription: "Build custom apps that make internal processes efficient",
datasourceCardTitle: "Connect to a data source",
datasourceCardDescription:
"Link your tools to existing databases, spreadsheets, APIs, and more",
workflowCardTitle: "Create a workflow",
workflowCardDescription:
"Automate repetitive tasks to streamline business process",
exploreTemplateCardTitle: "Explore templates",
exploreTemplateCardDescription:
"Get started quickly with ready-to-deploy applications",
};

View file

@ -0,0 +1,68 @@
export const licenseText = {
comparePlansText: "Compare plans",
license: "License",
licenseOverviewTitle: "License overview",
licenseKeyLabel: "License Key",
limitsTabTitle: "Limits",
accessTabTitle: "Access",
domainTabTitle: "Domain",
updateButton: "Update",
licenseKeyTab: {
licenseKeyTabTitle: "License Key",
licenseLabel: "License",
enterLicenseKeyPlaceholder: "Enter license key",
},
limitsTab: {
aiCreditsSubTab: "AI credits",
appsSubTab: "Apps",
workspacesSubTab: "Workspaces",
usersSubTab: "Users",
workflowsSubTab: "Workflows",
tablesSubTab: "Tables",
},
aiCreditsSubTab: {
monthlyRecurringLabel: "Monthly recurring",
addOnCreditsLabel: "Add on credits",
},
appsSubTab: {
noOfAppsLabel: "Number of Apps",
},
workspacesSubTab: {
noOfWorkspacesLabel: "Number of Workspaces",
},
usersSubTab: {
noOfTotalUsersLabel: "Number of Total Users",
noOfBuildersLabel: "Number of Builders",
noOfEndUsersLabel: "Number of End Users",
noOfSuperAdminsLabel: "Number of Super Admins",
},
workflowsSubTab: {
noOfWorkflowsLabel: "Number of Workflows",
},
tablesSubTab: {
noOfTablesLabel: "Number of Tables",
},
accessTab: {
openIDConnectLabel: "Open ID Connect",
apiKeyLabel: "Audit Logs",
ldapLabel: "LDAP",
samlLabel: "SAML",
customStylesLabel: "Custom styles",
multiEnvironmentLabel: "Multi-Environment",
gitSyncLabel: "GitSync",
},
domainTab: {
noDomainLinkedLabel: "No Domain Linked",
noDomainInfoText: "Please contact ToolJet team to link your domain",
},
};

View file

@ -85,12 +85,29 @@ export const groupsText = {
warningText: "Users must be always be part of one default group. This will define the user count in your plan.",
continueButtonText: "Continue",
roleUpdateToastMessage: "Role updated successfully",
endUserToBuilderMessage: "Changing the user role from end-user to builder will grant access the user access to all resources.Are you sure you want to continue?",
endUserToAdminMessage: "Changing the user role from end-user to admin will grant the user access to all resources and settings.Are you sure you want to continue?",
builderToEnduserMessage: "Changing the user role from builder to end-user will revoke their access to edit all resources.Are you sure you want to continue?",
builderToAdminMessage: "Changing user role from builder to admin will grant access to all resources and settings.Are you sure you want to continue?",
endUserToBuilderMessage: Cypress.env('environment') === 'Community'
? "Changing the user role from end-user to builder will grant access the user access to all resources.Are you sure you want to continue?"
: "Changing user default group from end-user to builder will affect the count of users covered by your plan.Are you sure you want to continue?",
endUserToAdminMessage: Cypress.env('environment') === 'Community'
? "Changing the user role from end-user to admin will grant the user access to all resources and settings.Are you sure you want to continue?"
: "Changing user default group from end-user to admin will affect the count of users covered by your plan.Are you sure you want to continue?",
builderToEnduserMessage: Cypress.env('environment') === 'Community'
? "Changing the user role from builder to end-user will revoke their access to edit all resources.Are you sure you want to continue?"
: "Changing user default group from builder to end-user will affect the count of users covered by your plan.This will also remove the user from any custom groups with builder-like permissions.Are you sure you want to continue?",
builderToAdminMessage: Cypress.env('environment') === 'Community'
? "Changing user role from builder to admin will grant access to all resources.Are you sure you want to continue?"
: "Changing user role from builder to admin will grant access to all resources and settings.Are you sure you want to continue?",
adminToBuilderMessage: "Changing your user default group from admin to builder will revoke your access to settings.Are you sure you want to continue?",
adminToEnduserMessage: "Changing the user role from admin to end-user will revoke their access to edit all resources and settings.Are you sure you want to continue?",
adminToEnduserMessage: Cypress.env('environment') === 'Community'
? "Changing the user role from admin to end-user will revoke their access to edit all resources and settings.Are you sure you want to continue?"
: "Changing your user group from admin to end-user will revoke your access to settings. This will also affect the count of users covered by your plan.Are you sure you want to continue?",
modalHeader: "Can not remove last active admin",
modalMessage: "Cannot change role of last present admin, please add another admin and change the role",
userAddedToast: "Users added to the group",

View file

@ -33,8 +33,8 @@ FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE';`,
postgresResponseNodeQuery: "return postgresql1.data",
postgresExpectedValue: "employees",
postgresExpectedValue: "server_side_pagination",
restApiUrl: "http://9.234.17.31:8000/delay/10s",
restApiResponseNodeQuery: "return restapi1.data",
restApiExpectedValue: "<!DOCTYPE html>",
@ -47,7 +47,7 @@ AND table_type = 'BASE TABLE';`,
runjsCodeForWebhooks: 'return "Verifying webhooks response"',
runjsExpectedValueForWebhooks: "Verifying webhooks response",
expectedStatusCodeText: 201,
expectedStatusCodeText: 200,
exportFixturePath: "cypress/fixtures/exportedApp.json",
workflowLabel: "Workflow",
};

View file

@ -14,7 +14,7 @@ describe("Chaining of queries", () => {
cy.apiLogin();
cy.apiCreateApp(`${fake.companyName}-chaining-App`);
cy.openApp();
cy.apiFetchDataSourcesId();
cy.apiFetchDataSourcesIdFromApp();
cy.viewport(1800, 1800);
cy.dragAndDropWidget("Button");
resizeQueryPanel("80");

View file

@ -1,15 +1,17 @@
import { fake } from "Fixtures/fake";
import { commonSelectors } from "Selectors/common";
import { importSelectors } from "Selectors/exportImport";
import { commonText } from "Texts/common";
import { commonSelectors } from "Selectors/common";
import { importSelectors, exportAppModalSelectors } from "Selectors/exportImport";
import { commonText } from "Texts/common";
import { exportAppModalText } from "Texts/exportImport";
import {
clickOnExportButtonAndVerify,
exportAllVersionsAndVerify,
verifyElementsOfExportModal,
validateExportedAppStructure,
} from "Support/utils/exportImport";
import { selectAppCardOption, closeModal } from "Support/utils/common";
import { selectAppCardOption, closeModal, deleteDownloadsFolder } from "Support/utils/common";
describe("App Export", () => {
const TEST_DATA = {
@ -19,35 +21,30 @@ describe("App Export", () => {
},
};
let data;
data = {
const generateTestData = () => ({
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
appName: `${fake.companyName}-IE-App`,
appReName: `${fake.companyName}-${fake.companyName}-IE-App`,
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
};
});
let data;
beforeEach(() => {
data = {
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
appName: `${fake.companyName}-IE-App`,
appReName: `${fake.companyName}-${fake.companyName}-IE-App`,
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
};
data = generateTestData();
deleteDownloadsFolder();
cy.exec("mkdir -p ./cypress/downloads/");
cy.exec("cd ./cypress/downloads/ && rm -rf *");
cy.exec("mkdir -p ./cypress/downloads/");
cy.wait(3000);
cy.apiLogin();
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
cy.apiLogout();
});
it("Verify the elements of export dialog box", () => {
it("should verify elements of export dialog box", () => {
cy.intercept("POST", "**/api/v2/resources/import").as("importApp");
cy.intercept("POST", "**/api/v2/resources/export").as("exportApp");
cy.skipWalkthrough();
cy.apiLogin();
@ -55,31 +52,25 @@ describe("App Export", () => {
cy.get(importSelectors.importOptionInput)
.eq(0)
.selectFile(TEST_DATA.appFiles.multiVersion, { force: true });
cy.wait(2000);
cy.clearAndType(commonSelectors.appNameInput, data.appName);
cy.get(importSelectors.importAppButton).click();
cy.wait(3000);
cy.wait("@importApp");
cy.backToApps();
cy.reload();
// Select the app card option to export the app
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.exportAppOption)
);
// Verify the elements of the export modal
verifyElementsOfExportModal("v3", ["v2", "v1"], [true, false, false]);
// Close the modal
closeModal(exportAppModalText.modalCloseButton);
// Ensure the modal title is no longer visible
cy.get(
commonSelectors.modalTitle(exportAppModalText.selectVersionTitle)
).should("not.exist");
// Re-open the export modal and click the export button
cy.wait(2000);
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.exportAppOption)
@ -90,95 +81,24 @@ describe("App Export", () => {
const downloadedAppExportFileName = result.stdout.split("\n")[0];
const filePath = `./cypress/downloads/${downloadedAppExportFileName}`;
// Ensure the file name contains the expected app export name
expect(downloadedAppExportFileName).to.contain(
data.appName.toLowerCase()
);
// Read and validate the exported JSON file
cy.readFile(filePath).then((appData) => {
// Validate the app name
const appNameFromFile = appData.app[0].definition.appV2.name;
expect(appNameFromFile).to.equal(data.appName);
// Validate the schema for the student table in tooljetdb
const tooljetDatabase = appData.tooljet_database.find(
(db) => db.table_name === "student"
);
expect(tooljetDatabase).to.exist;
expect(tooljetDatabase.schema).to.exist;
// Validate components and queries
const components = appData.app[0].definition.appV2.components;
const text2Component = components.find(
(component) => component.name === "text2"
);
expect(text2Component).to.exist;
expect(text2Component.properties.text.value).to.equal(
"{{constants.pageHeader}}"
);
const textinput1 = components.find(
(component) => component.name === "textinput1"
);
expect(textinput1).to.exist;
expect(textinput1.properties.value.value).to.include("queries");
const textinput2 = components.find(
(component) => component.name === "textinput2"
);
expect(textinput2).to.exist;
expect(textinput2.properties.value.value).to.include("queries");
const textinput3 = components.find(
(component) => component.name === "textinput3"
);
expect(textinput3).to.exist;
expect(textinput3.properties.value.value).to.include("queries");
// Validate the data queries
const dataQueries = appData.app[0].definition.appV2.dataQueries;
const postgresqlQuery = dataQueries.find(
(query) => query.name === "postgresql1"
);
expect(postgresqlQuery).to.exist;
expect(postgresqlQuery.options.query).to.include(
"Select * from {{secrets.db_name}}"
);
const restapiQuery = dataQueries.find(
(query) => query.name === "restapi1"
);
expect(restapiQuery).to.exist;
expect(restapiQuery.options.url).to.equal(
"https://jsonplaceholder.typicode.com/users/1"
);
const tooljetdbQuery = dataQueries.find(
(query) => query.name === "tooljetdb1"
);
expect(tooljetdbQuery).to.exist;
expect(tooljetdbQuery.options.operation).to.equal("list_rows");
// Ensure appVersions exists
const appVersions = appData.app[0].definition.appV2.appVersions;
expect(appVersions).to.exist;
// Map and verify app version names
const versionNames = appVersions.map((version) => version.name);
expect(versionNames).to.include.members(["v1", "v2", "v3"]);
validateExportedAppStructure(appData, data.appName, {
expectedVersions: ["v1", "v2", "v3"],
});
});
});
cy.exec("cd ./cypress/downloads/ && rm -rf *");
deleteDownloadsFolder();
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.exportAppOption)
);
cy.get(`[data-cy="v1-radio-button"]`).check();
cy.get(exportAppModalSelectors.versionRadioButton("v3")).check();
cy.get(
commonSelectors.buttonSelector(exportAppModalText.exportSelectedVersion)
).click();
@ -187,32 +107,32 @@ describe("App Export", () => {
const downloadedAppExportFileName = result.stdout.split("\n")[0];
const filePath = `./cypress/downloads/${downloadedAppExportFileName}`;
// Ensure the file name contains the expected app export name
expect(downloadedAppExportFileName).to.contain(
data.appName.toLowerCase()
);
// Read and validate the exported JSON file
cy.readFile(filePath).then((appData) => {
// Validate the app name
const appNameFromFile = appData.app[0].definition.appV2.name;
expect(appNameFromFile).to.equal(data.appName);
validateExportedAppStructure(appData, data.appName, {
validateVersions: false,
});
});
});
});
it.skip("Verify 'Export app' functionality of an application inside app editor", () => {
it("should verify export app functionality inside app editor", () => {
cy.intercept("POST", "**/api/v2/resources/export").as("exportApp");
data.appName2 = `${fake.companyName}-App`;
cy.apiCreateApp(data.appName2);
cy.openApp(data.appName2);
cy.dragAndDropWidget("Text Input", 50, 50);
cy.dragAndDropWidget("Text Input", 200, 200);
cy.get('[data-cy="left-sidebar-settings-button"]').click();
cy.get('[data-cy="button-user-status-change"]').click();
cy.get(commonSelectors.leftSideBarSettingsButton).click();
cy.get(commonSelectors.buttonSelector("Export app")).click();
verifyElementsOfExportModal("v1");
exportAllVersionsAndVerify(data.appName1, "v1");
exportAllVersionsAndVerify(data.appName2, "v1");
});
});
});

View file

@ -2,13 +2,16 @@ import { fake } from "Fixtures/fake";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { appVersionSelectors, importSelectors } from "Selectors/exportImport";
import { dashboardSelector } from "Selectors/dashboard";
import { buttonText } from "Texts/button";
import { importText } from "Texts/exportImport";
import { importAndVerifyApp } from "Support/utils/exportImport";
import {
importAndVerifyApp,
verifyImportModalElements,
setupDataSourceWithConstants,
} from "Support/utils/exportImport";
import { switchVersionAndVerify } from "Support/utils/version";
import { renameApp } from 'Support/utils/editor/editorHeaderOperations';
describe("App Import Functionality", () => {
describe("App Import", () => {
const TEST_DATA = {
toolJetImage: "cypress/fixtures/Image/tooljet.png",
invalidApp: "cypress/fixtures/templates/invalid_app.json",
@ -19,112 +22,94 @@ describe("App Import Functionality", () => {
},
};
const generateTestData = () => ({
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
appName: `${fake.companyName}-IE-App`,
appReName: `${fake.companyName}-${fake.companyName}-IE-App`,
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
});
const setupWorkspaceConstants = (dsEnv) => {
cy.apiCreateWorkspaceConstant("pageHeader", "Import and Export", ["Global"], [dsEnv]);
cy.apiCreateWorkspaceConstant("db_name", "persons", ["Secret"], [dsEnv]);
};
const getDataSourceEnvironment = () => {
const edition = Cypress.env("environment");
return edition === "Community" ? "production" : "development";
};
const verifyAppNameInEditor = (expectedName) => {
cy.get('[data-cy="edit-app-name-button"]')
.should("be.visible")
.verifyVisibleElement("have.text", expectedName);
};
const setupCommunityOrEnterpriseDataSource = () => {
const edition = Cypress.env("environment");
if (edition === "Community" || edition === "Enterprise") {
const dsEnv = getDataSourceEnvironment();
setupDataSourceWithConstants(dsEnv);
setupWorkspaceConstants(dsEnv);
return dsEnv;
}
return null;
};
let data;
beforeEach(() => {
cy.viewport(1400, 1400);
data = {
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
appName: `${fake.companyName}-IE-App`,
appReName: `${fake.companyName}-${fake.companyName}-IE-App`,
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
};
data = generateTestData();
cy.apiLogin();
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
cy.apiLogout();
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug).then((workspace) => {
Cypress.env("workspaceId", workspace.body.organization_id);
});
cy.skipWalkthrough();
});
it("should verify app import functionality", () => {
cy.apiLogin();
cy.visit(`${data.workspaceSlug}`);
// Test invalid file import
cy.get(dashboardSelector.importAppButton).click();
importAndVerifyApp(
TEST_DATA.toolJetImage,
importText.couldNotImportAppToastMessage
);
cy.wait(500);
cy.get(dashboardSelector.importAppButton).click();
importAndVerifyApp(
TEST_DATA.invalidApp,
"Could not import: SyntaxError: Expected ',' or '}' after property value in JSON at position 246 (line 11 column 13)"
);
cy.wait(500);
// Test valid app import
});
it("should verify invalid import files", () => {
cy.get(importSelectors.dropDownMenu).should("be.visible").click();
cy.get(importSelectors.importOptionLabel).verifyVisibleElement(
"have.text",
importText.importOption
);
cy.get(dashboardSelector.importAppButton).click();
importAndVerifyApp(
TEST_DATA.toolJetImage,
importText.couldNotImportAppToastMessage
);
cy.get(dashboardSelector.importAppButton).should("be.visible").click();
importAndVerifyApp(
TEST_DATA.invalidApp,
"Could not import: SyntaxError: Expected ',' or '}' after property value in JSON at position 246 (line 11 column 13)"
);
});
it("should verify app with multiple version", () => {
cy.intercept("POST", "/api/v2/resources/import").as("importApp");
cy.get(importSelectors.importOptionInput)
.eq(0)
.selectFile(TEST_DATA.appFiles.multiVersion, {
force: true,
});
cy.wait(1500);
.selectFile(TEST_DATA.appFiles.multiVersion, { force: true });
cy.get(importSelectors.importAppTitle).verifyVisibleElement(
"have.text",
"Import app"
);
cy.get(commonSelectors.appNameLabel).verifyVisibleElement(
"have.text",
"App name"
);
cy.get(commonSelectors.appNameInput)
.should("be.visible")
.and("have.value", "three-versions");
cy.get(commonSelectors.appNameInfoLabel).verifyVisibleElement(
"have.text",
"App name must be unique and max 50 characters"
);
cy.get(commonSelectors.cancelButton)
.should("be.visible")
.and("have.text", "Cancel");
cy.get(commonSelectors.importAppButton).verifyVisibleElement(
"have.text",
"Import app"
);
verifyImportModalElements("three-versions");
cy.get(importSelectors.importAppButton).click();
cy.get(".go3958317564")
cy.get(commonSelectors.toastMessage)
.should("be.visible")
.and("have.text", importText.appImportedToastMessage);
// Verify imported app
cy.get(commonSelectors.toastCloseButton).click();
cy.wait(500);
cy.get(commonSelectors.appNameInput).verifyVisibleElement(
"contain.value",
"three-versions"
);
verifyAppNameInEditor("three-versions");
cy.get(appVersionSelectors.currentVersionField("v3")).should("be.visible");
// Configure app
cy.skipEditorPopover();
cy.dragAndDropWidget(buttonText.defaultWidgetText);
cy.get(appVersionSelectors.appVersionLabel).should("be.visible");
cy.get(commonWidgetSelector.draggableWidget("button1")).should(
"be.visible"
);
cy.renameApp(data.appName);
cy.get(commonSelectors.appNameInput).verifyVisibleElement(
"contain.value",
data.appName
);
cy.waitForAutoSave();
// Verify initial widget states
renameApp(data.appName);
verifyAppNameInEditor(data.appName);
verifyCommonData({
text2: "",
@ -132,72 +117,16 @@ describe("App Import Functionality", () => {
textInput2: "Leanne Graham",
});
// cy.get(
// commonWidgetSelector.draggableWidget("textInput3")
// ).verifyVisibleElement("have.value", "");
// Setup database and data sources
cy.visit(`${data.workspaceSlug}/database`);
cy.get('[data-cy="student-table"]').verifyVisibleElement(
"have.text",
"student"
);
// cy.apiAddDataToTable("student", {
// name: "Paramu",
// country: "India",
// state: "Kerala",
// });
cy.get('[data-cy="student-table"]').verifyVisibleElement("have.text", "student");
cy.visit(`${data.workspaceSlug}/data-sources`);
cy.get('[data-cy="postgresql-button"]').should("be.visible");
setupCommunityOrEnterpriseDataSource();
cy.ifEnv("Community", () => {
cy.apiUpdateDataSource("postgresql", "production", {
options: [
{
key: "password",
value: `${Cypress.env("pg_password")}`,
encrypted: true,
},
],
});
});
cy.ifEnv("Enterprise", () => {
cy.apiUpdateDataSource("postgresql", "development", {
options: [
{
key: "password",
value: `${Cypress.env("pg_password")}`,
encrypted: true,
},
],
});
});
cy.ifEnv("Community", () => {
cy.apiCreateWorkspaceConstant(
"pageHeader",
"Import and Export",
["Global"],
["production"]
);
cy.apiCreateWorkspaceConstant("db_name", "persons", ["Secret"], ["production"]);
});
cy.ifEnv("Enterprise", () => {
cy.apiCreateWorkspaceConstant(
"pageHeader",
"Import and Export",
["Global"],
["development"]
);
cy.apiCreateWorkspaceConstant("db_name", "persons", ["Secret"], ["development"]);
});
// Verify app after setup
cy.wait("@importApp").then((interception) => {
const appId = interception.response.body.imports.app[0].id;
cy.log(`Imported app id: ${appId}`);
cy.openApp(
"",
Cypress.env("workspaceId"),
@ -211,31 +140,28 @@ describe("App Import Functionality", () => {
textInput1: "John",
textInput2: "Leanne Graham",
});
// cy.get(
// commonWidgetSelector.draggableWidget("textInput3")
// ).verifyVisibleElement("have.value", "India");
switchVersionAndVerify("v3", "v1");
verifyCommonData({
text2: "Import and Export",
textInput1: "John",
textInput2: "Leanne Graham",
});
});
cy.wait(1000);
cy.backToApps();
// Test single version import
it("should verify app with single version", () => {
cy.get(importSelectors.dropDownMenu).click();
const dsEnv = setupCommunityOrEnterpriseDataSource();
importAndVerifyApp(TEST_DATA.appFiles.singleVersion);
// Verify final state
cy.get(commonSelectors.appNameInput).verifyVisibleElement(
"contain.value",
"one_version"
);
verifyAppNameInEditor("one_version");
if (dsEnv) {
setupDataSourceWithConstants(dsEnv);
}
cy.reload();
verifyCommonData({
text2: "Import and Export",
textInput1: "John",

View file

@ -1,53 +1,40 @@
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { fake } from "Fixtures/fake";
import { releaseApp } from "Support/utils/common";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import {
resolveHost,
verifySlugValidations,
verifySuccessfulSlugUpdate,
verifyURLs,
resolveHost,
} from "Support/utils/apps";
import { appPromote } from "Support/utils/platform/multiEnv";
import { releaseApp } from "Support/utils/common";
describe("App Slug", () => {
const data = {};
const host = resolveHost();
beforeEach(() => {
data.slug = `${fake.companyName.toLowerCase()}-app`;
data.appName = `${fake.companyName} App`;
cy.defaultWorkspaceLogin();
const generateTestData = () => ({
slug: `${fake.companyName.toLowerCase()}-app`,
appName: `${fake.companyName} App`,
});
before(() => {
data.appName = `${fake.companyName} App`;
const host = resolveHost();
let data;
beforeEach(() => {
data = generateTestData();
cy.apiLogin();
cy.skipWalkthrough();
cy.apiCreateApp(data.appName);
cy.wait(1000);
cy.apiLogout();
cy.openApp()
});
it("Verify app slug cases in global settings", () => {
const workspaceId = Cypress.env("workspaceId");
const appId = Cypress.env("appId");
const appUrl = `${host}/${Cypress.env("workspaceId")}/apps/${Cypress.env("appId")}/`;
cy.apiLogin();
cy.skipWalkthrough();
cy.visit(appUrl);
cy.url().then((url) => {
if (url !== appUrl) {
cy.visit(appUrl);
}
});
cy.url().should("eq", appUrl);
cy.wait(1000);
cy.url().should("eq", `${host}/${Cypress.env("workspaceId")}/apps/${Cypress.env("appId")}/`);
cy.get('[data-cy="query-manager-toggle-button"]', { timeout: 20000 }).should("be.visible").click();
cy.get(commonSelectors.leftSideBarSettingsButton).click();
// Verify initial state
cy.get(commonWidgetSelector.appSlugLabel).verifyVisibleElement(
"have.text",
"Unique app slug"
@ -68,26 +55,21 @@ describe("App Slug", () => {
cy.get(commonWidgetSelector.appLinkField).verifyVisibleElement(
"have.text",
`${host}/${workspaceId}/apps/${appId}`
`${host}/${Cypress.env("workspaceId")}/apps/${Cypress.env("appId")}`
);
// Validate all error cases
verifySlugValidations(commonWidgetSelector.appSlugInput);
// Verify successful slug update
cy.clearAndType(commonWidgetSelector.appSlugInput, data.slug);
verifySuccessfulSlugUpdate(workspaceId, data.slug);
verifySuccessfulSlugUpdate(Cypress.env("workspaceId"), data.slug);
// Verify persistence
cy.get('[data-cy="left-sidebar-debugger-button"]').click();
cy.get(commonSelectors.leftSideBarSettingsButton).click();
cy.get(commonWidgetSelector.appSlugInput).should("have.value", data.slug);
// Release and verify URLs
releaseApp();
verifyURLs(workspaceId, data.slug, true);
verifyURLs(Cypress.env("workspaceId"), data.slug, true);
// Verify duplicate slug validation
cy.visit("/my-workspace");
cy.apiCreateApp(data.slug);
cy.openApp("my-workspace");
@ -101,20 +83,14 @@ describe("App Slug", () => {
});
it("Verify app slug cases in share modal", () => {
cy.apiLogin();
const workspaceId = Cypress.env("workspaceId");
cy.apiCreateApp(data.appName);
cy.openApp("my-workspace");
// Set up initial slug
cy.get(commonSelectors.leftSideBarSettingsButton).click();
cy.get(commonWidgetSelector.appSlugInput).clear();
cy.clearAndType(commonWidgetSelector.appSlugInput, data.slug);
releaseApp();
// Verify share modal
cy.get(commonWidgetSelector.shareAppButton).click();
cy.get(commonWidgetSelector.appLink).verifyVisibleElement(
"have.text",
@ -125,12 +101,11 @@ describe("App Slug", () => {
data.slug
);
// Validate all error cases in share modal
verifySlugValidations(commonWidgetSelector.appNameSlugInput);
cy.wait(500);
cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug);
cy.get('[data-cy="app-slug-info-label"]')
.should("be.visible")
.invoke("text")
.then((text) => {
expect(text.trim()).to.eq(
@ -138,26 +113,23 @@ describe("App Slug", () => {
);
});
// Verify successful slug update in share modal
data.slug = `${fake.companyName.toLowerCase()}-app`;
cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug);
const newSlug = `${fake.companyName.toLowerCase()}-app`;
cy.clearAndType(commonWidgetSelector.appNameSlugInput, newSlug);
cy.get('[data-cy="app-slug-accepted-label"]').verifyVisibleElement(
"have.text",
"Slug accepted!"
);
// Close modal and verify URLs
cy.get(commonWidgetSelector.modalCloseButton).click();
verifyURLs(workspaceId, data.slug, true);
verifyURLs(Cypress.env("workspaceId"), newSlug, true);
// Verify duplicate slug validation in share modal
cy.visit("/my-workspace");
cy.apiCreateApp(data.slug);
cy.apiCreateApp(newSlug);
cy.openApp("my-workspace");
releaseApp();
cy.get(commonWidgetSelector.shareAppButton).click();
cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug);
cy.clearAndType(commonWidgetSelector.appNameSlugInput, newSlug);
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
"have.text",
"This app slug is already taken."

View file

@ -11,6 +11,7 @@ import {
verifyElementsOfCreateNewVersionModal,
navigateToEditVersionModal,
switchVersionAndVerify,
openPreviewSettings,
} from "Support/utils/version";
import { appVersionSelectors } from "Selectors/exportImport";
import { editVersionSelectors } from "Selectors/version";
@ -27,48 +28,48 @@ import { createRestAPIQuery } from "Support/utils/dataSource";
import { deleteQuery } from "Support/utils/queries";
import { selectEnv, appPromote } from "Support/utils/platform/multiEnv";
describe("App Version", () => {
const generateTestData = () => ({
appName: `${fake.companyName}-Version-App`,
datasourceName: fake.firstName.toLowerCase(),
query1: fake.firstName.toLowerCase(),
query2: fake.firstName.toLowerCase(),
});
const verifyWidget = (selector, assertion, expectedValue) => {
cy.get(commonWidgetSelector.draggableWidget(selector))
.verifyVisibleElement(assertion, expectedValue);
};
let data;
let currentVersion = "";
let newVersion = [];
let versionFrom = "";
beforeEach(() => {
data = {
appName: `${fake.companyName}-Version-App`,
datasourceName: fake.firstName.toLowerCase(),
query1: fake.firstName.toLowerCase(),
query2: fake.firstName.toLowerCase(),
};
data = generateTestData();
cy.defaultWorkspaceLogin();
cy.apiCreateApp(data.appName);
cy.openApp();
cy.viewport(1400, 1400);
});
it("should verify basic version management operations", () => {
// Version modal verification
cy.get(appVersionSelectors.appVersionLabel).should("be.visible");
navigateToCreateNewVersionModal("v1");
verifyElementsOfCreateNewVersionModal(["v1"]);
// Empty version name validation
navigateToCreateNewVersionModal("v1");
cy.get('[data-cy="create-new-version-button"]').click();
cy.get(appVersionSelectors.createNewVersionButton).click();
cy.get(appVersionSelectors.createNewVersionButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Version name should not be empty"
);
cy.wait(2000);
cy.get(appVersionSelectors.createVersionTitle).should("not.exist");
// Duplicate version name check
verifyDuplicateVersion(["v1"], "v1");
closeModal(commonText.closeButton);
// Version edit modal verification
navigateToEditVersionModal("v1");
verifyModal(
editVersionText.editVersionTitle,
@ -76,45 +77,39 @@ describe("App Version", () => {
editVersionSelectors.versionNameInputField
);
closeModal(commonText.closeButton);
cy.wait(1000);
cy.get(editVersionSelectors.editVersionTitle).should("not.exist");
// Version editing
editVersionAndVerify(
"v1",
["v2"],
editVersionText.VersionNameUpdatedToastMessage
);
// Component operations in version
verifyComponentinrightpannel("table");
cy.get(commonSelectors.rightSidebarPlusButton).click();
cy.dragAndDropWidget("text");
cy.waitForAutoSave();
// New version creation
navigateToCreateNewVersionModal("v2");
createNewVersion(["v3"], "v2");
cy.waitForAutoSave();
verifyComponentinrightpannel("table");
// Component deletion
deleteComponentAndVerify("text1");
cy.waitForAutoSave();
cy.wait(2000);
cy.get(commonWidgetSelector.draggableWidget("text1")).should("not.exist");
// Version deletion
deleteVersionAndVerify(
"v3",
onlydeleteVersionText.deleteToastMessage("v3")
);
cy.get(appVersionSelectors.currentVersionField("v2")).should("be.visible");
cy.wait(3000);
cy.get(appVersionSelectors.currentVersionField("v3")).should("not.exist");
// cy.reload();
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible", {
timeout: 10000,
});
// Preview and release verification
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.ifEnv("Community", () => {
@ -135,7 +130,6 @@ describe("App Version", () => {
});
it("should verify version management with components and queries", () => {
// Initial setup with component and datasource
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
data.datasourceName,
@ -157,20 +151,15 @@ describe("App Version", () => {
appPromote("development", "production");
});
// Version v2 creation and verification and v2 is created from v1 production environment
navigateToCreateNewVersionModal("v1");
createNewVersion(["v2"], "v1");
cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement(
"have.text",
"Leanne Graham"
);
verifyWidget("text1", "have.text", "Leanne Graham");
cy.get(`[data-cy="list-query-${data.query1}"]`).should("be.visible");
// Modify v2 with new components and queries
deleteComponentAndVerify("text1");
cy.waitForAutoSave();
deleteQuery(data.query1);
cy.get('[data-cy="modal-confirm-button"]').click();
cy.get(commonSelectors.modalConfirmButton).click();
createRestAPIQuery(data.query2, data.datasourceName, "", "", "/2", true);
cy.apiAddComponentToApp(
data.appName,
@ -181,7 +170,6 @@ describe("App Version", () => {
);
cy.waitForAutoSave();
// Version creation and state verification
const versionChecks = [
{
create: { version: "v3", from: "v2" },
@ -210,29 +198,21 @@ describe("App Version", () => {
navigateToCreateNewVersionModal(check.create.from);
createNewVersion([check.create.version], check.create.from);
cy.waitForAutoSave();
cy.wait(1000);
if (check.verify.component.value) {
cy.get(
commonWidgetSelector.draggableWidget(check.verify.component.selector)
).verifyVisibleElement("have.value", check.verify.component.value);
} else {
cy.get(
commonWidgetSelector.draggableWidget(check.verify.component.selector)
).verifyVisibleElement("have.text", check.verify.component.text);
}
cy.get(`[data-cy="list-query-${check.verify.query}"]`).should(
"be.visible"
);
cy.get(appVersionSelectors.currentVersionField(check.create.version)).should("be.visible");
const assertion = check.verify.component.value ? "have.value" : "have.text";
const expected = check.verify.component.value || check.verify.component.text;
verifyWidget(check.verify.component.selector, assertion, expected);
cy.get(`[data-cy="list-query-${check.verify.query}"]`).should("be.visible");
});
// Release and version state verification
releasedVersionAndVerify("v5");
cy.get(appVersionSelectors.currentVersionField("v5")).should(
"have.class",
"color-light-green"
);
// Version switching and component verification
cy.ifEnv("Enterprise", () => {
selectEnv("development");
});
@ -242,13 +222,9 @@ describe("App Version", () => {
"not.have.class",
"color-light-green"
);
cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement(
"have.text",
"Leanne Graham"
);
verifyWidget("text1", "have.text", "Leanne Graham");
cy.get(`[data-cy="list-query-${data.query1}"]`).should("be.visible");
// Preview and version switching verification
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.ifEnv("Community", () => {
@ -258,17 +234,12 @@ describe("App Version", () => {
cy.url().should("include", "/home?env=development&version=v4");
});
cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement(
"have.text",
"Leanne Graham"
);
verifyWidget("text1", "have.text", "Leanne Graham");
cy.get('[data-cy="preview-settings"]').click();
openPreviewSettings();
switchVersionAndVerify("v4", "v5");
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
verifyWidget("textInput", "have.value", "Ervin Howell");
cy.ifEnv("Enterprise", () => {
cy.openApp(
@ -281,62 +252,43 @@ describe("App Version", () => {
navigateToCreateNewVersionModal("v5");
createNewVersion(["v6"], "v5");
cy.waitForAutoSave();
cy.wait(1000);
cy.get(appVersionSelectors.currentVersionField("v6")).should("be.visible");
appPromote("development", "staging");
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
verifyWidget("textInput", "have.value", "Ervin Howell");
cy.get(`[data-cy="list-query-${data.query2}"]`).should("be.visible");
appPromote("staging", "production");
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
verifyWidget("textInput", "have.value", "Ervin Howell");
cy.get(`[data-cy="list-query-${data.query2}"]`).should("be.visible");
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
verifyWidget("textInput", "have.value", "Ervin Howell");
cy.url().should("include", "/home?env=production&version=v6");
cy.wait(1000);
cy.get('[data-cy="preview-settings"]').click();
openPreviewSettings();
switchVersionAndVerify("v6", "v1");
cy.get(
commonWidgetSelector.draggableWidget("text1")
).verifyVisibleElement("have.text", "Leanne Graham");
// url bug
// cy.url().should("include", "/home?env=production&version=v1");
cy.wait(1000);
cy.get('[data-cy="preview-settings"]').click();
verifyWidget("text1", "have.text", "Leanne Graham");
openPreviewSettings();
switchVersionAndVerify("v1", "v6");
cy.wait(1000);
cy.get('[data-cy="preview-settings"]').click();
openPreviewSettings();
cy.forceClickOnCanvas();
openPreviewSettings();
selectEnv("staging");
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
// cy.url().should("include", "/home?env=staging&version=v6");
verifyWidget("textInput", "have.value", "Ervin Howell");
cy.wait(1000);
cy.get('[data-cy="preview-settings"]').click();
openPreviewSettings();
selectEnv("development");
cy.wait(1000);
cy.get('[data-cy="preview-settings"]').click();
openPreviewSettings();
switchVersionAndVerify("v6", "v1");
cy.get(
commonWidgetSelector.draggableWidget("text1")
).verifyVisibleElement("have.text", "Leanne Graham");
verifyWidget("text1", "have.text", "Leanne Graham");
});
});
});

View file

@ -1,11 +1,11 @@
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { fake } from "Fixtures/fake";
import { logout, releaseApp } from "Support/utils/common";
import { inviteUserToWorkspace } from "Support/utils/manageUsers";
import { inviteUserToWorkspace, fetchAndVisitInviteLinkViaMH } from "Support/utils/manageUsers";
import { setSignupStatus } from "Support/utils/manageSSO";
import { onboardingSelectors } from "Selectors/onboarding";
import { commonText } from "Texts/common";
import { userSignUp, addNewUser } from "Support/utils/onboarding";
import { userSignUp } from "Support/utils/onboarding";
import {
setUpSlug,
setupAppWithSlug,
@ -21,35 +21,46 @@ describe(
retries: { runMode: 2 },
},
() => {
const generateTestData = () => ({
appName: `${fake.companyName} P P App`,
slug: `${fake.companyName} P P App`.toLowerCase().replace(/\s+/g, "-"),
firstName: fake.firstName,
email: fake.email.toLowerCase(),
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
appPublicSlug: `${fake.companyName} Public App`
.toLowerCase()
.replace(/\s+/g, "-"),
appPublicName: `${fake.companyName} Public App`,
appPrivateSlug: `${fake.companyName} Private App`
.toLowerCase()
.replace(/\s+/g, "-"),
appPrivateName: `${fake.companyName} Private App`,
});
const verifyWidget = (widgetName) => {
cy.get(commonWidgetSelector.draggableWidget(widgetName)).should("be.visible");
};
const getAppUrl = (slug) => `${Cypress.config("baseUrl")}/applications/${slug}`;
let data;
beforeEach(() => {
data = {
appName: `${fake.companyName} P P App`,
slug: `${fake.companyName} P P App`.toLowerCase().replace(/\s+/g, "-"),
firstName: fake.firstName,
email: fake.email.toLowerCase(),
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
};
data = generateTestData();
cy.defaultWorkspaceLogin();
cy.skipWalkthrough();
});
it("Verify private and public app share functionality", () => {
it("should verify private and public app share functionality", () => {
cy.apiCreateApp(data.appName);
cy.openApp();
cy.apiAddComponentToApp(data.appName, "text1");
cy.dragAndDropWidget("text", 500, 500);
// Check unreleased version state
cy.get('[data-cy="share-button-link"]>span').should("be.visible").click();
cy.contains("This version has not been released yet").should(
"be.visible"
);
cy.get(commonWidgetSelector.shareAppButton).should("be.visible").click();
cy.contains("This version has not been released yet").should("be.visible");
cy.get(commonWidgetSelector.modalCloseButton).click();
// Release and verify share modal
releaseApp();
cy.get(commonWidgetSelector.shareAppButton).click();
for (const elements in commonWidgetSelector.shareModalElements) {
@ -61,7 +72,6 @@ describe(
);
}
// Verify share modal elements
const shareModalSelectors = [
"copyAppLinkButton",
"makePublicAppToggle",
@ -73,9 +83,8 @@ describe(
cy.get(commonWidgetSelector[selector]).should("be.visible");
});
// Configure and verify slug
cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug);
cy.get('[data-cy="app-slug-accepted-label"]')
cy.get(commonSelectors.appSlugAccept)
.should("be.visible")
.and("have.text", "Slug accepted!");
@ -83,24 +92,14 @@ describe(
cy.forceClickOnCanvas();
cy.backToApps();
// Test private access
logout();
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
});
cy.visitSlug({ actualUrl: getAppUrl(data.slug) });
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should(
"be.visible"
);
cy.wait(2000);
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible");
cy.appUILogin();
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
"be.visible"
);
verifyWidget("text1");
// Test public access
// cy.get(commonSelectors.viewerPageLogo).click();
cy.openApp(
"appSlug",
Cypress.env("workspaceId"),
@ -113,75 +112,39 @@ describe(
cy.backToApps();
logout();
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
});
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
"be.visible"
);
cy.visitSlug({ actualUrl: getAppUrl(data.slug) });
verifyWidget("text1");
});
it("Verify app private and public app visibility for the same workspace user", () => {
it("should verify app private and public app visibility for the same workspace user", () => {
setupAppWithSlug(data.appName, data.slug);
inviteUserToWorkspace(data.firstName, data.email);
logout();
cy.visit("/");
cy.wait(2000);
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should(
"be.visible"
);
// Test private access
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
});
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible");
cy.visitSlug({ actualUrl: getAppUrl(data.slug) });
cy.wait(2000);
cy.appUILogin(data.email, "password");
verifyWidget("text1");
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
"be.visible"
);
cy.visitSlug({ actualUrl: getAppUrl(data.slug) });
verifyWidget("text1");
// Test with private app valid session
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
});
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
"be.visible"
);
// cy.get(commonSelectors.viewerPageLogo).click();
// Test public access
cy.defaultWorkspaceLogin();
cy.wait(1000);
cy.apiMakeAppPublic();
logout();
cy.wait(1000);
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should(
"be.visible"
);
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible");
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
});
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
"be.visible"
);
cy.visitSlug({ actualUrl: getAppUrl(data.slug) });
verifyWidget("text1");
// Test with public app with valid session
cy.apiLogin(data.email, "password");
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
});
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
"be.visible"
);
cy.visitSlug({ actualUrl: getAppUrl(data.slug) });
verifyWidget("text1");
});
it("Verify app private and public app visibility for the same instance user", () => {
it("should verify app private and public app visibility for the same instance user", () => {
setupAppWithSlug(data.appName, data.slug);
cy.apiLogout();
@ -189,131 +152,77 @@ describe(
InstanceSSO(true, true, true);
});
userSignUp(data.firstName, data.email, data.workspaceName);
cy.wait(1000);
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
});
cy.visitSlug({ actualUrl: getAppUrl(data.slug) });
cy.visit("/");
logout();
// Test public access
cy.defaultWorkspaceLogin();
cy.apiMakeAppPublic();
logout();
cy.wait(1000);
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should(
"be.visible"
);
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible");
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
});
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
"be.visible"
);
cy.visitSlug({ actualUrl: getAppUrl(data.slug) });
verifyWidget("text1");
// Verify public app with valid session
cy.apiLogin(data.email, "password");
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
});
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
"be.visible"
);
cy.visitSlug({ actualUrl: getAppUrl(data.slug) });
verifyWidget("text1");
});
it("Should redirect to workspace login and handle signup flow of existing and non-existing user", () => {
setSignupStatus(true);
setupAppWithSlug(data.appName, data.slug);
cy.apiLogout();
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
});
cy.get(commonSelectors.workspaceName).verifyVisibleElement(
"have.text",
"My workspace"
);
// Test signup flow
it("should redirect to workspace login and handle signup flow of existing and non-existing user", () => {
cy.intercept("POST", "/api/onboarding/signup").as("signup");
cy.get(commonSelectors.createAnAccountLink).click();
cy.wait(3000);
cy.intercept('GET', '**/api/white-labelling').as('whiteLabelling');
cy.apiUserInvite(`${data.firstName}_invited`, `invited_${data.email}`);
setSignupStatus(true);
setupAppWithSlug(data.appPublicName, data.appPublicSlug, 'public');
const publicAppId = Cypress.env("appId");
cy.apiMakeAppPublic(publicAppId);
setupAppWithSlug(data.appPrivateName, data.appPrivateSlug);
cy.visitSlug({ actualUrl: getAppUrl(data.slug) })
cy.visitSlug({ actualUrl: getAppUrl(data.appPublicSlug) });
verifyWidget('public')
cy.visitSlug({ actualUrl: getAppUrl(data.appPrivateSlug) });
verifyWidget('private')
cy.apiLogout();
cy.visitSlug({ actualUrl: getAppUrl(data.appPublicSlug) });
verifyWidget('public')
cy.visitSlug({ actualUrl: getAppUrl(data.appPrivateSlug) });
cy.wait('@whiteLabelling');
cy.get(commonSelectors.createAnAccountLink, { timeout: 20000 }).click();
cy.get(onboardingSelectors.loginPasswordInput).should("be.visible").click({ force: true });
cy.clearAndType(commonSelectors.inputFieldFullName, data.firstName);
cy.clearAndType(commonSelectors.inputFieldEmailAddress, data.email);
cy.clearAndType('[data-cy="email-input"]', data.email);
cy.clearAndType(onboardingSelectors.loginPasswordInput, "password");
cy.get(commonSelectors.signUpButton).click();
cy.wait('@signup');
cy.get(`[data-cy="resend-verification-email-button"]`).should("be.visible");
cy.wait("@signup").then((interception) => {
expect(interception.response.statusCode).to.eq(201);
});
fetchAndVisitInviteLinkViaMH(data.email);
verifyWidget('private')
// Process invitation
onboardUserFromAppLink(data.email, data.slug);
// cy.apiLogout();
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
"be.visible"
);
// cy.visitSlug({ actualUrl: getAppUrl(data.appPrivateSlug) })
// cy.get(onboardingSelectors.loginPasswordInput).should("be.visible").click({ force: true });
// cy.clearAndType('[data-cy="email-input"]', `invited_${data.email}`);
// cy.clearAndType(onboardingSelectors.loginPasswordInput, "password");
// cy.get('[data-cy="viewer-page-logo"]').click();
cy.visit("/my-workspace");
cy.wait(2000);
logout();
cy.wait(1000);
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should(
"be.visible"
);
// Setup new workspace and app
cy.defaultWorkspaceLogin();
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
cy.apiLogout();
cy.apiLogin();
cy.visit(`${data.workspaceSlug}`);
setSignupStatus(true, data.workspaceName);
data.slug = fake.firstName.toLowerCase().replace(/\s+/g, "-");
cy.createApp(data.appName);
cy.wait(2000);
cy.dragAndDropWidget("Text", 500, 500);
releaseApp();
setUpSlug(data.slug);
cy.forceClickOnCanvas();
cy.backToApps();
// Test signup flow in new workspace
cy.apiLogout();
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
});
cy.get(commonSelectors.workspaceName).verifyVisibleElement(
"have.text",
data.workspaceName
);
cy.get(commonSelectors.createAnAccountLink).click();
cy.wait(3000);
cy.clearAndType(commonSelectors.inputFieldFullName, data.firstName);
cy.clearAndType(commonSelectors.inputFieldEmailAddress, data.email);
cy.clearAndType(onboardingSelectors.loginPasswordInput, "password");
cy.get(commonSelectors.signUpButton).click();
cy.wait("@signup").then((interception) => {
expect(interception.response.statusCode).to.eq(201);
});
onboardUserFromAppLink(data.email, data.slug, data.workspaceName, false);
cy.get(commonWidgetSelector.draggableWidget("text1")).should(
"be.visible"
);
});
it("Should verify restricted app access", () => {
it("should verify restricted app access", () => {
data.workspaceName = fake.firstName;
data.workspaceSlug = fake.firstName.toLowerCase().replace(/\s+/g, "-");
@ -330,65 +239,12 @@ describe(
inviteUserToWorkspace(data.firstName, data.email);
// Verify restricted access
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
});
verifyRestrictedAccess();
cy.get('[data-cy="back-to-home-button"]').click();
cy.get(commonSelectors.backToHomeButton).click();
cy.get(commonSelectors.homePageLogo).should("be.visible");
cy.apiLogout();
});
it.skip("Should verify private app access for different workspace users", () => {
const firstName1 = fake.firstName;
const email1 = fake.email.toLowerCase();
const permissionName = fake.firstName.toLowerCase(); // Defined but not used in original
const urls = {
editor: `${Cypress.config("baseUrl")}/my-workspace/apps/${data.slug}/home`,
preview: `${Cypress.config("baseUrl")}/applications/${data.slug}/home?version=v1`,
released: `${Cypress.config("baseUrl")}/applications/${data.slug}`,
};
// Setup workspace and app
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
cy.apiLogout();
cy.apiLogin();
cy.visit(`${data.workspaceSlug}`);
setupAppWithSlug(data.appName, data.slug);
// Invite workspace user
addNewUser(data.firstName, data.email);
cy.wait(500);
// Verify access restrictions
cy.visitSlug({ actualUrl: urls.editor });
verifyRestrictedAccess();
cy.get('[data-cy="back-to-home-button"]').click();
cy.get(commonSelectors.homePageLogo).should("be.visible");
cy.visitSlug({ actualUrl: urls.preview });
// Switch users and verify access
cy.apiLogout();
cy.apiLogin();
cy.apiDeleteGranularPermission("end-user");
cy.apiLogin(data.email, "password");
cy.visitSlug({ actualUrl: urls.editor });
verifyRestrictedAccess();
cy.get('[data-cy="back-to-home-button"]').click();
cy.get(commonSelectors.homePageLogo).should("be.visible");
cy.visitSlug({ actualUrl: urls.preview });
cy.apiLogout();
// Test with new user
userSignUp(firstName1, email1, data.workspaceName);
cy.visitSlug({ actualUrl: urls.editor });
cy.visitSlug({ actualUrl: urls.preview });
cy.visitSlug({ actualUrl: urls.released });
});
}
);

View file

@ -1,143 +0,0 @@
import { commonSelectors } from "Selectors/common";
import { groupsSelector } from "Selectors/manageGroups";
import { fake } from "Fixtures/fake";
import {
navigateToManageGroups,
viewAppCardOptions,
} from "Support/utils/common";
import {
OpenGroupCardOption,
verifyGroupCardOptions,
duplicateMultipleGroups,
createGroupAddAppAndUserToGroup,
groupPermission,
} from "Support/utils/manageGroups";
import { cyParamName } from "Selectors/common";
import { roleBasedOnboarding } from "Support/utils/onboarding";
const data = {};
data.groupName = fake.firstName.replaceAll("[^A-Za-z]", "");
data.appName = `${fake.companyName}-App`;
const workspaceName = fake.firstName;
const workspaceSlug = fake.firstName.toLowerCase().replace(/[^A-Za-z]/g, "");
describe("Groups duplication", () => {
beforeEach(() => {
cy.defaultWorkspaceLogin();
cy.apiCreateWorkspace(workspaceName, workspaceSlug);
cy.visit(`${workspaceSlug}`);
cy.apiLogout();
cy.apiLogin();
cy.visit(`${workspaceSlug}`);
groupPermission(
[
"appsCreateCheck",
"appsDeleteCheck",
"workspaceVarCheckbox",
"foldersCreateCheck",
],
"Admin"
);
cy.apiCreateApp(data.appName);
});
it("Should verify the group duplication feature", () => {
data.firstName = fake.firstName;
data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
cy.visit(`${workspaceSlug}`);
roleBasedOnboarding(data.firstName, data.email, "builder");
cy.apiLogout();
cy.apiLogin();
cy.visit(`${workspaceSlug}`);
navigateToManageGroups();
verifyGroupCardOptions("Admin");
cy.wait(3000);
cy.get('[datacy="groups-list-option-button"]').click();
cy.get('[data-cy="delete-group-card-option"] > .col').should(
"have.class",
"disable"
);
duplicateMultipleGroups(["Admin", "Builder", "End-user"]);
createGroupAddAppAndUserToGroup(data.groupName, data.email);
groupPermission(
[
"appsCreateCheck",
"appsDeleteCheck",
"workspaceVarCheckbox",
"foldersCreateCheck",
],
data.groupName,
true
);
cy.wait(1000);
verifyGroupCardOptions(data.groupName);
cy.get(groupsSelector.duplicateOption).click();
cy.get(commonSelectors.defaultModalTitle).verifyVisibleElement(
"have.text",
"Duplicate group"
);
cy.get(commonSelectors.modalMessage).verifyVisibleElement(
"have.text",
"Duplicate the following parts of the group"
);
cy.get(groupsSelector.usersCheckInput).should("be.visible");
cy.verifyLabel("Users");
cy.get(groupsSelector.permissionCheckInput).should("be.visible");
cy.verifyLabel("Permissions");
cy.get(groupsSelector.appsCheckInput).should("be.visible");
cy.verifyLabel("Apps");
cy.get(commonSelectors.cancelButton).verifyVisibleElement(
"have.text",
"Cancel"
);
cy.get(groupsSelector.confimButton).verifyVisibleElement(
"have.text",
"Duplicate"
);
cy.get(groupsSelector.confimButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Group duplicated successfully!"
);
cy.wait(500);
cy.get(
groupsSelector.duplicatedGroupLink(data.groupName)
).verifyVisibleElement("have.text", `${data.groupName}_copy`);
OpenGroupCardOption(data.groupName);
cy.get(groupsSelector.deleteGroupOption).click();
cy.get(commonSelectors.buttonSelector("Yes")).click();
cy.apiLogout();
cy.apiLogin(data.email, "password");
cy.visit(`${workspaceSlug}`);
cy.wait(2000);
cy.get(commonSelectors.appCreateButton).should("be.visible");
cy.get(commonSelectors.createNewFolderButton).should("be.visible");
cy.wait(2000);
cy.reload();
viewAppCardOptions(data.appName);
cy.contains("Delete app").should("exist");
cy.get(commonSelectors.workspaceConstantsIcon).should("be.visible");
cy.apiLogout();
cy.apiLogin();
cy.visit(`${workspaceSlug}`);
navigateToManageGroups();
OpenGroupCardOption(`${data.groupName}_copy`);
cy.get(groupsSelector.deleteGroupOption).click();
cy.get(commonSelectors.buttonSelector("Yes")).click();
cy.apiLogout();
cy.apiLogin(data.email, "password");
cy.visit(`${workspaceSlug}`);
cy.get(commonSelectors.appCreateButton).should("not.exist");
cy.get(commonSelectors.createNewFolderButton).should("not.exist");
cy.get(commonSelectors.workspaceConstantsIcon).should("not.exist");
});
});

View file

@ -31,21 +31,17 @@ describe("dashboard", () => {
folderName: `${fake.companyName.toLowerCase()}-folder`,
cloneAppName: `cloned-${fake.companyName}-App`,
updatedFolderName: `new-${fake.companyName.toLowerCase()}-folder`,
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replaceAll("[^A-Za-z]", ""),
};
cy.intercept("GET", "/api/library_apps").as("appLibrary");
cy.intercept("DELETE", "/api/folders/*").as("folderDeleted");
cy.skipWalkthrough();
cy.apiLogin();
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
cy.apiLogout();
cy.apiLogin();
cy.visit(`${data.workspaceSlug}`);
});
it("should verify the elements on empty dashboard", () => {
cy.intercept("GET", "/api/apps*", {
fixture: 'intercept/emptyDashboard.json'
}).as("dashboardPage");
cy.intercept("GET", "/api/metadata", {
body: {
installed_version: "2.9.2",
@ -53,13 +49,13 @@ describe("dashboard", () => {
},
}).as("version");
cy.get(commonSelectors.homePageLogo).should("be.visible");
cy.visit("/");
cy.get(commonSelectors.workspaceName).verifyVisibleElement(
"have.text",
data.workspaceName
"My workspace"
);
cy.get(commonSelectors.workspaceName).click();
// cy.get(commonSelectors.editRectangleIcon).should("be.visible");
cy.get(commonSelectors.appCreateButton).verifyVisibleElement(
"have.text",
"Create an app"
@ -105,6 +101,12 @@ describe("dashboard", () => {
cy.wait(500);
cy.get(commonSelectors.settingsIcon).click();
cy.get(dashboardSelector.versionLabel).verifyVisibleElement(
"have.text",
"Version 2.9.2"
);
cy.get(commonSelectors.marketplaceOption).verifyVisibleElement(
"have.text",
"Marketplace"
@ -122,7 +124,7 @@ describe("dashboard", () => {
commonText.logoutLink
);
cy.get(commonSelectors.breadcrumbTitle).should(($el) => {
cy.get(commonSelectors.breadcrumbHeaderTitle).should(($el) => {
expect($el.contents().first().text().trim()).to.eq(
commonText.breadcrumbApplications
);
@ -132,10 +134,6 @@ describe("dashboard", () => {
dashboardText.dashboardAppsHeaderLabel
);
cy.get(dashboardSelector.versionLabel).verifyVisibleElement(
"have.text",
"Version 2.9.2"
);
cy.get(dashboardSelector.emptyPageImage).should("be.visible");
cy.get(dashboardSelector.emptyPageHeader).verifyVisibleElement(
"have.text",
@ -158,6 +156,12 @@ describe("dashboard", () => {
cy.get(dashboardSelector.appTemplateRow).should("be.visible");
cy.reload();
const env = Cypress.env("environment");
if (env === "Enterprise" || env === "Cloud") {
cy.get(commonSelectors.homePageLogo).should("be.visible");
verifyTooltip(commonSelectors.homePageIcon, "Home");
verifyTooltip(commonSelectors.globalWorkFlowsIcon, "Workflows");
};
verifyTooltip(commonSelectors.dashboardIcon, "Apps");
verifyTooltip(commonSelectors.databaseIcon, "ToolJet Database");
verifyTooltip(commonSelectors.globalDataSourceIcon, "Data sources");
@ -171,17 +175,17 @@ describe("dashboard", () => {
it("Should verify app card elements and app card operations", () => {
cy.exec("mkdir -p ./cypress/downloads/");
cy.exec("cd ./cypress/downloads/ && rm -rf *");
cy.exec("cd ./cypress/downloads/ && rm -rf '*'");
const customLayout = {
desktop: { top: 100, left: 20 },
mobile: { width: 8, height: 50 },
};
cy.visit("/");
cy.apiCreateApp(data.appName);
cy.visit(`${data.workspaceSlug}`);
cy.wait(2000);
// cy.ifEnv(["Enterprise", "Cloud"], () => { cy.get('.basic-plan-migration-banner').invoke('css', 'display', 'none') });
// cy.wait(2000);
cy.get(commonSelectors.appCreationDetails).should("be.visible");
cy.get(commonSelectors.appCard(data.appName)).should("be.visible");
cy.get(commonSelectors.appTitle(data.appName)).verifyVisibleElement(
@ -189,6 +193,7 @@ describe("dashboard", () => {
data.appName
);
viewAppCardOptions(data.appName);
cy.get(
commonSelectors.appCardOptions(commonText.changeIconOption)
@ -205,7 +210,6 @@ describe("dashboard", () => {
cy.get(
commonSelectors.appCardOptions(commonText.deleteAppOption)
).verifyVisibleElement("have.text", commonText.deleteAppOption);
modifyAndVerifyAppCardIcon(data.appName);
createFolder(data.folderName);
@ -261,12 +265,15 @@ describe("dashboard", () => {
commonText.appRemovedFromFolderTaost,
false
);
cy.get(commonSelectors.modalComponent).should("not.exist");
cy.get(commonSelectors.empytyFolderImage).should("be.visible");
cy.wait(1000);
cy.get(commonSelectors.emptyFolderText).verifyVisibleElement(
"have.text",
commonText.emptyFolderText
);
cy.get(commonSelectors.allApplicationsLink).click();
deleteFolder(data.folderName);
@ -277,6 +284,7 @@ describe("dashboard", () => {
cy.wait(2000);
cy.get(commonSelectors.appCardOptions(commonText.exportAppOption)).click();
cy.get(commonSelectors.exportAllButton).click();
cy.wait(2000)
cy.exec("ls ./cypress/downloads/").then((result) => {
@ -292,9 +300,12 @@ describe("dashboard", () => {
.and("have.text", dashboardText.appClonedToast);
cy.wait(3000);
cy.get(commonSelectors.editorAppNameInput).click();
cy.renameApp(data.cloneAppName);
cy.apiAddComponentToApp(data.cloneAppName, "button", 25, 25);
cy.backToApps();
cy.ifEnv(["Enterprise", "Cloud"], () => { cy.get('.basic-plan-migration-banner').invoke('css', 'display', 'none') });
cy.wait("@appLibrary");
cy.wait(1000);
@ -303,6 +314,7 @@ describe("dashboard", () => {
cy.wait(1000);
viewAppCardOptions(data.cloneAppName);
cy.get(commonSelectors.deleteAppOption).click();
cy.get(commonSelectors.modalMessage).verifyVisibleElement(
"have.text",
@ -337,7 +349,7 @@ describe("dashboard", () => {
desktop: { top: 100, left: 20 },
mobile: { width: 8, height: 50 },
};
cy.visit("/");
cy.createApp(data.appName);
cy.apiAddComponentToApp(data.appName, "text1", customLayout);
@ -349,11 +361,11 @@ describe("dashboard", () => {
);
navigateToAppEditor(data.appName);
// cy.get(commonSelectors.canvas).should("contain", "text1");
cy.get(".text-widget-section > div").should("be.visible");
cy.backToApps();
cy.wait("@appLibrary");
cy.ifEnv(["Enterprise", "Cloud"], () => { cy.get('.basic-plan-migration-banner').invoke('css', 'display', 'none') });
cy.wait(2000);
cy.deleteApp(data.appName);
cy.get(commonSelectors.appCard(data.appName)).should("not.exist");
@ -364,7 +376,7 @@ describe("dashboard", () => {
desktop: { top: 100, left: 20 },
mobile: { width: 8, height: 50 },
};
cy.visit("/");
cy.createApp(data.appName);
cy.apiAddComponentToApp(data.appName, "text1", customLayout);
cy.backToApps();
@ -473,7 +485,8 @@ describe("dashboard", () => {
cy.get(dashboardSelector.folderName(data.updatedFolderName)).should(
"not.exist"
);
cy.ifEnv(["Enterprise", "Cloud"], () => { cy.get('.basic-plan-migration-banner').invoke('css', 'display', 'none') });
cy.wait(2000);
cy.get(commonSelectors.allApplicationsLink).click();
cy.deleteApp(data.appName);

View file

@ -1,10 +1,7 @@
import { fake } from "Fixtures/fake";
import { commonSelectors } from "Selectors/common";
import { groupsSelector } from "Selectors/manageGroups";
import { workspaceConstantsSelectors } from "Selectors/workspaceConstants";
import {
createFolder,
deleteFolder,
navigateToManageGroups,
selectAppCardOption,
} from "Support/utils/common";
@ -12,15 +9,25 @@ import {
createGroupsAndAddUserInGroup,
setupWorkspaceAndInviteUser,
updateRole,
} from "Support/utils/manageGroups";
import {
uiAppCRUDWorkflow,
uiDataSourceCRUDWorkflow,
uiFolderCRUDWorkflow,
uiVerifyAdminPrivileges,
uiVerifyBuilderPrivileges,
uiWorkflowCRUDWorkflow,
uiWorkspaceConstantCRUDWorkflow,
} from "Support/utils/uiPermissions";
import {
verifyBasicPermissions,
verifySettingsAccess,
} from "Support/utils/manageGroups";
import { addAndVerifyConstants } from "Support/utils/workspaceConstants";
} from "Support/utils/userPermissions";
import { commonText } from "Texts/common";
import { dashboardText } from "Texts/dashboard";
import { groupsText } from "Texts/manageGroups";
describe("Manage Groups", () => {
describe("Basic Permissions", () => {
let data = {};
before(() => {
@ -52,9 +59,98 @@ describe("Manage Groups", () => {
);
verifyBasicPermissions(false);
verifySettingsAccess(false);
});
it("should verify builder privileges and role updates in custom groups", () => {
it("should verify builder privileges", () => {
setupWorkspaceAndInviteUser(
data.workspaceName,
data.workspaceSlug,
data.firstName,
data.email,
"builder"
);
// UI-based privilege verification for Builder
cy.get(".basic-plan-migration-banner").invoke("css", "display", "none");
uiVerifyBuilderPrivileges();
// UI CRUD workflows validation
cy.get(commonSelectors.dashboardIcon).click();
const uiTestAppName = `${data.appName}_ui`;
const uiTestFolderName = `${data.folderName}-ui`;
const uiTestConstName = `${data.firstName}_const`;
const uiTestConstValue = "test_value";
// Perform UI-based CRUD operations
uiAppCRUDWorkflow(uiTestAppName);
uiFolderCRUDWorkflow(uiTestFolderName);
uiWorkspaceConstantCRUDWorkflow(uiTestConstName, uiTestConstValue);
// Enterprise-specific UI workflows
cy.ifEnv("Enterprise", () => {
const uiTestDsName = `${data.appName}_ds`;
const uiTestWorkflowName = `${data.appName}_wf`;
uiDataSourceCRUDWorkflow(uiTestDsName, "restapi");
uiWorkflowCRUDWorkflow(uiTestWorkflowName);
});
cy.get(commonSelectors.dashboardIcon).click();
cy.apiCreateApp(data.appName);
cy.openApp();
cy.releaseApp();
//verify clone access
cy.visit(data.workspaceSlug);
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.cloneAppOption)
);
cy.get(commonSelectors.cloneAppButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
dashboardText.appClonedToast,
false
);
});
it("should verify admin privileges", () => {
setupWorkspaceAndInviteUser(
data.workspaceName,
data.workspaceSlug,
data.firstName,
data.email,
"admin"
);
// API-based verification
verifyBasicPermissions(true);
// UI-based privilege verification for Admin (includes settings access)
uiVerifyAdminPrivileges();
// UI CRUD workflows for validation
cy.get(commonSelectors.dashboardIcon).click();
const uiTestAppName = `${data.appName}_admin_ui`;
const uiTestFolderName = `${data.folderName}-admin-ui`;
const uiTestConstName = `${data.firstName}_admin_const`;
const uiTestConstValue = "admin_test_value";
// Perform UI-based CRUD operations
uiAppCRUDWorkflow(uiTestAppName);
uiFolderCRUDWorkflow(uiTestFolderName);
uiWorkspaceConstantCRUDWorkflow(uiTestConstName, uiTestConstValue);
// Enterprise-specific UI workflows
cy.ifEnv("Enterprise", () => {
const uiTestDsName = `${data.appName}_admin_ds`;
const uiTestWorkflowName = `${data.appName}_admin_wf`;
uiDataSourceCRUDWorkflow(uiTestDsName, "restapi");
uiWorkflowCRUDWorkflow(uiTestWorkflowName);
});
});
it("should verify role updates in custom groups", () => {
const builderGroup = fake.firstName.replace(/[^A-Za-z]/g, "");
const endUserGroup = fake.firstName.replace(/[^A-Za-z]/g, "");
@ -66,47 +162,10 @@ describe("Manage Groups", () => {
"builder"
);
// Verify builder permissions
verifyBasicPermissions(true);
// App operations
cy.apiCreateApp(data.appName);
cy.apiDeleteApp();
// Folder operations
cy.apiCreateFolder(data.folderName);
cy.apiDeleteFolder();
// Constants management
cy.get(commonSelectors.workspaceConstantsIcon).click();
addAndVerifyConstants(data.firstName, data.appName);
cy.get(
workspaceConstantsSelectors.constDeleteButton(data.firstName)
).click();
cy.get(commonSelectors.yesButton).click();
verifySettingsAccess(false);
cy.get(commonSelectors.dashboardIcon).click();
cy.apiCreateApp(data.appName);
//verify clone access
cy.reload();
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.cloneAppOption)
);
cy.get(commonSelectors.cloneAppButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
dashboardText.appClonedToast,
false
);
// cy.get(commonSelectors.cancelButton).click();
cy.apiLogout();
cy.apiLogin();
cy.visit(data.workspaceSlug);
cy.apiCreateApp(`${data.appName}_builder`);
navigateToManageGroups();
[builderGroup, endUserGroup].forEach((group) => {
@ -140,43 +199,10 @@ describe("Manage Groups", () => {
cy.apiLogout();
cy.apiLogin(data.email, "password");
cy.visit(data.workspaceSlug);
cy.get(commonSelectors.appCard(data.appName))
cy.get(commonSelectors.appCard(`${data.appName}_builder`))
.trigger("mouseover")
.trigger("mouseenter")
.find(commonSelectors.editButton)
.should("not.exist");
});
it("should verify admin privileges", () => {
setupWorkspaceAndInviteUser(
data.workspaceName,
data.workspaceSlug,
data.firstName,
data.email,
"admin"
);
verifyBasicPermissions(true);
// App operations
cy.apiCreateApp(data.appName);
cy.apiDeleteApp();
// Folder operations
cy.apiCreateFolder(data.folderName);
cy.apiDeleteFolder();
// Constants management
cy.get(commonSelectors.workspaceConstantsIcon).click();
addAndVerifyConstants(data.firstName, data.appName);
cy.get(
workspaceConstantsSelectors.constDeleteButton(data.firstName)
).click();
cy.get(commonSelectors.yesButton).click();
// Settings access check - explicitly verify workspace settings
cy.get(commonSelectors.settingsIcon).click();
cy.get(commonSelectors.workspaceSettings).should("exist");
cy.wait(1000);
});
});

View file

@ -0,0 +1,227 @@
import { fake } from "Fixtures/fake";
import { commonSelectors } from "Selectors/common";
import { dataSourceSelector } from "Selectors/dataSource";
import { groupsSelector } from "Selectors/manageGroups";
import { navigateToManageGroups } from "Support/utils/common";
import {
createGroupsAndAddUserInGroup,
setupWorkspaceAndInviteUser,
} from "Support/utils/manageGroups";
import { getGroupPermissionInput } from "Support/utils/userPermissions";
import { groupsText } from "Texts/manageGroups";
describe("Custom Group Granular Access", () => {
let data = {};
const isEnterprise = Cypress.env("environment") === "Enterprise";
before(() => {
cy.exec("mkdir -p ./cypress/downloads/");
cy.wait(3000);
});
beforeEach(() => {
data = {
firstName: fake.firstName,
appName: fake.companyName,
email: fake.email.toLowerCase().replaceAll("[^A-Za-z]", ""),
workspaceName: fake.lastName.toLowerCase().replace(/[^A-Za-z]/g, ""),
workspaceSlug: fake.lastName.toLowerCase().replace(/[^A-Za-z]/g, ""),
folderName: fake.companyName,
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
};
cy.defaultWorkspaceLogin();
cy.intercept("DELETE", "/api/folders/*").as("folderDeleted");
cy.skipWalkthrough();
cy.viewport(2400, 2000);
});
it("should verify the granular permissions in custom groups", () => {
const groupName = fake.firstName.replace(/[^A-Za-z]/g, "");
const appName2 = fake.companyName;
const appName3 = fake.companyName;
const appSlug = appName3.toLowerCase().replace(/\s+/g, "-");
const workflowName1 = `${data.appName}-workflow`;
const workflowName2 = `${appName2}-workflow`;
const datasourceName1 = `${data.appName}-datasource`;
const datasourceName2 = `${appName2}-datasource`;
setupWorkspaceAndInviteUser(
data.workspaceName,
data.workspaceSlug,
data.firstName,
data.email,
"builder"
);
cy.apiLogin();
cy.visit(data.workspaceSlug);
cy.apiUpdateGroupPermission(
"builder",
getGroupPermissionInput(isEnterprise, false)
);
cy.apiDeleteGranularPermission("builder", []);
navigateToManageGroups();
createGroupsAndAddUserInGroup(groupName, data.email);
cy.apiCreateApp(data.appName);
cy.apiCreateApp(appName2);
// App Hide from dashboard
cy.apiCreateApp(appName3);
cy.openApp();
cy.apiAddComponentToApp(appName3, "text1");
cy.apiPromoteAppVersion().then(() => {
const stagingId = Cypress.env("stagingEnvId");
cy.apiPromoteAppVersion(Cypress.env("stagingEnvId"));
});
cy.apiReleaseApp(appName3);
cy.apiAddAppSlug(appName3, appSlug);
cy.go("back");
// Configure app permissions
navigateToManageGroups();
cy.get(groupsSelector.groupLink(groupName)).click();
cy.get(groupsSelector.granularLink).click();
// Setup permissions for both apps
[data.appName, appName2, appName3].forEach((app) => {
cy.ifEnv("Community", () => {
cy.get(groupsSelector.addAppsButton).click();
});
cy.ifEnv("Enterprise", () => {
cy.get(groupsSelector.addPermissionButton).click();
cy.get(groupsSelector.addAppButton).click();
});
cy.clearAndType(groupsSelector.permissionNameInput, app);
cy.get(groupsSelector.customRadio).check();
cy.get(".css-1gfides").click({ force: true }).type(`${app}{enter}`);
cy.get(groupsSelector.confimButton).click({ force: true });
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.createPermissionToast
);
});
cy.get(groupsSelector.groupChip).contains(data.appName).click();
cy.get(groupsSelector.editPermissionRadio).click();
cy.get(groupsSelector.confimButton).click();
//To hide app
cy.get(groupsSelector.groupChip).contains(appName3).click();
cy.get(groupsSelector.hidePermissionInput).check();
cy.get(groupsSelector.confimButton).click();
cy.ifEnv("Enterprise", () => {
cy.apiCreateWorkflow(workflowName1);
cy.apiCreateWorkflow(workflowName2);
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
datasourceName1,
"restapi",
[{ key: "url", value: "https://jsonplaceholder.typicode.com/users" }]
);
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
datasourceName2,
"restapi",
[{ key: "url", value: "https://jsonplaceholder.typicode.com/users" }]
);
cy.get(groupsSelector.groupLink("Builder")).click();
cy.get(groupsSelector.groupLink(groupName)).click();
cy.get(groupsSelector.granularLink).click();
[workflowName1, workflowName2].forEach((workflow) => {
cy.get(groupsSelector.addPermissionButton).click();
cy.get(groupsSelector.addWorkflowButton).click();
cy.clearAndType(groupsSelector.permissionNameInput, workflow);
cy.get(groupsSelector.customRadio).check();
cy.get(".css-1gfides")
.click({ force: true })
.type(`${workflow}{enter}`);
cy.get(groupsSelector.confimButton).click({ force: true });
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.createPermissionToast
);
});
cy.get(groupsSelector.groupChip).contains(workflowName1).click();
cy.get(groupsSelector.buildWorkflowradio).click();
cy.get(groupsSelector.confimButton).click();
[datasourceName1, datasourceName2].forEach((datasource) => {
cy.get(groupsSelector.addPermissionButton).click();
cy.get(groupsSelector.addDatasourceButton).click();
cy.clearAndType(groupsSelector.permissionNameInput, datasource);
cy.get(groupsSelector.customRadio).check();
cy.get(".css-1gfides")
.click({ force: true })
.type(`${datasource}{enter}`);
cy.get(groupsSelector.confimButton).click({ force: true });
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.createPermissionToast
);
});
cy.get(groupsSelector.groupChip).contains(datasourceName1).click();
cy.get(groupsSelector.configureDatasourceradio).click();
cy.get(groupsSelector.confimButton).click();
});
// Verify as builder
cy.wait(1000);
cy.apiLogout();
cy.apiLogin(data.email);
cy.visit(data.workspaceSlug);
cy.get(commonSelectors.dashboardIcon).click();
cy.get(commonSelectors.appCreateButton).should("not.exist");
cy.get('.appcard-buttons-wrap [data-cy="edit-button"]').should(
"have.lengthOf",
1
);
cy.get('.appcard-buttons-wrap [data-cy="launch-button"]').should(
"have.lengthOf",
2
);
cy.ifEnv("Enterprise", () => {
cy.get(commonSelectors.globalWorkFlowsIcon).click();
cy.get('[data-cy="create-new-workflows-button"]').should("not.exist");
cy.get('.appcard-buttons-wrap [data-cy="edit-button"]').should(
"have.lengthOf",
1
);
cy.get('.appcard-buttons-wrap [data-cy="launch-button"]').should(
"have.lengthOf",
2
);
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.get(
dataSourceSelector.dataSourceNameButton(datasourceName1.toLowerCase())
).click();
cy.get(dataSourceSelector.dsNameInputField).should("be.enabled");
cy.get(
dataSourceSelector.dataSourceNameButton(datasourceName2.toLowerCase())
).click();
cy.get(dataSourceSelector.dsNameInputField).should("be.disabled");
cy.get(dataSourceSelector.commonDsLabelAndCount).click();
cy.get('[data-cy="rest-api-add-button"]').should("be.disabled");
});
//Visit hidden app url
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${appSlug}`,
});
});
});

View file

@ -0,0 +1,206 @@
import { fake } from "Fixtures/fake";
import { commonSelectors } from "Selectors/common";
import { dataSourceSelector } from "Selectors/dataSource";
import { groupsSelector } from "Selectors/manageGroups";
import { navigateToManageGroups } from "Support/utils/common";
import {
createGroupsAndAddUserInGroup,
setupWorkspaceAndInviteUser,
} from "Support/utils/manageGroups";
import {
getGroupPermissionInput,
verifyBuilderPermissions,
} from "Support/utils/userPermissions";
import { groupsText } from "Texts/manageGroups";
describe("Custom Group Permissions", () => {
let data = {};
const isEnterprise = Cypress.env("environment") === "Enterprise";
before(() => {
cy.exec("mkdir -p ./cypress/downloads/");
cy.wait(3000);
});
beforeEach(() => {
data = {
firstName: fake.firstName,
appName: fake.companyName,
email: fake.email.toLowerCase().replaceAll("[^A-Za-z]", ""),
workspaceName: fake.lastName.toLowerCase().replace(/[^A-Za-z]/g, ""),
workspaceSlug: fake.lastName.toLowerCase().replace(/[^A-Za-z]/g, ""),
folderName: fake.companyName,
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
};
cy.defaultWorkspaceLogin();
cy.intercept("DELETE", "/api/folders/*").as("folderDeleted");
cy.skipWalkthrough();
cy.viewport(2400, 2000);
});
it("should verify user permissions in custom groups", () => {
const groupName = fake.firstName.replace(/[^A-Za-z]/g, "");
const workflowName1 = fake.companyName;
const datasourceName1 = fake.companyName
.toLowerCase()
.replace(/[^A-Za-z]/g, "");
setupWorkspaceAndInviteUser(
data.workspaceName,
data.workspaceSlug,
data.firstName,
data.email
);
cy.apiLogout();
// Setup custom group
cy.apiLogin();
cy.visit(data.workspaceSlug);
cy.get(".basic-plan-migration-banner").invoke("css", "display", "none");
cy.apiUpdateGroupPermission(
"builder",
getGroupPermissionInput(isEnterprise, false)
);
cy.visit(data.workspaceSlug);
navigateToManageGroups();
createGroupsAndAddUserInGroup(groupName, data.email);
// Permission configuration and verification
cy.get(groupsSelector.permissionsLink).click();
// App creation permission
cy.get(groupsSelector.appsCreateCheck).check();
cy.get(commonSelectors.defaultModalTitle).contains(
groupsText.changeUserRoleHeader
);
cy.get(groupsSelector.changeRoleModalMessage).contains(
groupsText.changeUserRoleMessage
);
cy.get(".item-list").contains(data.email);
cy.get(groupsSelector.confimButton).should(
"have.text",
groupsText.continueButtonText
);
cy.get(commonSelectors.cancelButton).click();
// Other permissions
const permissions =
Cypress.env("environment") === "Community"
? [
groupsSelector.appsDeleteCheck,
groupsSelector.foldersCreateCheck,
groupsSelector.workspaceVarCheckbox,
]
: [
groupsSelector.appsDeleteCheck,
groupsSelector.appPromoteCheck,
groupsSelector.appReleaseCheck,
groupsSelector.workflowsCreateCheck,
groupsSelector.workflowsDeleteCheck,
groupsSelector.datasourcesCreateCheck,
groupsSelector.datasourcesDeleteCheck,
groupsSelector.foldersCreateCheck,
groupsSelector.workspaceVarCheckbox,
];
permissions.forEach((permission) => {
cy.get(permission).check();
cy.get(".modal-content").should("be.visible");
cy.get(commonSelectors.cancelButton).click();
});
// Granular permissions
cy.get(groupsSelector.granularLink).click();
cy.ifEnv("Community", () => {
cy.get(groupsSelector.addAppsButton).click();
});
cy.ifEnv("Enterprise", () => {
cy.get(groupsSelector.addPermissionButton).click();
cy.get(groupsSelector.addAppButton).click();
});
cy.clearAndType(groupsSelector.permissionNameInput, data.firstName);
cy.get(groupsSelector.editPermissionRadio).click();
cy.get(groupsSelector.confimButton).click();
// Verify modal
cy.get(".modal-content").should("be.visible");
cy.get(groupsSelector.modalHeader).should(
"have.text",
groupsText.cantCreatePermissionModalHeader
);
cy.get(groupsSelector.modalMessage).should(
"have.text",
groupsText.cantCreatePermissionModalMessage
);
cy.get(".item-list").contains(data.email);
cy.get(commonSelectors.closeButton).click();
// Role transition
cy.get(groupsSelector.permissionsLink).click();
cy.get(groupsSelector.appsCreateCheck).check();
cy.get(groupsSelector.confimButton).click();
permissions.forEach((permission) => {
cy.get(permission).check();
});
cy.get(groupsSelector.groupLink("Builder")).click();
cy.get(groupsSelector.usersLink).click();
cy.get(`[data-cy="${data.email}-user-row"]`).should("be.visible");
cy.apiLogout();
cy.apiLogin(data.email);
cy.visit(data.workspaceSlug);
verifyBuilderPermissions(
data.appName,
data.folderName,
data.firstName,
data.appName
);
cy.apiLogout();
cy.apiLogin();
cy.visit(data.workspaceSlug);
// Reset permissions
cy.apiDeleteGranularPermission("builder", []);
cy.apiDeleteGranularPermission(groupName, []);
cy.apiCreateApp(data.appName);
cy.ifEnv("Enterprise", () => {
cy.apiCreateWorkflow(workflowName1);
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
datasourceName1,
"restapi",
[{ key: "url", value: "https://jsonplaceholder.typicode.com/users" }]
);
});
cy.apiLogout();
cy.apiLogin(data.email);
cy.visit(data.workspaceSlug);
cy.get(commonSelectors.appCreateButton).should("be.enabled");
cy.get(commonSelectors.appCard(data.appName)).should("not.exist");
cy.ifEnv("Enterprise", () => {
cy.get(commonSelectors.globalWorkFlowsIcon).click();
cy.get('[data-cy="create-new-workflows-button"]').should("exist");
cy.get(commonSelectors.appCard(workflowName1)).should("not.exist");
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.get(dataSourceSelector.dataSourceNameButton(datasourceName1)).should(
"exist"
);
cy.get(dataSourceSelector.commonDsLabelAndCount).click();
cy.get('[data-cy="rest-api-add-button"]').should("be.enabled");
});
});
});

View file

@ -0,0 +1,273 @@
import { fake } from "Fixtures/fake";
import { commonSelectors } from "Selectors/common";
import { commonEeSelectors } from "Selectors/eeCommon";
import { groupsSelector } from "Selectors/manageGroups";
import { navigateToManageGroups } from "Support/utils/common";
import {
apiAddUserToGroup,
apiCreateGroup,
apiDeleteGroup,
} from "Support/utils/manageGroups";
import {
addGranularPermissionViaUI,
createGroupViaUI,
deleteGroupViaUI,
openGroupThreeDotMenu,
renameGroupViaUI,
switchBetweenAllAndCustom,
verifyDuplicateModal,
verifyGroupCreatedInSidebar,
verifyGroupRemovedFromSidebar,
} from "Support/utils/platform/customGroups";
import {
verifyCheckPermissionStates,
verifyEmptyStates,
verifyGranularAccessByRole,
verifyGranularEditModal,
verifyGranularPermissionModalStates,
verifyGroupLinks,
verifyPermissionCheckBoxLabelsAndHelperTexts,
verifyUserRow,
} from "Support/utils/platform/groupsUI";
import { getGroupPermissionInput } from "Support/utils/userPermissions";
describe("Custom groups UI and Functionality verification", () => {
const isEnterprise = Cypress.env("environment") === "Enterprise";
const groupName = fake.firstName.replaceAll("[^A-Za-z]", "");
const newGroupname = `New ${groupName}`;
const groupName2 = `${fake.firstName.replaceAll("[^A-Za-z]", "")}2`;
const groupName3 = `${fake.firstName.replaceAll("[^A-Za-z]", "")}3`;
const duplicatedGroupName = `${groupName3}_copy`;
const appPermissionName = "Apps";
const workflowPermissionName = "Workflows";
const datasourcePermissionName = " Data sources";
const permissionName2 = "Permission2";
const data = {
firstName: fake.firstName,
email: fake.email.toLowerCase().replaceAll("[^A-Za-z]", ""),
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replaceAll("[^A-Za-z]", ""),
};
let groupId3;
before(() => {
cy.apiLogin();
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
});
beforeEach(() => {
cy.apiLogin();
cy.visit(`${data.workspaceSlug}`);
cy.viewport(2000, 1900);
});
it("should create, rename, and delete a custom group", () => {
navigateToManageGroups();
createGroupViaUI(groupName);
verifyGroupCreatedInSidebar(groupName);
renameGroupViaUI(groupName, newGroupname);
verifyGroupCreatedInSidebar(newGroupname);
deleteGroupViaUI(newGroupname);
verifyGroupRemovedFromSidebar(newGroupname);
});
it("should create custom group, verify empty states, add permissions, and manage granular access", () => {
apiCreateGroup(groupName2);
cy.apiCreateApp(groupName);
cy.ifEnv("Enterprise", () => {
cy.apiCreateWorkflow(groupName);
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
groupName,
"restapi",
[{ key: "url", value: "https://jsonplaceholder.typicode.com/users" }]
);
});
navigateToManageGroups();
cy.get(groupsSelector.groupLink(groupName2)).click();
verifyGroupLinks();
verifyEmptyStates(true);
cy.get(groupsSelector.permissionsLink).click();
verifyCheckPermissionStates("custom");
verifyPermissionCheckBoxLabelsAndHelperTexts();
cy.get(groupsSelector.granularLink).click();
addGranularPermissionViaUI(appPermissionName, {
resourceType: "app",
permission: "edit",
scope: "all",
});
cy.ifEnv("Enterprise", () => {
addGranularPermissionViaUI(workflowPermissionName, {
resourceType: "workflow",
permission: "build",
scope: "all",
});
addGranularPermissionViaUI(datasourcePermissionName, {
resourceType: "datasource",
permission: "configure",
scope: "all",
});
});
verifyGranularAccessByRole("builder");
verifyGranularEditModal("custom");
cy.get('[data-cy="apps-granular-access"]').realHover();
cy.get('[data-cy="edit-apps-granular-access"]').click();
verifyGranularPermissionModalStates("app", "custom");
cy.get(groupsSelector.viewPermissionRadio).check();
switchBetweenAllAndCustom("custom");
verifyGranularPermissionModalStates("app", "custom", {
editRadio: { checked: false, enabled: true },
viewRadio: { checked: true, enabled: true },
hideCheckbox: { enabled: true },
allAppsRadio: { checked: false, enabled: true },
customRadio: { checked: true, enabled: true },
});
switchBetweenAllAndCustom("all");
verifyGranularPermissionModalStates("app", "custom", {
editRadio: { checked: false, enabled: true },
viewRadio: { checked: true, enabled: true },
hideCheckbox: { enabled: true },
allAppsRadio: { checked: true, enabled: true },
customRadio: { checked: false, enabled: true },
});
cy.get(groupsSelector.editPermissionRadio).check();
verifyGranularPermissionModalStates("app", "custom", {
editRadio: { checked: true, enabled: true },
viewRadio: { checked: false, enabled: true },
hideCheckbox: { enabled: false },
allAppsRadio: { checked: true, enabled: true },
customRadio: { checked: false, enabled: true },
});
cy.get(groupsSelector.cancelButton).click();
cy.ifEnv("Enterprise", () => {
cy.get('[data-cy="workflow-granular-access"]').realHover();
cy.get('[data-cy="edit-workflow-granular-access"]').click();
verifyGranularPermissionModalStates("workflow", "custom");
cy.get(groupsSelector.executeWorkflowradio).check();
verifyGranularPermissionModalStates("workflow", "custom", {
buildRadio: { checked: false, enabled: true },
executeRadio: { checked: true, enabled: true },
});
cy.get(groupsSelector.cancelButton).click();
cy.get('[data-cy="datasource-granular-access"]').realHover();
cy.get('[data-cy="edit-datasource-granular-access"]').click();
verifyGranularPermissionModalStates("datasource", "custom");
cy.get(groupsSelector.buildWithDatasourceRadio).check();
verifyGranularPermissionModalStates("datasource", "custom", {
configureRadio: { checked: false, enabled: true },
buildWithRadio: { checked: true, enabled: true },
});
cy.get(groupsSelector.cancelButton).click();
});
apiDeleteGroup(groupName2);
});
it("should create group via API, add permissions, duplicate group and verify all permissions are copied", () => {
cy.apiFullUserOnboarding(data.firstName, data.email, "builder");
cy.apiLogout();
cy.apiLogin();
cy.apiCreateApp(groupName3);
cy.ifEnv("Enterprise", () => {
cy.apiCreateWorkflow(groupName3);
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
groupName3,
"restapi",
[{ key: "url", value: "https://jsonplaceholder.typicode.com/users" }]
);
});
apiCreateGroup(groupName3).then((groupId) => {
groupId3 = groupId;
apiAddUserToGroup(groupId3, data.email);
cy.apiCreateGranularPermission(
groupName3,
"Apps",
"app",
{ canEdit: true, canView: false, hideFromDashboard: false },
[]
);
cy.ifEnv("Enterprise", () => {
cy.apiCreateGranularPermission(
groupName3,
"Workflows",
"workflow",
{ canEdit: true, canView: false, hideFromDashboard: false },
[]
);
cy.apiCreateGranularPermission(
groupName3,
"Data sources",
"datasource",
{ canUse: false, canConfigure: true },
[]
);
});
});
cy.apiUpdateGroupPermission(
groupName3,
getGroupPermissionInput(isEnterprise, true)
);
navigateToManageGroups();
cy.wait(2000);
openGroupThreeDotMenu(groupName3);
cy.get(groupsSelector.duplicateOption).click();
verifyDuplicateModal(groupName3);
cy.get(groupsSelector.cancelButton).click();
openGroupThreeDotMenu(groupName3);
cy.get(groupsSelector.duplicateOption).click();
cy.get(commonEeSelectors.confirmButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Group duplicated successfully"
);
cy.wait(1000);
verifyUserRow(data.firstName, ` ${data.email}`);
cy.get(groupsSelector.groupLink(duplicatedGroupName)).click();
cy.get(groupsSelector.permissionsLink).click();
verifyCheckPermissionStates("builder");
cy.get(groupsSelector.granularLink).click();
verifyGranularAccessByRole("builder");
apiDeleteGroup(duplicatedGroupName);
apiDeleteGroup(groupName3);
});
});

View file

@ -1,264 +0,0 @@
import { fake } from "Fixtures/fake";
import { commonSelectors } from "Selectors/common";
import { groupsSelector } from "Selectors/manageGroups";
import * as common from "Support/utils/common";
import * as groups from "Support/utils/manageGroups";
import { groupsText } from "Texts/manageGroups";
const verifyModalFields = (isEdit = false, groupName = '') => {
// Modal header verification
cy.get(groupsSelector.permissionNameLabel).verifyVisibleElement(
"have.text",
groupsText.permissionNameLabel
);
cy.get(groupsSelector.permissionNameInput)
.should("be.visible")
.and(isEdit ? "have.value" : "have.attr",
isEdit ? groupName : "placeholder",
isEdit ? groupName : "Eg. Product analytics apps");
cy.get(groupsSelector.permissionNameHelperText).verifyVisibleElement(
"have.text",
groupsText.permissionNameHelperText
);
// Permission section verification
const permissionChecks = [
{ selector: groupsSelector.permissionLabel, text: groupsText.permissionLabel },
{ selector: groupsSelector.editPermissionLabel, text: groupsText.editPermissionLabel },
{ selector: groupsSelector.editPermissionHelperText, text: groupsText.appEditHelperText },
{ selector: groupsSelector.viewPermissionLabel, text: groupsText.viewPermissionLabel },
{ selector: groupsSelector.viewPermissionHelperText, text: groupsText.appViewHelperText }
];
permissionChecks.forEach(({ selector, text }) => {
cy.get(selector).verifyVisibleElement("have.text", text);
});
// Hide permission verification
cy.get(groupsSelector.hidePermissionInput).should("be.visible");
cy.get(groupsSelector.appHidePermissionModalLabel).verifyVisibleElement(
"have.text",
groupsText.appHideLabel
);
cy.get(groupsSelector.appHidePermissionModalHelperText).verifyVisibleElement(
"have.text",
groupsText.appHideHelperText
);
// Resource section verification
const resourceChecks = [
{ selector: groupsSelector.resourceLabel, text: groupsText.resourcesheader },
{ selector: groupsSelector.allAppsLabel, text: isEdit ? groupsText.allAppsLabel : groupsText.groupChipText },
{ selector: groupsSelector.allAppsHelperText, text: groupsText.allAppsHelperText }
];
resourceChecks.forEach(({ selector, text }) => {
cy.get(selector).verifyVisibleElement("have.text", text);
});
// Button states
cy.get(groupsSelector.confimButton).verifyVisibleElement(
"have.text",
isEdit ? groupsText.updateButtonText : groupsText.addButtonText
);
cy.get(groupsSelector.confimButton).should(isEdit ? "be.enabled" : "be.disabled");
cy.get(groupsSelector.cancelButton).verifyVisibleElement("have.text", groupsText.cancelButton);
if (isEdit) {
cy.get(groupsSelector.deletePermissionIcon).should("be.visible");
}
};
const verifyPermissionSection = () => {
const permissionElements = [
{
resource: groupsSelector.resourcesApps,
resourceText: groupsText.resourcesApps,
check: groupsSelector.appsCreateCheck,
label: groupsSelector.appsCreateLabel,
labelText: groupsText.createLabel,
helperText: groupsSelector.appCreateHelperText,
helperContent: groupsText.appCreateHelperText
},
{
resource: groupsSelector.resourcesFolders,
resourceText: groupsText.resourcesFolders,
check: groupsSelector.foldersCreateCheck,
label: groupsSelector.foldersCreateLabel,
labelText: groupsText.folderCreateLabel,
helperText: groupsSelector.foldersHelperText,
helperContent: groupsText.folderHelperText
}
];
permissionElements.forEach(({ resource, resourceText, check, label, labelText, helperText, helperContent }) => {
cy.get(resource).verifyVisibleElement("have.text", resourceText);
cy.get(check).should("be.visible");
cy.get(check).check();
cy.verifyToastMessage(commonSelectors.toastMessage, groupsText.permissionUpdatedToast);
cy.get(check).uncheck();
cy.verifyToastMessage(commonSelectors.toastMessage, groupsText.permissionUpdatedToast);
cy.get(label).verifyVisibleElement("have.text", labelText);
cy.get(helperText).verifyVisibleElement("have.text", helperContent);
});
};
const verifyEmptyStates = () => {
// Users empty state
cy.get(groupsSelector.userEmptyPageIcon).should("be.visible");
cy.get(groupsSelector.userEmptyPageTitle).verifyVisibleElement("have.text", groupsText.userEmptyPageTitle);
cy.get(groupsSelector.userEmptyPageHelperText).verifyVisibleElement("have.text", groupsText.userEmptyPageHelperText);
// Granular permissions empty state
cy.get(groupsSelector.granularLink).click();
cy.get(groupsSelector.granularEmptyPageIcon).should("be.visible");
cy.get(groupsSelector.emptyPagePermissionTitle).verifyVisibleElement("have.text", groupsText.emptyPagePermissionTitle);
cy.get(groupsSelector.emptyPagePermissionHelperText).verifyVisibleElement("have.text", groupsText.emptyPagePermissionHelperText);
};
const verifyGroupLinks = () => {
const links = [
{ selector: groupsSelector.usersLink, text: groupsText.usersLink },
{ selector: groupsSelector.permissionsLink, text: groupsText.permissionsLink },
{ selector: groupsSelector.granularLink, text: "Granular access" }
];
links.forEach(({ selector, text }) => {
cy.get(selector).verifyVisibleElement("have.text", text);
});
};
describe("Manage Groups", () => {
const groupName = fake.firstName.replaceAll("[^A-Za-z]", "");
const newGroupname = `New ${groupName}`;
const data = {
firstName: fake.firstName,
email: fake.email.toLowerCase().replaceAll("[^A-Za-z]", ""),
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replaceAll("[^A-Za-z]", "")
};
beforeEach(() => {
cy.defaultWorkspaceLogin();
});
it("Should verify the elements and functionalities on manage groups page", () => {
// Setup workspace
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
cy.visit(`${data.workspaceSlug}`);
common.navigateToManageGroups();
// Verify page headers
cy.get(commonSelectors.breadcrumbTitle).should(($el) => {
expect($el.contents().first().text().trim()).to.eq("Workspace settings");
});
cy.get(commonSelectors.breadcrumbPageTitle).verifyVisibleElement("have.text", "Groups");
// Verify base group elements
groups.manageGroupsElements();
// Test group creation flow
const verifyGroupCreation = () => {
cy.get(groupsSelector.createNewGroupButton).should("be.visible").click();
cy.get(groupsSelector.addNewGroupModalTitle).verifyVisibleElement("have.text", groupsText.cardTitle);
cy.get(groupsSelector.groupNameInput).should("be.visible");
cy.get(groupsSelector.cancelButton).verifyVisibleElement("have.text", groupsText.cancelButton);
cy.get(groupsSelector.createGroupButton).verifyVisibleElement("have.text", groupsText.createGroupButton);
cy.get(groupsSelector.cancelButton).click();
};
// Test duplicate group name
const testDuplicateGroup = () => {
cy.get(groupsSelector.createNewGroupButton).click();
cy.clearAndType(groupsSelector.groupNameInput, groupsText.admin);
cy.get(groupsSelector.createGroupButton).click();
cy.verifyToastMessage(commonSelectors.toastMessage, groupsText.groupNameExistToast);
cy.get(groupsSelector.cancelButton).click();
cy.get(groupsSelector.groupNameInput).should("not.exist");
};
// Create and verify new group
const createNewGroup = () => {
cy.get(groupsSelector.createNewGroupButton).click();
cy.clearAndType(groupsSelector.groupNameInput, groupName);
cy.get(groupsSelector.createGroupButton).click();
cy.verifyToastMessage(commonSelectors.toastMessage, groupsText.groupCreatedToast);
// Verify group creation
cy.get(groupsSelector.groupLink(groupName)).verifyVisibleElement("have.text", groupName);
cy.get(groupsSelector.groupLink(groupName)).click();
cy.get(groupsSelector.groupPageTitle(groupName)).verifyVisibleElement("have.text", `${groupName} (0)`);
cy.get(groupsSelector.groupNameUpdateLink).should("be.visible");
// Verify group links and tables
verifyGroupLinks();
cy.get(groupsSelector.usersLink).click();
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement("have.text", groupsText.userNameTableHeader);
cy.get(groupsSelector.emailTableHeader).verifyVisibleElement("have.text", groupsText.emailTableHeader);
verifyEmptyStates();
};
verifyGroupCreation();
testDuplicateGroup();
createNewGroup();
cy.get(groupsSelector.permissionsLink).click();
verifyPermissionSection();
// Test granular access section
const verifyGranularAccess = () => {
cy.get(groupsSelector.granularLink).click();
cy.get(groupsSelector.addAppButton).click();
verifyModalFields();
// Add permission
cy.clearAndType(groupsSelector.permissionNameInput, groupName);
cy.get(groupsSelector.confimButton).click();
// Verify edit mode
cy.get(`[data-cy="${groupName.toLowerCase()}-text"]`).click();
cy.get(`${groupsSelector.addEditPermissionModalTitle}:eq(2)`)
.verifyVisibleElement("have.text", groupsText.editPermissionModalTitle);
cy.get(groupsSelector.editPermissionRadio).check();
verifyModalFields(true, groupName);
cy.get(groupsSelector.confimButton).click();
};
verifyGranularAccess();
// Test group rename and delete
const verifyGroupRename = () => {
cy.get(groupsSelector.groupNameUpdateLink).click();
cy.get(groupsSelector.updateGroupNameModalTitle)
.verifyVisibleElement("have.text", groupsText.updateGroupNameModalTitle);
cy.clearAndType(groupsSelector.groupNameInput, newGroupname);
cy.get(groupsSelector.createGroupButton).click();
cy.verifyToastMessage(commonSelectors.toastMessage, groupsText.groupNameUpdateSucessToast);
cy.get(groupsSelector.groupLink(newGroupname))
.verifyVisibleElement("have.text", newGroupname);
};
const verifyGroupDelete = () => {
cy.get(groupsSelector.groupLink(newGroupname)).click();
groups.OpenGroupCardOption(newGroupname);
cy.get(groupsSelector.deleteGroupOption).click();
// Verify delete confirmation
cy.get(groupsSelector.confirmText)
.verifyVisibleElement("have.text", groupsText.confirmText);
cy.get(commonSelectors.buttonSelector("Cancel"))
.verifyVisibleElement("have.text", groupsText.confirmCancelButton);
cy.get(commonSelectors.buttonSelector("Yes"))
.verifyVisibleElement("have.text", groupsText.confirmYesButton);
cy.get(commonSelectors.buttonSelector("Cancel")).click();
// Actual delete
groups.OpenGroupCardOption(newGroupname);
cy.get(groupsSelector.deleteGroupOption).click();
cy.get(commonSelectors.buttonSelector("Yes")).click();
};
verifyGroupRename();
verifyGroupDelete();
});
});

View file

@ -1,37 +1,30 @@
import { fake } from "Fixtures/fake";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { commonSelectors } from "Selectors/common";
import {
exportAppModalSelectors,
importSelectors,
} from "Selectors/exportImport";
import { groupsSelector } from "Selectors/manageGroups";
import { workspaceConstantsSelectors } from "Selectors/workspaceConstants";
import {
createFolder,
deleteFolder,
logout,
navigateToManageGroups,
navigateToManageUsers,
releaseApp,
selectAppCardOption,
} from "Support/utils/common";
import {
createGroupsAndAddUserInGroup,
inviteUserBasedOnRole,
setupAndUpdateRole,
setupWorkspaceAndInviteUser,
updateRole,
verifyBasicPermissions,
verifySettingsAccess,
verifyUserPrivileges,
} from "Support/utils/manageGroups";
import { addAndVerifyConstants } from "Support/utils/workspaceConstants";
import { commonText } from "Texts/common";
import { exportAppModalText, importText } from "Texts/exportImport";
import { groupsText } from "Texts/manageGroups";
describe("Manage Groups", () => {
let data = {};
const isEnterprise = Cypress.env("environment") === "Enterprise";
before(() => {
cy.exec("mkdir -p ./cypress/downloads/");
@ -52,6 +45,7 @@ describe("Manage Groups", () => {
cy.defaultWorkspaceLogin();
cy.intercept("DELETE", "/api/folders/*").as("folderDeleted");
cy.skipWalkthrough();
cy.viewport(2400, 2000);
});
it("should verify the last active admin role update protection", () => {
@ -89,215 +83,6 @@ describe("Manage Groups", () => {
cy.get(commonSelectors.closeButton).click();
});
it("should verify user privileges in custom groups", () => {
const groupName = fake.firstName.replace(/[^A-Za-z]/g, "");
const appName2 = fake.companyName;
const appName3 = fake.companyName;
const appSlug = appName3.toLowerCase().replace(/\s+/g, "-");
setupWorkspaceAndInviteUser(
data.workspaceName,
data.workspaceSlug,
data.firstName,
data.email
);
cy.apiLogout();
// Setup custom group
cy.apiLogin();
cy.visit(data.workspaceSlug);
navigateToManageGroups();
cy.get(groupsSelector.groupLink("Builder")).click();
cy.get(groupsSelector.permissionsLink).click();
cy.get(groupsSelector.appsCreateCheck).uncheck();
cy.get(groupsSelector.appsDeleteCheck).uncheck();
cy.get(groupsSelector.foldersCreateCheck).uncheck();
cy.get(groupsSelector.workspaceVarCheckbox).uncheck();
createGroupsAndAddUserInGroup(groupName, data.email);
// Permission configuration and verification
cy.get(groupsSelector.permissionsLink).click();
// App creation permission
cy.get(groupsSelector.appsCreateCheck).check();
cy.get(commonSelectors.defaultModalTitle).contains(
groupsText.changeUserRoleHeader
);
cy.get(groupsSelector.changeRoleModalMessage).contains(
groupsText.changeUserRoleMessage
);
cy.get(".item-list").contains(data.email);
cy.get(groupsSelector.confimButton).should(
"have.text",
groupsText.continueButtonText
);
cy.get(commonSelectors.cancelButton).click();
// Other permissions
const permissions = [
groupsSelector.appsDeleteCheck,
groupsSelector.foldersCreateCheck,
groupsSelector.workspaceVarCheckbox,
];
permissions.forEach((permission) => {
cy.get(permission).check();
cy.get(".modal-content").should("be.visible");
cy.get(commonSelectors.cancelButton).click();
});
// Granular permissions
cy.get(groupsSelector.granularLink).click();
cy.get(groupsSelector.addAppButton).click();
cy.clearAndType(groupsSelector.permissionNameInput, data.firstName);
cy.get(groupsSelector.editPermissionRadio).click();
cy.get(groupsSelector.confimButton).click();
// Verify modal
cy.get(".modal-content").should("be.visible");
cy.get(groupsSelector.modalHeader).should(
"have.text",
groupsText.cantCreatePermissionModalHeader
);
cy.get(groupsSelector.modalMessage).should(
"have.text",
groupsText.cantCreatePermissionModalMessage
);
cy.get(".item-list").contains(data.email);
cy.get(commonSelectors.closeButton).click();
// Role transition
cy.get(groupsSelector.permissionsLink).click();
cy.get(groupsSelector.appsCreateCheck).check();
cy.get(groupsSelector.confimButton).click();
permissions.forEach((permission) => {
cy.get(permission).check();
});
cy.get(groupsSelector.groupLink("Builder")).click();
cy.get(groupsSelector.usersLink).click();
cy.get(`[data-cy="${data.email}-user-row"]`).should("be.visible");
cy.apiLogout();
cy.apiLogin(data.email);
cy.visit(data.workspaceSlug);
// Verify builder permissions
verifyBasicPermissions(true);
// App operations
cy.createApp(data.appName);
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// commonText.appCreatedToast
// );
cy.backToApps();
cy.wait(2500);
cy.deleteApp(data.appName);
// Folder operations
createFolder(data.folderName);
deleteFolder(data.folderName);
// Constants management
cy.get(commonSelectors.workspaceConstantsIcon).click();
addAndVerifyConstants(data.firstName, data.appName);
cy.get(
workspaceConstantsSelectors.constDeleteButton(data.firstName)
).click();
cy.get(commonSelectors.yesButton).click();
verifySettingsAccess(false);
cy.get(commonSelectors.settingsIcon).click();
cy.wait(500);
cy.apiLogout();
cy.apiLogin();
cy.visit(data.workspaceSlug);
navigateToManageGroups();
cy.get(groupsSelector.groupLink(groupName)).click();
cy.get(groupsSelector.permissionsLink).click();
cy.get(groupsSelector.appsCreateCheck).uncheck();
permissions.forEach((permission) => {
cy.get(permission).uncheck();
});
cy.wait(2000);
cy.get(groupsSelector.groupLink("Builder")).click();
cy.get(groupsSelector.granularLink).click();
cy.wait(2000);
cy.get(groupsSelector.granularAccessPermission)
.trigger("mouseenter")
.click({ force: true });
cy.get(groupsSelector.deletePermissionIcon).click();
cy.get(groupsSelector.yesButton).click();
// Create test apps
cy.get(commonSelectors.homePageLogo).click();
cy.apiCreateApp(data.appName);
cy.apiCreateApp(appName2);
// App Hide from dashboard
cy.apiCreateApp(appName3);
cy.openApp();
cy.apiAddComponentToApp(appName3, "text1");
releaseApp();
cy.get(commonWidgetSelector.shareAppButton).click();
cy.clearAndType(commonWidgetSelector.appNameSlugInput, `${appSlug}`);
cy.wait(500);
cy.get(commonWidgetSelector.modalCloseButton).click();
cy.backToApps();
// Configure app permissions
navigateToManageGroups();
cy.get(groupsSelector.groupLink(groupName)).click();
cy.get(groupsSelector.granularLink).click();
// Setup permissions for both apps
[data.appName, appName2, appName3].forEach((app) => {
cy.get(groupsSelector.addAppButton).click();
cy.clearAndType(groupsSelector.permissionNameInput, app);
cy.get(groupsSelector.customradio).check();
cy.get(".css-1gfides").click({ force: true }).type(`${app}{enter}`);
cy.get(groupsSelector.confimButton).click({ force: true });
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.createPermissionToast
);
});
cy.get(groupsSelector.groupChip).contains(data.appName).click();
cy.get(groupsSelector.editPermissionRadio).click();
cy.get(groupsSelector.confimButton).click();
//To hide app
cy.get(groupsSelector.groupChip).contains(appName3).click();
cy.get(groupsSelector.hidePermissionInput).check();
cy.get(groupsSelector.confimButton).click();
// Verify as end user
cy.wait(1000);
cy.apiLogout()
cy.apiLogin(data.email);
cy.visit(data.workspaceSlug);
cy.get('.appcard-buttons-wrap [data-cy="edit-button"]').should(
"have.lengthOf",
1
);
cy.get('.appcard-buttons-wrap [data-cy="launch-button"]').should(
"have.lengthOf",
2
);
//Visit hidden app url
cy.visitSlug({
actualUrl: `${Cypress.config("baseUrl")}/applications/${appSlug}`,
});
});
it("should verify user role updating sequence", () => {
const roleUpdateSequence = [
{
@ -407,7 +192,7 @@ describe("Manage Groups", () => {
});
});
it("should verify query creation and import access for Builders and Admin", () => {
it.skip("should verify query creation and import access for Builders and Admin", () => {
const firstName2 = fake.firstName;
const email2 = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
const workspaceName2 = fake.firstName
@ -454,7 +239,7 @@ describe("Manage Groups", () => {
cy.wait(500);
cy.apiCreateGDS(
`${Cypress.env('server_host')}/api/data-sources`,
`${Cypress.env("server_host")}/api/data-sources`,
`cypress-${data.dsName}-qc-postgresql`,
"postgresql",
[
@ -479,21 +264,21 @@ describe("Manage Groups", () => {
//Create and run postgres query in the app
// Need to enable once bug is fixed
/*
addQuery(
"table_preview",
`SELECT * FROM persons;`,
`cypress-${data.dsName1}-postgresql`
);
cy.get('[data-cy="list-query-table_preview"]').verifyVisibleElement(
"have.text",
"table_preview "
);
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
verifyValueOnInspector("table_preview", "7 items ");
*/
addQuery(
"table_preview",
`SELECT * FROM persons;`,
`cypress-${data.dsName1}-postgresql`
);
cy.get('[data-cy="list-query-table_preview"]').verifyVisibleElement(
"have.text",
"table_preview "
);
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
verifyValueOnInspector("table_preview", "7 items ");
*/
cy.backToApps();

View file

@ -0,0 +1,202 @@
import { fake } from "Fixtures/fake";
import { commonSelectors } from "Selectors/common";
import { groupsSelector } from "Selectors/manageGroups";
import { navigateToManageGroups } from "Support/utils/common";
import {
commonGroupVerification,
toggleAllPermissions,
verifyAdminHelperText,
verifyCheckPermissionStates,
verifyEditUserRoleModal,
verifyEmptyStates,
verifyEnduserHelperText,
verifyGranularAccessByRole,
verifyGranularAddModal,
verifyGranularEditModal,
verifyPermissionCheckBoxLabelsAndHelperTexts,
verifyUserRow,
} from "Support/utils/platform/groupsUI";
import { groupsText } from "Texts/manageGroups";
describe("User Role UI and Functionality verification", () => {
const data = {
firstName: fake.firstName,
email: fake.email.toLowerCase().replaceAll("[^A-Za-z]", ""),
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replaceAll("[^A-Za-z]", ""),
};
before(() => {
cy.apiLogin();
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
});
beforeEach(() => {
cy.apiLogin();
cy.visit(`${data.workspaceSlug}`);
navigateToManageGroups();
cy.viewport(2000, 1900);
});
it("should verify admin role UI elements and interactions", () => {
// Verify page headers
cy.get(commonSelectors.breadcrumbTitle).should(($el) => {
expect($el.contents().first().text().trim()).to.eq("Workspace settings");
});
cy.get(commonSelectors.breadcrumbPageTitle).verifyVisibleElement(
"have.text",
"Groups"
);
cy.get('[data-cy="page-title"]').should(($el) => {
expect($el.contents().last().text().trim()).to.eq("Groups");
});
cy.verifyElement(
groupsSelector.createNewGroupButton,
groupsText.createNewGroupButton
);
cy.get('[data-cy="user-role-title"]').verifyVisibleElement(
"have.text",
"USER ROLE"
);
cy.verifyElement('[data-cy="custom-groups-title"]', "CUSTOM GROUPS");
cy.get('[data-cy="create-group-button-icon"]').should("be.visible");
cy.get('[data-cy="search-icon"]').should("be.visible");
// Admin List Item Verification
cy.verifyElement(groupsSelector.adminListItem, "Admin");
cy.verifyElement(groupsSelector.adminTitle, "Admin (1)");
cy.verifyElement(
groupsSelector.textDefaultGroup,
groupsText.textDefaultGroup
);
// Verify tabs visibility
cy.get(groupsSelector.usersLink).should("be.visible");
cy.get(groupsSelector.permissionsLink).should("be.visible");
cy.get(groupsSelector.granularLink).should("be.visible");
commonGroupVerification();
// Users Tab Verification
cy.get(groupsSelector.usersLink).click();
cy.get('[data-cy="user-group-search-btn"]').should("be.visible");
cy.verifyElement(
groupsSelector.nameTableHeader,
groupsText.userNameTableHeader
);
cy.verifyElement(
groupsSelector.emailTableHeader,
groupsText.emailTableHeader
);
verifyUserRow("The Developer", "dev@tooljet.io");
cy.get('[data-cy="edit-role-button"]')
.should("be.visible")
.and("be.enabled");
cy.get('[data-cy="edit-role-button"]').click();
verifyEditUserRoleModal("dev@tooljet.io");
cy.get(groupsSelector.cancelButton).click();
// Permissions Tab Verification
cy.get(groupsSelector.permissionsLink).click();
verifyAdminHelperText(0);
verifyCheckPermissionStates("admin");
verifyPermissionCheckBoxLabelsAndHelperTexts();
// Granular Access Tab Verification
verifyGranularAccessByRole("admin");
verifyAdminHelperText(1);
});
it("should verify builder role UI elements and interactions", () => {
// Builder Group Navigation and Title Verification
cy.get(groupsSelector.groupLink("Builder")).click();
cy.verifyElement(groupsSelector.builderListItem, "Builder");
cy.verifyElement(groupsSelector.builderTitle, "Builder (0)");
cy.verifyElement(
groupsSelector.textDefaultGroup,
groupsText.textDefaultGroup
);
// Verify tabs visibility
cy.get(groupsSelector.usersLink).should("be.visible");
cy.get(groupsSelector.permissionsLink).should("be.visible");
cy.get(groupsSelector.granularLink).should("be.visible");
// Users Tab Verification - Empty State
verifyEmptyStates();
// Permissions Tab Verification
cy.get(groupsSelector.permissionsLink).click();
verifyCheckPermissionStates("builder");
verifyPermissionCheckBoxLabelsAndHelperTexts();
toggleAllPermissions();
// Granular Access Tab Verification
verifyGranularAccessByRole("builder");
cy.verifyElement(
groupsSelector.appHideLabel,
groupsText.appHideLabelPermissionModal
);
// Edit Modal Verification
verifyGranularEditModal("builder");
// Add Modal Verification
verifyGranularAddModal("builder");
});
it("should verify enduser role UI and interaction", () => {
// End-user Group Navigation and Title Verification
cy.get(groupsSelector.groupLink("End-user")).click();
cy.get(groupsSelector.groupLink("End-user")).verifyVisibleElement(
"have.text",
"End-user"
);
cy.get(groupsSelector.enduserTitle).verifyVisibleElement(
"have.text",
"End-user (0)"
);
cy.verifyElement(
groupsSelector.textDefaultGroup,
groupsText.textDefaultGroup
);
// Verify tabs visibility
cy.get(groupsSelector.usersLink).should("be.visible");
cy.get(groupsSelector.permissionsLink).should("be.visible");
cy.get(groupsSelector.granularLink).should("be.visible");
// Users Tab Verification - Empty State
verifyEmptyStates();
// Permissions Tab Verification
cy.get(groupsSelector.permissionsLink).click();
verifyEnduserHelperText(0);
verifyCheckPermissionStates("enduser");
verifyPermissionCheckBoxLabelsAndHelperTexts();
// Granular Access Tab Verification
cy.reload();
cy.wait(2000);
cy.get(groupsSelector.groupLink("End-user")).click();
verifyGranularAccessByRole("enduser");
verifyEnduserHelperText(1);
cy.verifyElement(
groupsSelector.appHideLabel,
groupsText.appHideLabelPermissionModal
);
// Edit Modal Verification
verifyGranularEditModal("enduser");
// Add Modal Verification
verifyGranularAddModal("enduser");
});
});

View file

@ -0,0 +1,113 @@
import { fake } from "Fixtures/fake";
import { commonSelectors } from "Selectors/common";
import { dashboardSelector } from "Selectors/dashboard";
import { commonText } from "Texts/common";
import { dashboardText } from "Texts/dashboard";
describe("Home Page Dashboard Testcases", () => {
let data = {};
const isEnterprise = Cypress.env("environment") === "Enterprise";
before(function () {
if (Cypress.env("environment") === "Community") {
this.skip();
}
});
beforeEach(() => {
data = {
firstName: fake.firstName,
email: fake.email.toLowerCase().replaceAll("[^A-Za-z]", ""),
};
cy.intercept("GET", "/api/library_apps").as("appLibrary");
cy.apiLogin();
cy.visit("/");
});
it("Should verify elements on home page dashboard", () => {
cy.get(commonSelectors.homePageIcon).click();
cy.get(commonSelectors.breadcrumbHeaderTitle).should(($el) => {
expect($el.contents().first().text().trim()).to.eq(
commonText.breadcrumbHome
);
});
cy.get(commonSelectors.breadcrumbPageTitle).verifyVisibleElement(
"have.text",
commonText.breadcrumbHome
);
cy.get(dashboardSelector.aiIcon).should("be.visible");
cy.get(dashboardSelector.homePagePromptHeader).verifyVisibleElement(
"have.text",
dashboardText.homePagePromptHeader
);
cy.get(dashboardSelector.promptInput).should("be.visible");
cy.get(dashboardSelector.homePagePromptTextArea).should("be.enabled").type("Build a task manager app");
cy.get(dashboardSelector.promptEnterButton).should("have.attr", "class").and("include", "tw-opacity-100");
cy.get(dashboardSelector.homePageDividerText).verifyVisibleElement(
"have.text",
dashboardText.homePageDividerText
);
// Define card types
const cardTypes = [
{ type: 'app', title: dashboardText.appCardTitle, description: dashboardText.appCardDescription, url: "my-workspace" },
{ type: 'datasource', title: dashboardText.datasourceCardTitle, description: dashboardText.datasourceCardDescription, url: 'data-sources' },
{ type: 'workflow', title: dashboardText.workflowCardTitle, description: dashboardText.workflowCardDescription, url: 'workflows' },
{ type: 'template', title: dashboardText.templateCardTitle, description: dashboardText.templateCardDescription, url: '#' }
];
const env = Cypress.env('environment');
// Filter cards based on environment
const cardsToTest = cardTypes.filter(cardType => {
if (env === 'Enterprise') return ['app', 'datasource', 'workflow'].includes(cardType.type);
if (env === 'Cloud') return ['add', 'datasource', 'template'].includes(cardType.type);
return false;
});
cardsToTest.forEach(cardType => {
cy.get(dashboardSelector.widgetCardName(cardType.type)).within(() => {
cy.get(dashboardSelector.homePageIcon(cardType.type)).should("be.visible");
cy.get(dashboardSelector.widgetCardTitle).verifyVisibleElement(
"have.text",
cardType.title
);
cy.get(dashboardSelector.widgetCardDescription).verifyVisibleElement(
"have.text",
cardType.description
);
});
cy.get(dashboardSelector.widgetCardName(cardType.type)).should("have.attr", "href").and("include", cardType.url);
});
});
it("Should verify Home page accessibility for the specific role", () => {
//Invite End-user
cy.apiFullUserOnboarding(data.firstName, data.email);
cy.apiLogout();
cy.apiLogin(data.email);
cy.visit("/");
cy.get(commonSelectors.homePageIcon).should("not.exist");
cy.apiLogout();
cy.apiLogin();
cy.visit("/");
//Update role to Builder
cy.apiUpdateUserRole(data.email, "builder");
cy.apiLogout();
cy.apiLogin(data.email);
cy.visit("/");
cy.get(commonSelectors.homePageIcon).should("be.visible");
});
});

View file

@ -0,0 +1,219 @@
import { commonSelectors } from "Selectors/common";
import { commonText, settingsText, workspaceSettingsText } from "Texts/common";
import { fake } from "Fixtures/fake";
import { licenseText } from "Texts/license";
import { licenseSelectors } from "Selectors/license";
import { usersSelector } from "Selectors/manageUsers";
import {
switchTabs,
verifylicenseTab,
verifySubTabs,
verifyAccessTab,
verifyDomainTab,
verifyResourceLimit,
verifyTooltip,
} from "Support/utils/license";
import * as common from "Support/utils/common";
import { dashboardSelector } from "../../../../../constants/selectors/dashboard";
import { navigateToEditUser } from "Support/utils/manageUsers";
import {
instanceSettingsSelector,
whiteLabellingSelectors,
} from "Selectors/eeCommon";
import { workflowSelector } from "Selectors/workflows";
import { commonEeSelectors } from "Selectors/eeCommon";
describe("License Page", () => {
const data = {};
beforeEach(() => {
cy.apiLogin();
cy.visit("/");
});
it("Should verify license page elements with the basic plan ", () => {
common.navigateToSettingPage();
cy.get(licenseSelectors.listOfItems(licenseText.license)).click();
cy.contains(licenseText.licenseOverviewTitle).should("be.visible");
cy.get(commonSelectors.breadcrumbPageTitle).should(
"have.text",
licenseText.license
);
cy.get(licenseSelectors.comparePlansText)
.should("be.visible")
.and("contain.text", licenseText.comparePlansText);
switchTabs(licenseText.licenseKeyLabel);
verifylicenseTab();
switchTabs(licenseText.limitsTabTitle);
verifySubTabs(
licenseText.limitsTab.aiCreditsSubTab,
licenseText.aiCreditsSubTab,
{
"Monthly recurring": "0/0",
"Add on credits": "0/0",
}
);
verifySubTabs(licenseText.limitsTab.appsSubTab, licenseText.appsSubTab, {
"Number of Apps": "1/2",
});
verifySubTabs(
licenseText.limitsTab.workspacesSubTab,
licenseText.workspacesSubTab,
{
"Number of Workspaces": "1/1",
}
);
verifySubTabs(licenseText.limitsTab.usersSubTab, licenseText.usersSubTab, {
"Number of Total Users": "1/52",
"Number of Builders": "1/2",
"Number of End Users": "0/50",
"Number of Super Admins": "1/1",
});
verifySubTabs(
licenseText.limitsTab.workflowsSubTab,
licenseText.workflowsSubTab,
{
"Number of Workflows": "0/2",
}
);
verifySubTabs(
licenseText.limitsTab.tablesSubTab,
licenseText.tablesSubTab,
{
"Number of Tables": "Unlimited",
}
);
switchTabs(licenseText.accessTabTitle);
verifyAccessTab();
switchTabs(licenseText.domainTabTitle);
verifyDomainTab();
});
it("Should verify banners and tooltips with the basic plan ", () => {
cy.get(commonSelectors.workspaceName).click();
cy.get('[data-cy="workspace-count"]').should("be.visible");
verifyResourceLimit("workspaces", "basic");
cy.get(dashboardSelector.homePageContent).click();
verifyResourceLimit("apps", "basic");
// cy.get(workflowSelector.globalWorkFlowsIcon).click();
// verifyResourceLimit("workflow", "basic");
common.navigateToManageUsers();
cy.get(usersSelector.buttonAddUsers).click();
verifyResourceLimit("builders", "basic", "usage");
cy.reload();
common.navigateToManageGroups();
cy.get('[data-cy="create-new-group-button"]').should("be.disabled");
verifyTooltip(
'[data-cy="create-new-group-button"]',
"Custom groups are available only in paid plans",
true
);
verifyResourceLimit(
"custom-groups",
"basic",
"custom-groups",
"Custom groups & permissions are paid features"
);
cy.get(
licenseSelectors.listOfItems(workspaceSettingsText.customStylesListItem)
).click();
cy.get('[data-cy="modal-close"]').click();
cy.get(licenseSelectors.paidFeatureButton).should("be.visible");
cy.get(
licenseSelectors.listOfItems(
workspaceSettingsText.configureGitSyncListItem
)
).click();
cy.get(licenseSelectors.paidFeatureButton).should("be.visible");
cy.get(
licenseSelectors.listOfItems(workspaceSettingsText.themesListItem)
).click();
verifyResourceLimit(
"custom-themes",
"basic",
"custom-themes",
"Custom themes is our paid feature. Upgrade to a paid plan to add and customize themes."
);
common.navigateToSettingPage();
cy.get(licenseSelectors.listOfItems(settingsText.allUsersListItem)).click({
force: true,
});
navigateToEditUser(commonText.email);
verifyResourceLimit(
"edit-user",
"basic",
"edit-user",
"Edit user details with our paid plans. For more,"
);
cy.reload();
cy.get(
licenseSelectors.listOfItems(settingsText.manageInstanceSettingsListItem)
).click();
cy.get(licenseSelectors.paidFeatureButton).should("be.visible");
cy.get(instanceSettingsSelector.allowWorkspaceToggle).should("be.disabled");
cy.get(
licenseSelectors.listOfItems(settingsText.whiteLabellingListItem)
).click();
cy.get(licenseSelectors.paidFeatureButton).should("be.visible");
cy.get(whiteLabellingSelectors.appLogoInput).should("be.disabled");
cy.get(commonSelectors.workspaceConstantsIcon).click();
verifyTooltip(
licenseSelectors.listOfItems("staging"),
"Multi-environments are available only in paid plans",
true
);
verifyTooltip(
licenseSelectors.listOfItems("production"),
"Multi-environments are available only in paid plans",
true
);
cy.get(commonSelectors.dashboardIcon).click();
cy.get(commonSelectors.settingsIcon).click();
verifyTooltip(
commonEeSelectors.auditLogIcon,
"Audit logs are available only in paid plans",
true
);
// cy.apiCreateApp(`${fake.companyName}-license-App`);
// cy.openApp();
// cy.get('[data-cy="list-current-env-name"]').click();
// cy.get('[data-cy="env-name-list"]')
// .eq(1)
// .within(() => {
// verifyTooltip(
// '[data-cy="env-name-dropdown"]',
// "Multi-environments are available only in paid plans"
// );
// });
// cy.get('[data-cy="env-name-list"]')
// .eq(2)
// .within(() => {
// verifyTooltip(
// '[data-cy="env-name-dropdown"]',
// "Multi-environments are available only in paid plans"
// );
// });
});
});

View file

@ -1,15 +1,15 @@
import { commonSelectors } from "Selectors/common";
import { commonText } from "Texts/common";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { onboardingSelectors } from "Selectors/onboarding";
import { onboardingText } from "Texts/onboarding";
import { logout } from "Support/utils/common";
import {
bannerElementsVerification,
onboardingStepOne,
onboardingStepTwo,
onboardingStepThree,
onboardingStepTwo,
} from "Support/utils/onboarding";
import { updateLicense } from "Support/utils/platform/eeCommon";
import { commonText } from "Texts/common";
import { onboardingText } from "Texts/onboarding";
describe("Self host onboarding", () => {
const envVar = Cypress.env("environment");
@ -82,21 +82,10 @@ describe("Self host onboarding", () => {
cy.get(check.selector).verifyVisibleElement("have.text", check.text);
});
cy.ifEnv("Community", () => {
cy.get(commonSelectors.signUpTermsHelperText).should(($el) => {
expect($el.contents().first().text().trim()).to.eq(
// commonText.selfHostSignUpTermsHelperText
"By signing up you are agreeing to the"
);
});
});
cy.ifEnv("Enterprise", () => {
cy.get(commonSelectors.signUpTermsHelperText).should(($el) => {
expect($el.contents().first().text().trim()).to.eq(
"By signing up you are agreeing to the"
);
});
cy.get(commonSelectors.signUpTermsHelperText).should(($el) => {
expect($el.contents().first().text().trim()).to.eq(
"By signing up you are agreeing to the"
);
});
const links = [
@ -272,8 +261,9 @@ describe("Self host onboarding", () => {
onboardingStepThree();
});
// cy.get(commonSelectors.skipbutton).click();
cy.backToApps();
cy.wait(2000);
cy.get(commonWidgetSelector.previewButton).eq(1).should("be.visible");
cy.go("back");
logout();
cy.appUILogin();

View file

@ -2,15 +2,15 @@ import { fake } from "Fixtures/fake";
import { commonSelectors } from "Selectors/common";
import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { deleteDatasource } from "Support/utils/dataSource";
import { deleteDatasource} from "Support/utils/dataSource";
import { dataSourceSelector } from "Selectors/dataSource";
import { workflowsText } from "Texts/workflows";
import { workflowSelector } from "Selectors/workflows";
import {
enterJsonInputInStartNode,
deleteAppandWorkflowAfterExecution,
verifyPreviewOutputText,
verifyTextInResponseOutputLimited,
navigateBackToWorkflowsDashboard,
} from "Support/utils/workFlows";
const data = {};
@ -19,15 +19,15 @@ describe("Workflows in apps", () => {
beforeEach(() => {
cy.apiLogin();
cy.visit("/");
data.wfName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.appName = `${data.wfName}-wf-app`;
data.workflowName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.appName = `${data.workflowName}-wf-app`;
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
});
it("Creating workflows with runjs and validating execution in apps", () => {
cy.createWorkflowApp(data.wfName);
cy.createWorkflowApp(data.workflowName);
enterJsonInputInStartNode();
cy.connectDataSourceNode(workflowsText.runjsNodeLabel);
@ -51,7 +51,7 @@ describe("Workflows in apps", () => {
cy.apiCreateApp(data.appName);
cy.openApp();
cy.addWorkflowInApp(data.wfName);
cy.addWorkflowInApp(data.workflowName);
cy.get(dataSourceSelector.queryPreviewButton).click();
@ -59,19 +59,19 @@ describe("Workflows in apps", () => {
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// `Query (${data.dsName}) completed.`
// `Query (${data.dataSourceName}) completed.`
// );
deleteAppandWorkflowAfterExecution(data.wfName, data.appName);
cy.apiDeleteApp();
cy.apiDeleteWorkflow(data.workflowName);
});
it("Creating workflows with postgres and validating execution in apps", () => {
const dsName = `cypress-${data.dataSourceName}-manual-pgsql`;
const dataSourceName = `cypress-${data.dataSourceName}-manual-pgsql`;
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
dsName,
dataSourceName,
"postgresql",
[
{ key: "connection_type", value: "manual", encrypted: false },
@ -94,7 +94,7 @@ describe("Workflows in apps", () => {
]
);
cy.get(dataSourceSelector.dataSourceNameButton(dsName))
cy.get(dataSourceSelector.dataSourceNameButton(dataSourceName))
.should("be.visible")
.click();
cy.get(postgreSqlSelector.buttonTestConnection).click();
@ -103,9 +103,10 @@ describe("Workflows in apps", () => {
}).should("have.text", postgreSqlText.labelConnectionVerified);
cy.reload();
cy.createWorkflowApp(data.wfName);
cy.apiCreateWorkflow(data.workflowName)
cy.openWorkflow();
enterJsonInputInStartNode();
cy.connectDataSourceNode(dsName);
cy.connectDataSourceNode(dataSourceName);
cy.get(workflowSelector.nodeName(workflowsText.postgresqlNodeName)).click({
force: true,
@ -127,7 +128,7 @@ describe("Workflows in apps", () => {
cy.apiCreateApp(data.appName);
cy.openApp();
cy.addWorkflowInApp(data.wfName);
cy.addWorkflowInApp(data.workflowName);
cy.get(dataSourceSelector.queryPreviewButton).click();
@ -135,10 +136,10 @@ describe("Workflows in apps", () => {
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// `Query (${data.dsName}) completed.`
// `Query (${data.dataSourceName}) completed.`
// );
deleteAppandWorkflowAfterExecution(data.wfName, data.appName);
deleteDatasource(`cypress-${data.dataSourceName}-manual-pgsql`);
cy.apiDeleteApp();
cy.apiDeleteWorkflow(data.workflowName);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-manual-pgsql`);
});
});

View file

@ -2,7 +2,7 @@ import { fake } from "Fixtures/fake";
import { commonSelectors } from "Selectors/common";
import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { deleteWorkflowAndDS } from "Support/utils/dataSource";
import { deleteWorkflowAndDS, deleteDatasource } from "Support/utils/dataSource";
import { dataSourceSelector } from "Selectors/dataSource";
import { harperDbText } from "Texts/harperDb";
import { workflowsText } from "Texts/workflows";
@ -14,6 +14,7 @@ import {
import {
enterJsonInputInStartNode,
verifyTextInResponseOutputLimited,
navigateBackToWorkflowsDashboard,
} from "Support/utils/workFlows";
const data = {};
@ -22,14 +23,14 @@ describe("Workflows with Datasource", () => {
beforeEach(() => {
cy.apiLogin();
cy.visit("/");
data.wfName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.workflowName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
});
it("RunJS workflow - execute and validate", () => {
cy.createWorkflowApp(data.wfName);
cy.createWorkflowApp(data.workflowName);
enterJsonInputInStartNode();
cy.connectDataSourceNode(workflowsText.runjsNodeLabel);
@ -50,16 +51,16 @@ describe("Workflows with Datasource", () => {
);
cy.verifyTextInResponseOutput(workflowsText.responseNodeExpectedValueText);
cy.deleteWorkflow(data.wfName);
cy.apiDeleteWorkflow(data.workflowName);
});
it("Postgres workflow - execute and validate", () => {
const dsName = `cypress-${data.dataSourceName}-manual-pgsql`;
const dataSourceName = `cypress-${data.dataSourceName}-manual-pgsql`;
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
dsName,
dataSourceName,
"postgresql",
[
{ key: "connection_type", value: "manual", encrypted: false },
@ -82,7 +83,7 @@ describe("Workflows with Datasource", () => {
]
);
cy.get(dataSourceSelector.dataSourceNameButton(dsName))
cy.get(dataSourceSelector.dataSourceNameButton(dataSourceName))
.should("be.visible")
.click();
cy.get(postgreSqlSelector.buttonTestConnection).click();
@ -91,9 +92,10 @@ describe("Workflows with Datasource", () => {
}).should("have.text", postgreSqlText.labelConnectionVerified);
cy.reload();
cy.createWorkflowApp(data.wfName);
cy.apiCreateWorkflow(data.workflowName)
cy.openWorkflow();
enterJsonInputInStartNode();
cy.connectDataSourceNode(dsName);
cy.connectDataSourceNode(dataSourceName);
cy.get(workflowSelector.nodeName(workflowsText.postgresqlNodeName)).click({
force: true,
@ -112,15 +114,17 @@ describe("Workflows with Datasource", () => {
);
verifyTextInResponseOutputLimited(workflowsText.postgresExpectedValue);
deleteWorkflowAndDS(data.wfName, dsName);
cy.apiDeleteWorkflow(data.workflowName);
cy.apiDeleteGDS(dataSourceName);
});
it("REST API workflow - execute and validate", () => {
const dsName = `cypress-${data.dataSourceName}-restapi`;
const dataSourceName = `cypress-${data.dataSourceName}-restapi`;
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
dsName,
dataSourceName,
"restapi",
[
{ key: "url", value: "https://jsonplaceholder.typicode.com" },
@ -152,9 +156,10 @@ describe("Workflows with Datasource", () => {
]
);
cy.reload();
cy.createWorkflowApp(data.wfName);
cy.apiCreateWorkflow(data.workflowName)
cy.openWorkflow();
enterJsonInputInStartNode();
cy.connectDataSourceNode(dsName);
cy.connectDataSourceNode(dataSourceName);
cy.get(workflowSelector.nodeName(workflowsText.restapiNodeName)).click({
force: true,
@ -174,40 +179,41 @@ describe("Workflows with Datasource", () => {
);
cy.verifyTextInResponseOutput(workflowsText.restApiExpectedValue);
deleteWorkflowAndDS(data.wfName, dsName);
cy.apiDeleteWorkflow(data.workflowName);
cy.apiDeleteGDS(dataSourceName);
});
it("HarperDB workflow - execute and validate", () => {
const dsName = `cypress-${data.dataSourceName}-harperdb`;
const dataSourceName = `cypress-${data.dataSourceName}-harperdb`;
const Host = Cypress.env("harperdb_host");
const Port = Cypress.env("harperdb_port");
const Username = Cypress.env("harperdb_username");
const Password = Cypress.env("harperdb_password");
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.installMarketplacePlugin(workflowsText.harperDbPluginName);
cy.installMarketplacePlugin("HarperDB");
selectAndAddDataSource(
"databases",
harperDbText.harperDb,
data.dataSourceName
);
selectAndAddDataSource("databases", harperDbText.harperDb, data.dataSourceName);
fillDataSourceTextField(
harperDbText.hostLabel,
harperDbText.hostInputPlaceholder,
Host
);
fillDataSourceTextField(
harperDbText.portLabel,
harperDbText.portPlaceholder,
Port
);
fillDataSourceTextField(
harperDbText.userNameLabel,
harperDbText.userNamePlaceholder,
Username
);
fillDataSourceTextField(
harperDbText.passwordlabel,
harperDbText.passwordPlaceholder,
@ -228,9 +234,10 @@ describe("Workflows with Datasource", () => {
postgreSqlText.toastDSSaved
);
cy.createWorkflowApp(data.wfName);
cy.apiCreateWorkflow(data.workflowName)
cy.openWorkflow();
enterJsonInputInStartNode();
cy.connectDataSourceNode(dsName);
cy.connectDataSourceNode(dataSourceName);
cy.get(workflowSelector.nodeName(workflowsText.harperdbNodeName)).click({
force: true,
@ -256,7 +263,8 @@ describe("Workflows with Datasource", () => {
workflowsText.harperDbResponseNodeQuery
);
cy.verifyTextInResponseOutput(workflowsText.harperDbExpectedValue);
deleteWorkflowAndDS(data.wfName, dsName);
navigateBackToWorkflowsDashboard();
cy.apiDeleteWorkflow(data.workflowName);
deleteDatasource(dataSourceName);
});
});

View file

@ -2,7 +2,7 @@ import { fake } from "Fixtures/fake";
import { commonSelectors } from "Selectors/common";
import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { deleteWorkflowAndDS } from "Support/utils/dataSource";
import { deleteWorkflowAndDS,deleteDatasource } from "Support/utils/dataSource";
import { dataSourceSelector } from "Selectors/dataSource";
import { workflowsText } from "Texts/workflows";
import { workflowSelector } from "Selectors/workflows";
@ -11,6 +11,7 @@ import {
enterJsonInputInStartNode,
importWorkflowApp,
verifyTextInResponseOutputLimited,
navigateBackToWorkflowsDashboard
} from "Support/utils/workFlows";
const data = {};
@ -19,16 +20,16 @@ describe("Workflows Export/Import Sanity", () => {
beforeEach(() => {
cy.apiLogin();
cy.visit("/");
data.wfName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.workflowName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
});
it("RunJS workflow - execute, export/import, re-execute", () => {
const wfName = `${data.wfName}-runjs`;
const workflowName = `${data.workflowName}-runjs`;
cy.createWorkflowApp(wfName);
cy.createWorkflowApp(workflowName);
enterJsonInputInStartNode();
cy.connectDataSourceNode(workflowsText.runjsNodeLabel);
@ -49,23 +50,22 @@ describe("Workflows Export/Import Sanity", () => {
);
cy.verifyTextInResponseOutput(workflowsText.responseNodeExpectedValueText);
cy.exportWorkflowApp(wfName);
importWorkflowApp(wfName, workflowsText.exportFixturePath);
cy.exportWorkflowApp(workflowName);
cy.apiDeleteWorkflow(workflowName);
importWorkflowApp(workflowName, workflowsText.exportFixturePath);
cy.verifyTextInResponseOutput(workflowsText.responseNodeExpectedValueText);
cy.deleteWorkflow(wfName);
cy.apiDeleteWorkflow(workflowName);
cy.task("deleteFile", workflowsText.exportFixturePath);
});
it("Postgres workflow - execute, export/import, re-execute", () => {
const wfName = `${data.wfName}-pg`;
const dsName = `cypress-${data.dataSourceName}-manual-pgsql`;
const workflowName = `${data.workflowName}-pg`;
const dataSourceName = `cypress-${data.dataSourceName}-manual-pgsql`;
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
dsName,
dataSourceName,
"postgresql",
[
{ key: "connection_type", value: "manual", encrypted: false },
@ -87,7 +87,7 @@ describe("Workflows Export/Import Sanity", () => {
]
);
cy.get(dataSourceSelector.dataSourceNameButton(dsName))
cy.get(dataSourceSelector.dataSourceNameButton(dataSourceName))
.should("be.visible")
.click();
@ -98,9 +98,10 @@ describe("Workflows Export/Import Sanity", () => {
cy.reload();
cy.createWorkflowApp(wfName);
cy.apiCreateWorkflow(data.workflowName)
cy.openWorkflow();
enterJsonInputInStartNode();
cy.connectDataSourceNode(dsName);
cy.connectDataSourceNode(dataSourceName);
cy.get(workflowSelector.nodeName(workflowsText.postgresqlNodeName)).click({
force: true,
@ -120,12 +121,14 @@ describe("Workflows Export/Import Sanity", () => {
);
verifyTextInResponseOutputLimited(workflowsText.postgresExpectedValue);
cy.exportWorkflowApp(wfName);
importWorkflowApp(wfName, workflowsText.exportFixturePath);
cy.exportWorkflowApp(workflowName);
cy.apiDeleteWorkflow(workflowName);
importWorkflowApp(workflowName, workflowsText.exportFixturePath);
verifyTextInResponseOutputLimited(workflowsText.postgresExpectedValue);
cy.apiDeleteWorkflow(workflowName);
deleteWorkflowAndDS(wfName, dsName);
cy.apiDeleteGDS(dataSourceName);
cy.task("deleteFile", workflowsText.exportFixturePath);
});
});

View file

@ -5,6 +5,7 @@ import { workflowSelector } from "Selectors/workflows";
import {
enterJsonInputInStartNode,
revealWorkflowToken,
navigateBackToWorkflowsDashboard
} from "Support/utils/workFlows";
const data = {};
@ -13,14 +14,14 @@ describe("Workflows with Webhooks", () => {
beforeEach(() => {
cy.apiLogin();
cy.visit("/");
data.wfName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.workflowName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
});
it("Creating workflows with runjs, triggering via webhook, and validating execution", () => {
cy.createWorkflowApp(data.wfName);
cy.createWorkflowApp(data.workflowName);
enterJsonInputInStartNode();
cy.connectDataSourceNode(workflowsText.runjsNodeLabel);
@ -64,6 +65,6 @@ describe("Workflows with Webhooks", () => {
});
});
});
cy.deleteWorkflow(data.wfName);
cy.apiDeleteWorkflow(data.workflowName);
});
});

View file

@ -15,17 +15,17 @@ describe("Workflows features", () => {
beforeEach(() => {
cy.apiLogin();
cy.visit("/");
data.wfName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.appName = `${data.wfName}-wf-app`;
data.childWFName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.parentWFName = `${data.wfName}-wf-app`;
data.workflowName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.appName = `${data.workflowName}-wf-app`;
data.childWorkflowName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.parentWorkflowName = `${data.workflowName}-wf-app`;
data.dataSourceName = fake.lastName
.toLowerCase()
.replaceAll("[^A-Za-z]", "");
});
it("Creating workflow with long string input and validating execution", () => {
cy.createWorkflowApp(data.wfName);
cy.createWorkflowApp(data.workflowName);
enterJsonInputInStartNode(workflowsText.longStringJsonText);
cy.connectDataSourceNode(workflowsText.runjsNodeLabel);
@ -45,11 +45,13 @@ describe("Workflows features", () => {
workflowsText.responseNodeQuery
);
cy.verifyTextInResponseOutput(workflowsText.longStringJsonText);
cy.deleteWorkflow(data.wfName);
cy.apiDeleteWorkflow(data.workflowName);
});
it("Creating workflow with Node Preview Validation and execution", () => {
cy.createWorkflowApp(data.wfName);
cy.apiCreateWorkflow(data.workflowName)
cy.openWorkflow();
enterJsonInputInStartNode();
cy.connectDataSourceNode(workflowsText.runjsNodeLabel);
@ -71,12 +73,14 @@ describe("Workflows features", () => {
workflowsText.responseNodeQuery
);
cy.verifyTextInResponseOutput(workflowsText.jsonValuePlaceholder);
cy.deleteWorkflow(data.wfName);
cy.apiDeleteWorkflow(data.workflowName);
});
// Need to run after bug fixes
it("Creating workflow inside Workflow and validating execution", () => {
cy.createWorkflowApp(data.childWFName);
cy.apiCreateWorkflow(data.childWorkflowName)
cy.openWorkflow();
enterJsonInputInStartNode();
cy.connectDataSourceNode(workflowsText.runjsNodeLabel);
@ -97,9 +101,8 @@ describe("Workflows features", () => {
);
cy.verifyTextInResponseOutput(workflowsText.responseNodeExpectedValueText);
navigateBackToWorkflowsDashboard();
cy.createWorkflowApp(data.parentWFName);
cy.apiCreateWorkflow(data.parentWorkflowName)
cy.openWorkflow();
enterJsonInputInStartNode();
cy.connectDataSourceNode(workflowsText.workflowNodeLabel);
@ -109,9 +112,9 @@ describe("Workflows features", () => {
cy.get('input[id^="react-select-"]')
.eq(1)
.type(data.childWFName, { force: true });
.type(data.childWorkflowName, { force: true });
cy.get(".react-select__option")
.contains(data.childWFName)
.contains(data.childWorkflowName)
.click({ force: true });
cy.get("body").click(50, 50);
@ -122,12 +125,13 @@ describe("Workflows features", () => {
workflowsText.workflowResponseNodeQuery
);
// cy.verifyTextInResponseOutput(workflowsText.responseNodeExpectedValueText);
cy.deleteWorkflow(data.childWFName);
cy.deleteWorkflowfromDashboard(data.parentWFName);
cy.apiDeleteWorkflow(data.childWorkflowName);
cy.apiDeleteWorkflow(data.parentWorkflowName);
});
it("Creating workflow with large datasets and validating execution", () => {
cy.createWorkflowApp(data.wfName);
cy.apiCreateWorkflow(data.workflowName)
cy.openWorkflow();
enterJsonInputInStartNode();
cy.connectDataSourceNode(workflowsText.runjsNodeLabel);
@ -152,7 +156,6 @@ describe("Workflows features", () => {
verifyTextInResponseOutputLimited(
workflowsText.responseNodeExpectedValueTextForLargeDataset
);
cy.deleteWorkflow(data.wfName);
cy.apiDeleteWorkflow(data.workflowName);
});
});

View file

@ -0,0 +1,24 @@
{
"basic": {
"planName": "Basic",
"Apps": 2,
"Workspaces": 1,
"Total Users": 52,
"Builders": 2,
"End Users": 50,
"Super Admins": 1,
"Workflows": 2,
"Tables": "Unlimited"
},
"enterprise": {
"planName": "Enterprise",
"Apps": 3,
"Workspaces": 2,
"Total Users": 3,
"Builders": 2,
"End Users": 1,
"Super Admins": 1,
"Workflows": 2,
"Tables": 2
}
}

View file

@ -14,11 +14,14 @@
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "cypress-real-events/support";
import "../commands/commands";
import "../commands/apiCommands";
import "../commands/workflowsApiCommands";
import '../commands/workflowCommands';
import '../commands/platform/platformApiCommands';
import "@cypress/code-coverage/support";
import "cypress-real-events";
// Alternatively you can use CommonJS syntax:
// require('./commands')

View file

@ -1,4 +1,4 @@
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { commonWidgetSelector } from "Selectors/common";
import { appPromote } from "Support/utils/platform/multiEnv";
const slugValidations = [
@ -76,16 +76,20 @@ export const setUpSlug = (slug) => {
cy.get(commonWidgetSelector.modalCloseButton).click();
};
export const setupAppWithSlug = (appName, slug) => {
export const setupAppWithSlug = (appName, slug, appType = 'private') => {
const defaultLayout = {
desktop: { top: 90, left: 9, width: 6, height: 40 },
mobile: { top: 90, left: 9, width: 6, height: 40 },
};
cy.apiCreateApp(appName);
cy.apiAddComponentToApp(appName, "text1");
cy.apiAddComponentToApp(appName, appType, defaultLayout, "Text", appType);
cy.ifEnv("Enterprise", () => {
cy.openApp(
"",
Cypress.env("workspaceId"),
Cypress.env("appId"),
commonWidgetSelector.draggableWidget("text1")
commonWidgetSelector.draggableWidget(appType)
);
appPromote("development", "production");
});

View file

@ -7,6 +7,7 @@ import {
import { profileSelector } from "Selectors/profile";
import { appPromote } from "Support/utils/platform/multiEnv";
import { commonText, path } from "Texts/common";
import { commonEeSelectors } from "Selectors/eeCommon";
export const navigateToProfile = () => {
cy.get(commonSelectors.settingsIcon).click();
@ -101,15 +102,17 @@ export const navigateToAppEditor = (appName) => {
};
export const viewAppCardOptions = (appName) => {
cy.get(".homepage-app-card .home-app-card-header .menu-ico").then(($el) => {
$el[0].style.setProperty("visibility", "visible", "important");
});
cy.contains('.homepage-app-card', appName).within(() => {
cy.get('.home-app-card-header .menu-ico')
.then(($el) => {
$el[0].style.setProperty('visibility', 'visible', 'important');
});
cy.get('[data-cy="app-card-menu-icon"]').click();
cy.get('[data-cy="app-card-menu-icon"]').click();
});
};
export const viewFolderCardOptions = (folderName) => {
cy.reloadAppForTheElement(folderName);
cy.get(commonSelectors.folderListcard(folderName))
.parent()
.within(() => {
@ -128,7 +131,7 @@ export const verifyModal = (title, buttonText, inputFiledSelector) => {
cy.get(commonSelectors.buttonSelector(commonText.cancelButton))
.should("be.visible")
.and("have.text", commonText.cancelButton);
cy.get(commonSelectors.buttonSelector(buttonText))
cy.get(commonSelectors.buttonSelector(buttonText)).first()
.should("be.visible")
.and("have.text", buttonText);
@ -184,7 +187,6 @@ export const searchUser = (email) => {
};
export const selectAppCardOption = (appName, appCardOption) => {
cy.wait(1000);
viewAppCardOptions(appName);
cy.get(appCardOption).should("be.visible").click();
};
@ -251,3 +253,9 @@ export const fillInputField = (data) => {
cy.get(inputSelector).type(`{selectall}{backspace}${value}`);
});
};
export const navigateToSettingPage = () => {
cy.get(commonSelectors.settingsIcon).click();
cy.get(commonEeSelectors.instanceSettingIcon).click();
cy.get(commonSelectors.pageSectionHeader).should("be.visible");
};

View file

@ -1,13 +1,13 @@
import { commonSelectors } from "Selectors/common";
import { dashboardSelector } from "Selectors/dashboard";
import { dashboardText } from "Texts/dashboard";
import { commonText } from "Texts/common";
import {
viewAppCardOptions,
verifyModal,
closeModal,
cancelModal,
closeModal,
verifyModal,
viewAppCardOptions,
} from "Support/utils/common";
import { commonText } from "Texts/common";
import { dashboardText } from "Texts/dashboard";
export const modifyAndVerifyAppCardIcon = (appName) => {
var random = function (obj) {

View file

@ -1,10 +1,9 @@
import { postgreSqlSelector } from "Selectors/postgreSql";
import { postgreSqlText } from "Texts/postgreSql";
import { cyParamName } from "Selectors/common";
import { commonSelectors } from "Selectors/common";
import { commonSelectors, cyParamName } from "Selectors/common";
import { dataSourceSelector } from "Selectors/dataSource";
import { postgreSqlSelector } from "Selectors/postgreSql";
import { navigateToAppEditor } from "Support/utils/common";
import { verifyAppDelete } from "Support/utils/dashboard";
import { postgreSqlText } from "Texts/postgreSql";
export const verifyCouldnotConnectWithAlert = (alertText) => {
cy.get(postgreSqlSelector.connectionFailedText, {

View file

@ -0,0 +1,38 @@
export const renameApp = (name) => {
cy.get('[data-cy="edit-app-name-button"]').click();
cy.get("[data-cy='app-name-input']").type(`{selectAll}{backspace}${name}`, { force: true });
cy.get("[data-cy='rename-app']").click();
};
export const verifyAppName = (name) => {
cy.get('[data-cy="edit-app-name-button"]').should("have.text", name);
}
export const verifyCurrentEnvironment = (envName) => {
cy.get('[data-cy="list-current-env-name"]').should("have.text", envName);
}
export const verifyCurrentVersion = (version) => {
cy.get('[data-cy*="-current-version-text"]').should("have.text", version);
}
export const addNewVersion = (newVersion, fromVersion) => {
cy.get('[data-cy*="-current-version-text"]').click();
cy.get('[data-cy="create-new-version-button"]').click();
if (fromVersion) {
cy.get('[data-cy="create-version-from-input-field"]').click();
cy.contains('[id*="react-select"]', fromVersion).click();
}
cy.get('[data-cy="version-name-input-field"]').type(newVersion, { force: true });
cy.get('[data-cy="create-new-version-button"]').click();
cy.verifyToastMessage("Version Created");
};
export const promoteEnv = () => {
cy.get('[data-cy="promote-button"]').first().click();
cy.get('[data-cy="promote-button"]').last().click();
}
// import { renameApp, verifyAppName, verifyCurrentEnvironment, verifyCurrentVersion, addNewVersion, promoteEnv } from 'cypress-tests/cypress/support/utils/editor/editorHeaderOperations';

View file

@ -1,12 +1,11 @@
import { commonSelectors } from "Selectors/common";
import {
appVersionSelectors,
exportAppModalSelectors,
importSelectors,
} from "Selectors/exportImport";
import { exportAppModalText, appVersionText } from "Texts/exportImport";
import { commonSelectors } from "Selectors/common";
import { verifyModal, commonWidgetSelector } from "Support/utils/common";
import { importText } from "Texts/exportImport";
import { verifyModal } from "Support/utils/common";
import { appVersionText, exportAppModalText, importText } from "Texts/exportImport";
export const verifyElementsOfExportModal = (
currentVersionName,
@ -137,3 +136,122 @@ export const importAndVerifyApp = (filePath, expectedToast) => {
}
};
export const verifyImportModalElements = (expectedAppName) => {
cy.get(importSelectors.importAppTitle).verifyVisibleElement("have.text", "Import app");
cy.get(commonSelectors.appNameLabel).verifyVisibleElement("have.text", "App name");
cy.get(commonSelectors.appNameInput)
.should("be.visible")
.and("have.value", expectedAppName);
cy.get(commonSelectors.appNameInfoLabel).verifyVisibleElement(
"have.text",
"App name must be unique and max 50 characters"
);
cy.get(commonSelectors.cancelButton)
.should("be.visible")
.and("have.text", "Cancel");
cy.get(commonSelectors.importAppButton).verifyVisibleElement("have.text", "Import app");
};
export const setupDataSourceWithConstants = (dsEnv, password = Cypress.env("pg_password")) => {
cy.apiUpdateDataSource("postgresql", dsEnv, {
options: [{
key: "password",
value: password,
encrypted: true,
}],
});
};
export const validateExportedAppStructure = (
appData,
expectedAppName,
options = {}
) => {
const {
validateComponents = true,
validateQueries = true,
validateVersions = true,
validateTooljetDB = true,
expectedVersions = ["v1", "v2", "v3"],
} = options;
// Validate the app name
const appNameFromFile = appData.app[0].definition.appV2.name;
expect(appNameFromFile).to.equal(expectedAppName);
// Validate the schema for the student table in tooljetdb
if (validateTooljetDB) {
const tooljetDatabase = appData.tooljet_database.find(
(db) => db.table_name === "student"
);
expect(tooljetDatabase).to.exist;
expect(tooljetDatabase.schema).to.exist;
}
// Validate components
if (validateComponents) {
const components = appData.app[0].definition.appV2.components;
const text2Component = components.find(
(component) => component.name === "text2"
);
expect(text2Component).to.exist;
expect(text2Component.properties.text.value).to.equal(
"{{constants.pageHeader}}"
);
const textinput1 = components.find(
(component) => component.name === "textinput1"
);
expect(textinput1).to.exist;
expect(textinput1.properties.value.value).to.include("queries");
const textinput2 = components.find(
(component) => component.name === "textinput2"
);
expect(textinput2).to.exist;
expect(textinput2.properties.value.value).to.include("queries");
const textinput3 = components.find(
(component) => component.name === "textinput3"
);
cy.log(JSON.stringify(appData.app[0].definition.appV2.appVersions));
expect(textinput3).to.exist;
expect(textinput3.properties.value.value).to.include("queries");
}
// Validate the data queries
if (validateQueries) {
const dataQueries = appData.app[0].definition.appV2.dataQueries;
const postgresqlQuery = dataQueries.find(
(query) => query.name === "postgresql1"
);
expect(postgresqlQuery).to.exist;
expect(postgresqlQuery.options.query).to.include(
"Select * from {{secrets.db_name}}"
);
const restapiQuery = dataQueries.find((query) => query.name === "restapi1");
expect(restapiQuery).to.exist;
expect(restapiQuery.options.url).to.equal(
"https://jsonplaceholder.typicode.com/users/1"
);
const tooljetdbQuery = dataQueries.find(
(query) => query.name === "tooljetdb1"
);
expect(tooljetdbQuery).to.exist;
expect(tooljetdbQuery.options.operation).to.equal("list_rows");
}
// Validate app versions
if (validateVersions) {
const appVersions = appData.app[0].definition.appV2.appVersions;
expect(appVersions).to.exist;
const versionNames = appVersions.map((version) => version.name);
expect(versionNames).to.include.members(expectedVersions);
}
};

View file

@ -0,0 +1,224 @@
import { licenseSelectors } from "Selectors/license";
import { licenseText } from "Texts/license";
export const switchTabs = (tabTitle) => {
cy.get(licenseSelectors.listOfItems(tabTitle)).should("be.visible").click();
cy.get(licenseSelectors.tabTitle(tabTitle)).should("have.text", tabTitle);
};
export const verifylicenseTab = () => {
cy.get(licenseSelectors.label(licenseText.licenseKeyTab.licenseLabel)).should(
"be.visible"
);
cy.get(licenseSelectors.licenseTextArea).should(
"have.attr",
"placeholder",
licenseText.licenseKeyTab.enterLicenseKeyPlaceholder
);
};
export const verifySubTabs = (subTabName, subTabDataObj, valuesObj) => {
const subTabData = Object.values(subTabDataObj);
cy.get(licenseSelectors.subTab(subTabName))
.verifyVisibleElement("have.text", subTabName)
.click();
subTabData.forEach((label) => {
const displayLabel = /^Number of\s+/i.test(label)
? label.replace(/^Number of\s+/i, "")
: label;
cy.get(
licenseSelectors.numberOfTextLabel(displayLabel)
).verifyVisibleElement("have.text", label);
if (valuesObj && valuesObj[displayLabel] !== undefined) {
cy.get(licenseSelectors.inputField(displayLabel)).verifyVisibleElement(
"have.value",
valuesObj[displayLabel]
);
}
});
};
export const verifyAccessTab = () => {
const accessTabLabels = Object.values(licenseText.accessTab);
accessTabLabels.forEach((accessTabLabel) => {
cy.get(licenseSelectors.label(accessTabLabel)).verifyVisibleElement(
"have.text",
accessTabLabel
);
cy.get(licenseSelectors.label(accessTabLabel))
.next(licenseSelectors.circularToggleDisabledIcon)
.should("be.visible");
});
};
export const verifyDomainTab = () => {
cy.get(licenseSelectors.warningIcon).should("be.visible");
cy.get(licenseSelectors.noDomainLinkedLabel).verifyVisibleElement(
"have.text",
licenseText.domainTab.noDomainLinkedLabel
);
cy.get(licenseSelectors.noDomainInfoText).verifyVisibleElement(
"have.text",
licenseText.domainTab.noDomainInfoText
);
};
export const verifyTooltip = (
selector,
expectedTooltip,
isDisabled = false
) => {
const hoverTarget = isDisabled
? cy.get(selector).parent().trigger("mouseover", { force: true })
: cy.get(selector).trigger("mouseover", { force: true });
cy.get(".tooltip", { timeout: 3000 })
.should("be.visible")
.and("contain.text", expectedTooltip);
hoverTarget.trigger("mouseout", { force: true });
};
const normalizeText = (text) =>
text
.trim()
.replace(/[\u00A0\s]+/g, " ")
.replace(/[\u2018\u2019]/g, "'")
.replace(/[\u201C\u201D]/g, '"');
const verifyLimitUI = (
type,
baseLabel,
headingSelector,
infoSelector,
limit
) => {
cy.get(headingSelector)
.invoke("text")
.then((headingText) => {
cy.log(`Heading: ${headingText}`);
cy.get(infoSelector)
.invoke("text")
.then((infoText) => {
cy.log(`Info: ${infoText}`);
const ratioMatch = headingText.match(/(\d+)\/(\d+)/);
if (ratioMatch) {
const [_, currentCount, headingLimit] = ratioMatch.map(Number);
cy.log(`Current ${type}: ${currentCount}`);
expect(headingLimit).to.equal(limit);
expect(headingText).to.match(
new RegExp(`${baseLabel}\\s+limit\\s+nearing`, "i")
);
expect(infoText).to.match(/nearing/i);
} else {
expect(headingText).to.match(
new RegExp(`${baseLabel}\\s+limit\\s+reached`, "i")
);
expect(infoText).to.contain(
`reached your limit for number of ${type}`
);
}
});
});
};
const verifyFeatureBanner = (cyPrefix, expectedHeading = null) => {
const isSmallBanner = cyPrefix === "edit-user";
const headingSelector = isSmallBanner
? licenseSelectors.licenseBannerHeading
: licenseSelectors.limitHeading(cyPrefix);
const infoSelector = isSmallBanner
? licenseSelectors.licenseBannerInfo
: licenseSelectors.limitInfo(cyPrefix);
cy.get(headingSelector)
.should("be.visible")
.invoke("text")
.then((headingText) => {
const actual = normalizeText(headingText);
if (expectedHeading) {
const expected = normalizeText(expectedHeading);
expect(actual).to.equal(expected);
}
});
cy.get("body").then(($body) => {
if ($body.find(infoSelector).length > 0) {
cy.get(infoSelector).then(($el) => {
const text = $el.text().trim();
if ($el.is(":visible") && text) cy.log(`Feature banner info: ${text}`);
});
}
});
};
const getLimitFromPlan = (planName, type) => {
return cy.fixture("license/license.json").then((licenseData) => {
const planKey = planName.toLowerCase();
const plan = licenseData[planKey];
expect(plan, `Plan "${planName}" should exist in license.json`).to.not.be
.undefined;
const resourceKeyMap = {
workspaces: "Workspaces",
apps: "Apps",
tables: "Tables",
workflows: "Workflows",
users: "Total Users",
builders: "Builders",
endusers: "End Users",
superadmins: "Super Admins",
};
const resourceKey = resourceKeyMap[type];
if (!resourceKey) return cy.wrap(null);
const limit = plan[resourceKey];
expect(limit, `Limit for "${resourceKey}" should exist in ${planName} plan`)
.to.not.be.undefined;
cy.log(`Using limit from ${planName} plan: ${resourceKey} = ${limit}`);
return cy.wrap(limit);
});
};
export const verifyResourceLimit = (
resourceType,
limitOrPlan = null,
dataCyPrefix = null,
expectedHeading = null
) => {
const type = resourceType.toLowerCase().trim();
const cyPrefix = dataCyPrefix || type;
const typeCapitalized = type.charAt(0).toUpperCase() + type.slice(1);
const baseLabel = typeCapitalized.slice(0, -1);
const headingSelector = licenseSelectors.limitHeading(cyPrefix);
const infoSelector = licenseSelectors.limitInfo(cyPrefix);
const isNumber = typeof limitOrPlan === "number";
const planName = isNumber ? null : limitOrPlan;
if (isNumber) {
return verifyLimitUI(
type,
baseLabel,
headingSelector,
infoSelector,
limitOrPlan
);
}
getLimitFromPlan(planName, type).then((limit) => {
if (limit === null) {
verifyFeatureBanner(cyPrefix, expectedHeading);
} else {
verifyLimitUI(type, baseLabel, headingSelector, infoSelector, limit);
}
});
};

View file

@ -10,631 +10,7 @@ import {
} from "Support/utils/manageUsers";
import { groupsText } from "Texts/manageGroups";
export const manageGroupsElements = () => {
cy.get('[data-cy="page-title"]').should(($el) => {
expect($el.contents().last().text().trim()).to.eq("Groups");
});
cy.get('[data-cy="user-role-title"]').verifyVisibleElement(
"have.text",
"USER ROLE"
);
// Admin Permissions
// Admin List Item Verification
cy.verifyElement(groupsSelector.adminListItem, "Admin");
cy.verifyElement(groupsSelector.adminTitle, "Admin (1)");
// Group Permission Elements Verification
cy.verifyElement(
groupsSelector.createNewGroupButton,
groupsText.createNewGroupButton
);
cy.verifyElement(groupsSelector.usersLink, groupsText.usersLink);
cy.verifyElement(groupsSelector.permissionsLink, groupsText.permissionsLink);
cy.verifyElement(groupsSelector.granularLink, "Granular access");
// Resource Verification
cy.verifyElement(
groupsSelector.textDefaultGroup,
groupsText.textDefaultGroup
);
cy.verifyElement(
groupsSelector.nameTableHeader,
groupsText.userNameTableHeader
);
cy.verifyElement(
groupsSelector.emailTableHeader,
groupsText.emailTableHeader
);
// Permissions Page Navigation and Verifications
cy.get(groupsSelector.permissionsLink).click();
cy.get(groupsSelector.helperTextAdminAppAccess)
.eq(0)
.verifyVisibleElement("have.text", groupsText.adminAccessHelperText);
// Granular Access Verifications
cy.verifyElement(groupsSelector.resourcesApps, groupsText.resourcesApps);
cy.verifyElement(
groupsSelector.permissionsTableHeader,
groupsText.permissionsTableHeader
);
cy.get(groupsSelector.appsCreateCheck)
.should("be.visible")
.and("be.checked")
.and("be.disabled");
cy.verifyElement(groupsSelector.appsCreateLabel, groupsText.createLabel);
cy.verifyElement(
groupsSelector.appCreateHelperText,
groupsText.appCreateHelperText
);
cy.get(groupsSelector.appsDeleteCheck)
.should("be.visible")
.and("be.checked")
.and("be.disabled");
cy.verifyElement(groupsSelector.appsDeleteLabel, groupsText.deleteLabel);
cy.verifyElement(
groupsSelector.appDeleteHelperText,
groupsText.appDeleteHelperText
);
// Folder Permissions
cy.verifyElement(
groupsSelector.resourcesFolders,
groupsText.resourcesFolders
);
cy.verifyElement(
groupsSelector.foldersCreateLabel,
groupsText.folderCreateLabel
);
cy.verifyElement(
groupsSelector.foldersHelperText,
groupsText.folderHelperText
);
cy.get(groupsSelector.foldersCreateCheck)
.should("be.visible")
.and("be.checked")
.and("be.disabled");
// Workspace Variable Permissions
cy.verifyElement(
groupsSelector.resourcesWorkspaceVar,
groupsText.resourcesWorkspaceVar
);
cy.verifyElement(
groupsSelector.workspaceCreateLabel,
groupsText.workspaceCreateLabel
);
cy.verifyElement(
groupsSelector.workspaceHelperText,
groupsText.workspaceHelperText
);
cy.get(groupsSelector.workspaceVarCheckbox)
.should("be.visible")
.and("be.checked")
.and("be.disabled");
// Granular Permissions
cy.get(groupsSelector.granularLink).click();
cy.verifyElement(groupsSelector.nameTableHeader, groupsText.nameTableHeader);
cy.verifyElement(
groupsSelector.permissionsTableHeader,
groupsText.granularAccessPermissionHeader
);
cy.verifyElement(
`${groupsSelector.resourceHeader}:eq(1)`,
groupsText.resourcesTableHeader
);
cy.verifyElement(groupsSelector.appsText, " Apps");
cy.get(groupsSelector.appEditRadio)
.should("be.visible")
.and("be.checked")
.and("have.attr", "disabled");
cy.verifyElement(groupsSelector.appEditLabel, groupsText.appEditLabelText);
cy.verifyElement(
groupsSelector.appEditHelperText,
groupsText.appEditHelperText
);
cy.get(groupsSelector.appViewRadio)
.should("be.visible")
.and("have.attr", "disabled");
cy.verifyElement(groupsSelector.appViewLabel, groupsText.appViewLabel);
cy.verifyElement(
groupsSelector.appViewHelperText,
groupsText.appViewHelperText
);
cy.get(groupsSelector.appHideCheckbox).should("be.visible");
cy.verifyElement(
groupsSelector.appHideHelperText,
groupsText.appHideHelperText
);
cy.verifyElement(groupsSelector.addAppButton, groupsText.addButton);
cy.get(groupsSelector.addAppButton).should("be.disabled");
//Builder
cy.get(groupsSelector.groupLink("Builder")).click();
cy.verifyElement(groupsSelector.builderListItem, "Builder");
cy.verifyElement(groupsSelector.builderTitle, "Builder (0)");
// Group Permission Elements Verification
cy.verifyElement(
groupsSelector.createNewGroupButton,
groupsText.createNewGroupButton
);
cy.verifyElement(groupsSelector.usersLink, groupsText.usersLink);
cy.verifyElement(groupsSelector.permissionsLink, groupsText.permissionsLink);
cy.verifyElement(groupsSelector.granularLink, "Granular access");
// Resource Verification
cy.verifyElement(
groupsSelector.textDefaultGroup,
groupsText.textDefaultGroup
);
cy.get(groupsSelector.usersLink).click();
cy.verifyElement(
groupsSelector.nameTableHeader,
groupsText.userNameTableHeader
);
cy.verifyElement(
groupsSelector.emailTableHeader,
groupsText.emailTableHeader
);
cy.get(groupsSelector.userEmptyPageIcon).should("be.visible");
cy.get(groupsSelector.userEmptyPageTitle).verifyVisibleElement(
"have.text",
groupsText.userEmptyPageTitle
);
cy.get(groupsSelector.userEmptyPageHelperText).verifyVisibleElement(
"have.text",
groupsText.userEmptyPageHelperText
);
// Granular Access Verifications
cy.get(groupsSelector.permissionsLink).click();
cy.verifyElement(groupsSelector.resourcesApps, groupsText.resourcesApps);
cy.verifyElement(
groupsSelector.permissionsTableHeader,
groupsText.permissionsTableHeader
);
cy.get(groupsSelector.appsCreateCheck).should("be.visible").and("be.checked");
cy.verifyElement(groupsSelector.appsCreateLabel, groupsText.createLabel);
cy.verifyElement(
groupsSelector.appCreateHelperText,
groupsText.appCreateHelperText
);
toggleCheckbox(
groupsSelector.appsCreateCheck,
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
cy.get(groupsSelector.appsDeleteCheck).should("be.visible").and("be.checked");
cy.verifyElement(groupsSelector.appsDeleteLabel, groupsText.deleteLabel);
cy.verifyElement(
groupsSelector.appDeleteHelperText,
groupsText.appDeleteHelperText
);
toggleCheckbox(
groupsSelector.appsDeleteCheck,
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
// Folder Permissions
cy.verifyElement(
groupsSelector.resourcesFolders,
groupsText.resourcesFolders
);
cy.get(groupsSelector.foldersCreateCheck)
.should("be.visible")
.and("be.checked");
cy.verifyElement(
groupsSelector.foldersCreateLabel,
groupsText.folderCreateLabel
);
cy.verifyElement(
groupsSelector.foldersHelperText,
groupsText.folderHelperText
);
toggleCheckbox(
groupsSelector.foldersCreateCheck,
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
// Workspace Variable Permissions
cy.verifyElement(
groupsSelector.resourcesWorkspaceVar,
groupsText.resourcesWorkspaceVar
);
cy.get(groupsSelector.workspaceVarCheckbox)
.should("be.visible")
.and("be.checked");
cy.verifyElement(
groupsSelector.workspaceCreateLabel,
groupsText.workspaceCreateLabel
);
cy.verifyElement(
groupsSelector.workspaceHelperText,
groupsText.workspaceHelperText
);
toggleCheckbox(
groupsSelector.workspaceVarCheckbox,
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
// Granular Permissions
cy.get(groupsSelector.granularLink).click();
cy.verifyElement(groupsSelector.nameTableHeader, groupsText.nameTableHeader);
cy.verifyElement(
groupsSelector.permissionsTableHeader,
groupsText.granularAccessPermissionHeader
);
cy.verifyElement(
`${groupsSelector.resourceHeader}:eq(1)`,
groupsText.resourcesTableHeader
);
cy.verifyElement(groupsSelector.appsText, groupsText.appsLink);
cy.get(groupsSelector.appEditRadio)
.should("be.visible")
.and("be.checked")
.and("be.enabled");
cy.verifyElement(groupsSelector.appEditLabel, groupsText.appEditLabelText);
cy.verifyElement(
groupsSelector.appEditHelperText,
groupsText.appEditHelperText
);
cy.get(groupsSelector.appViewRadio).should("be.visible").and("be.enabled");
cy.verifyElement(groupsSelector.appViewLabel, groupsText.appViewLabel);
cy.verifyElement(
groupsSelector.appViewHelperText,
groupsText.appViewHelperText
);
cy.get(groupsSelector.appHideCheckbox)
.should("be.visible")
.and("be.disabled");
cy.verifyElement(
groupsSelector.appHideLabel,
groupsText.appHideLabelPermissionModal
);
cy.verifyElement(
groupsSelector.appHideHelperText,
groupsText.appHideHelperText
);
cy.get(groupsSelector.granularAccessPermission)
.trigger("mouseenter")
.click({ force: true });
cy.get(".modal-base").should("be.visible");
cy.get(groupsSelector.deletePermissionIcon)
.should("be.visible")
.and("be.enabled");
cy.get(groupsSelector.deletePermissionIcon).click();
cy.get(".confirm-dialogue-modal").should("be.visible");
cy.verifyElement(groupsSelector.deleteMessage, groupsText.deleteMessage);
cy.get(groupsSelector.yesButton).should("be.visible").and("be.enabled");
cy.get(groupsSelector.cancelButton).should("be.visible").and("be.enabled");
cy.contains("Cancel").click();
cy.get(groupsSelector.granularAccessPermission)
.trigger("mouseenter")
.click({ force: true });
cy.verifyElement(
`${groupsSelector.addEditPermissionModalTitle}:eq(2)`,
groupsText.editPermissionModalTitle
);
permissionModal();
cy.get(groupsSelector.customradio).should("be.visible").should("be.disabled");
cy.verifyElement(groupsSelector.customLabel, groupsText.customLabel);
cy.verifyElement(
groupsSelector.customHelperText,
groupsText.customHelperText
);
cy.verifyElement(groupsSelector.confimButton, groupsText.updateButtonText);
cy.get(groupsSelector.confimButton).should("be.disabled");
cy.verifyElement(groupsSelector.cancelButton, groupsText.cancelButton);
cy.get(groupsSelector.cancelButton).click();
//Add modal
cy.verifyElement(groupsSelector.addAppButton, groupsText.addButton);
cy.get(groupsSelector.addAppButton)
.should("be.visible")
.and("be.enabled")
.click();
cy.verifyElement(
`${groupsSelector.addEditPermissionModalTitle}:eq(2)`,
groupsText.addPermissionModalTitle
);
permissionModal();
cy.get(groupsSelector.customradio).should("be.visible").should("be.disabled");
cy.verifyElement(groupsSelector.customLabel, groupsText.customLabel);
cy.verifyElement(
groupsSelector.customHelperText,
groupsText.customHelperText
);
cy.verifyElement(groupsSelector.confimButton, groupsText.addButtonText);
cy.get(groupsSelector.confimButton).should("be.disabled");
cy.verifyElement(groupsSelector.cancelButton, groupsText.cancelButton);
cy.get(groupsSelector.cancelButton).click();
//End User
cy.get(groupsSelector.groupLink("End-user")).click();
cy.get(groupsSelector.groupLink("End-user")).verifyVisibleElement(
"have.text",
"End-user"
);
cy.get(groupsSelector.enduserTitle).verifyVisibleElement(
"have.text",
"End-user (0)"
);
cy.verifyElement(
groupsSelector.createNewGroupButton,
groupsText.createNewGroupButton
);
cy.verifyElement(groupsSelector.usersLink, groupsText.usersLink);
cy.verifyElement(groupsSelector.permissionsLink, groupsText.permissionsLink);
cy.verifyElement(groupsSelector.granularLink, "Granular access");
// Resource Verification
cy.verifyElement(
groupsSelector.textDefaultGroup,
groupsText.textDefaultGroup
);
cy.get(groupsSelector.usersLink).click();
cy.verifyElement(
groupsSelector.nameTableHeader,
groupsText.userNameTableHeader
);
cy.verifyElement(
groupsSelector.emailTableHeader,
groupsText.emailTableHeader
);
cy.get(groupsSelector.userEmptyPageIcon).should("be.visible");
cy.get(groupsSelector.userEmptyPageTitle).verifyVisibleElement(
"have.text",
groupsText.userEmptyPageTitle
);
cy.get(groupsSelector.userEmptyPageHelperText).verifyVisibleElement(
"have.text",
groupsText.userEmptyPageHelperText
);
// Granular Access Verifications
cy.get(groupsSelector.permissionsLink).click();
cy.get(groupsSelector.helperTextAdminAppAccess)
.eq(0)
.verifyVisibleElement("have.text", groupsText.enduserAccessHelperText);
cy.verifyElement(groupsSelector.resourcesApps, groupsText.resourcesApps);
cy.verifyElement(
groupsSelector.permissionsTableHeader,
groupsText.permissionsTableHeader
);
cy.get(groupsSelector.appsCreateCheck)
.should("be.visible")
.and("not.be.checked")
.and("be.disabled");
cy.verifyElement(groupsSelector.appsCreateLabel, groupsText.createLabel);
cy.verifyElement(
groupsSelector.appCreateHelperText,
groupsText.appCreateHelperText
);
cy.get(groupsSelector.appsDeleteCheck)
.should("be.visible")
.and("not.be.checked")
.and("be.disabled");
cy.verifyElement(groupsSelector.appsDeleteLabel, groupsText.deleteLabel);
cy.verifyElement(
groupsSelector.appDeleteHelperText,
groupsText.appDeleteHelperText
);
// Folder Permissions
cy.verifyElement(
groupsSelector.resourcesFolders,
groupsText.resourcesFolders
);
cy.get(groupsSelector.foldersCreateCheck)
.should("be.visible")
.and("not.be.checked")
.and("be.disabled");
cy.verifyElement(
groupsSelector.foldersCreateLabel,
groupsText.folderCreateLabel
);
cy.verifyElement(
groupsSelector.foldersHelperText,
groupsText.folderHelperText
);
// Workspace Variable Permissions
cy.verifyElement(
groupsSelector.resourcesWorkspaceVar,
groupsText.resourcesWorkspaceVar
);
cy.get(groupsSelector.workspaceVarCheckbox)
.should("be.visible")
.and("not.be.checked")
.and("be.disabled");
cy.verifyElement(
groupsSelector.workspaceCreateLabel,
groupsText.workspaceCreateLabel
);
cy.verifyElement(
groupsSelector.workspaceHelperText,
groupsText.workspaceHelperText
);
// Granular Permissions
cy.get(groupsSelector.granularLink).click();
cy.verifyElement(groupsSelector.nameTableHeader, groupsText.nameTableHeader);
cy.verifyElement(
groupsSelector.permissionsTableHeader,
groupsText.granularAccessPermissionHeader
);
cy.verifyElement(
`${groupsSelector.resourceHeader}:eq(1)`,
groupsText.resourcesTableHeader
);
cy.verifyElement(groupsSelector.appsText, groupsText.appsLink);
cy.get(groupsSelector.appEditRadio)
.should("be.visible")
.and("not.be.checked")
.and("be.disabled");
cy.verifyElement(groupsSelector.appEditLabel, groupsText.appEditLabelText);
cy.verifyElement(
groupsSelector.appEditHelperText,
groupsText.appEditHelperText
);
cy.get(groupsSelector.appViewRadio)
.should("be.visible")
.and("be.disabled")
.and("be.checked");
cy.verifyElement(groupsSelector.appViewLabel, groupsText.appViewLabel);
cy.verifyElement(
groupsSelector.appViewHelperText,
groupsText.appViewHelperText
);
cy.get(groupsSelector.appHideCheckbox).should("be.visible").and("be.enabled");
cy.verifyElement(
groupsSelector.appHideLabel,
groupsText.appHideLabelPermissionModal
);
cy.verifyElement(
groupsSelector.appHideHelperText,
groupsText.appHideHelperText
);
cy.get(groupsSelector.granularAccessPermission)
.trigger("mouseenter")
.click({ force: true });
cy.get(".modal-base").should("be.visible");
cy.get(groupsSelector.deletePermissionIcon)
.should("be.visible")
.and("be.enabled");
cy.get(groupsSelector.deletePermissionIcon).click();
cy.get(".confirm-dialogue-modal").should("be.visible");
cy.verifyElement(groupsSelector.deleteMessage, groupsText.deleteMessage);
cy.get(groupsSelector.yesButton).should("be.visible").and("be.enabled");
cy.get(groupsSelector.cancelButton).should("be.visible").and("be.enabled");
cy.contains("Cancel").click();
cy.get(groupsSelector.granularAccessPermission)
.trigger("mouseenter")
.click({ force: true });
cy.verifyElement(
`${groupsSelector.addEditPermissionModalTitle}:eq(2)`,
groupsText.editPermissionModalTitle
);
permissionModal();
cy.get(groupsSelector.customradio).should("be.visible").should("be.disabled");
cy.verifyElement(groupsSelector.customLabel, groupsText.customLabel);
cy.verifyElement(
groupsSelector.customHelperText,
groupsText.customHelperText
);
cy.verifyElement(groupsSelector.confimButton, groupsText.updateButtonText);
cy.get(groupsSelector.confimButton).should("be.disabled");
cy.verifyElement(groupsSelector.cancelButton, groupsText.cancelButton);
cy.get(groupsSelector.cancelButton).click();
//Add Modal
cy.verifyElement(groupsSelector.addAppButton, groupsText.addButton);
cy.get(groupsSelector.addAppButton)
.should("be.visible")
.and("be.enabled")
.click();
cy.verifyElement(
`${groupsSelector.addEditPermissionModalTitle}:eq(2)`,
groupsText.addPermissionModalTitle
);
permissionModal();
cy.get(groupsSelector.customradio).should("be.visible").should("be.disabled");
cy.verifyElement(groupsSelector.customLabel, groupsText.customLabel);
cy.verifyElement(
groupsSelector.customHelperText,
groupsText.customHelperText
);
cy.verifyElement(groupsSelector.confimButton, groupsText.addButtonText);
cy.get(groupsSelector.confimButton).should("be.disabled");
cy.verifyElement(groupsSelector.cancelButton, groupsText.cancelButton);
cy.get(groupsSelector.cancelButton).click();
};
const toggleCheckbox = (selector, toastSelector, toastMessage) => {
cy.get(selector).should("be.visible").uncheck();
cy.verifyToastMessage(toastSelector, toastMessage);
cy.get(selector).check();
cy.verifyToastMessage(toastSelector, toastMessage);
};
// Permission Modal Verification
export const permissionModal = () => {
cy.verifyElement(
groupsSelector.permissionNameLabel,
groupsText.permissionNameLabel
);
cy.verifyElement(
groupsSelector.permissionNameHelperText,
groupsText.permissionNameHelperText
);
cy.verifyElement(groupsSelector.permissionLabel, groupsText.permissionLabel);
cy.verifyElement(
groupsSelector.editPermissionLabel,
groupsText.editPermissionLabel
);
cy.verifyElement(
groupsSelector.editPermissionHelperText,
groupsText.editPermissionHelperText
);
cy.verifyElement(
groupsSelector.viewPermissionLabel,
groupsText.viewPermissionLabel
);
cy.verifyElement(
groupsSelector.viewPermissionHelperText,
groupsText.viewPermissionHelperText
);
cy.get(groupsSelector.hidePermissionInput).should("be.visible");
cy.verifyElement(groupsSelector.resourceLabel, groupsText.resourcesheader);
cy.get(groupsSelector.resourceContainer).should("be.visible");
cy.get(groupsSelector.allAppsRadio).should("be.visible").and("be.checked");
cy.verifyElement(groupsSelector.allAppsLabel, groupsText.allAppsLabel);
cy.verifyElement(
groupsSelector.allAppsHelperText,
groupsText.allAppsHelperText
);
};
export const addAppToGroup = (appName) => {
cy.get(groupsSelector.appsLink).click();
cy.wait(500);
cy.get(groupsSelector.appSearchBox).click();
cy.wait(500);
cy.get(groupsSelector.searchBoxOptions).contains(appName).click();
cy.get(groupsSelector.selectAddButton).click();
cy.contains("tr", appName)
.parent()
.within(() => {
cy.get("td input").eq(1).check();
});
cy.verifyToastMessage(
commonSelectors.toastMessage,
"App permissions updated"
);
};
export const createGroup = (groupName) => {
export const apiCreateGroup = (groupName) => {
return cy.getAuthHeaders().then((headers) => {
return cy
.request({
@ -650,14 +26,16 @@ export const createGroup = (groupName) => {
});
};
export const apiDeleteGroup = (groupId) => {
cy.getAuthHeaders().then((headers) => {
cy.request({
method: "DELETE",
url: `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}`,
headers: headers,
}).then((response) => {
expect(response.status).to.equal(200);
export const apiDeleteGroup = (groupName) => {
cy.apiGetGroupId(groupName).then((groupId) => {
cy.getAuthHeaders().then((headers) => {
cy.request({
method: "DELETE",
url: `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}`,
headers: headers,
}).then((response) => {
expect(response.status).to.equal(200);
});
});
});
};
@ -669,52 +47,6 @@ export const deleteGroup = (groupName, workspaceId) => {
});
};
export const createGroupAddAppAndUserToGroup = (groupName, email) => {
cy.getAuthHeaders().then((headers) => {
createGroup(groupName).then((groupId) => {
// Add app to group
cy.request({
method: "POST",
url: `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}/granular-permissions/app`,
headers: headers,
body: {
name: "Apps",
type: "app",
groupId: groupId,
isAll: false,
createResourcePermissionObject: {
canEdit: true,
canView: false,
hideFromDashboard: false,
resourcesToAdd: [{ appId: Cypress.env("appId") }],
},
},
}).then((response) => {
expect(response.status).to.equal(201);
});
cy.wait(2000);
cy.task("dbConnection", {
dbconfig: Cypress.env("app_db"),
sql: `select id from users where email='${email}';`,
}).then((resp) => {
const userId = resp.rows[0].id;
// Add user to group
cy.request({
method: "POST",
url: `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}/users`,
headers: headers,
body: {
userIds: [userId],
groupId: groupId,
},
}).then((response) => {
expect(response.status).to.equal(201);
});
});
});
});
};
export const OpenGroupCardOption = (groupName) => {
cy.get(groupsSelector.groupLink(groupName))
.trigger("mouseenter")
@ -775,61 +107,6 @@ export const groupPermission = (
});
};
export const duplicateGroup = () => {
OpenGroupCardOption(groupName);
cy.get(groupsSelector.duplicateOption).click();
};
export const updateRoleUI = (user, role, email, message) => {
cy.get(groupsSelector.groupLink(user)).click();
cy.get(groupsSelector.usersLink).click();
cy.get(`[data-cy="${email}-user-row"] > :nth-child(3)`).click();
cy.get('[data-cy="modal-title"] > .tj-text-md').should(
"have.text",
"Edit user role"
);
cy.get('[data-cy="user-email"]').should("have.text", email);
cy.get(groupsSelector.userRoleLabel).should("have.text", groupsText.userRole);
cy.get(groupsSelector.warningText).should(
"have.text",
groupsText.warningText
);
cy.get(groupsSelector.cancelButton)
.should("have.text", groupsText.cancelButton)
.and("be.enabled");
cy.get(groupsSelector.confimButton).should("be.disabled");
cy.get(
".css-nwhe5y-container > .react-select__control > .react-select__value-container"
)
.click()
.type(`${role}{enter}`);
cy.get(groupsSelector.confimButton)
.should("be.enabled")
.and("have.text", groupsText.continueButtonText)
.click();
cy.get('[data-cy="modal-body"]').should("have.text", message);
cy.get(groupsSelector.cancelButton).click();
cy.get(`[data-cy="${email}-user-row"] > :nth-child(3)`).click();
cy.get(
".css-nwhe5y-container > .react-select__control > .react-select__value-container"
)
.click()
.type(`${role}{enter}`);
cy.get(groupsSelector.confimButton)
.should("be.enabled")
.and("have.text", groupsText.continueButtonText)
.click();
cy.get(groupsSelector.confimButton).click();
if (user != "Admin") {
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.roleUpdateToastMessage
);
}
cy.get(groupsSelector.groupLink(role)).click();
cy.get(`[data-cy="${email}-user-row"]`).should("exist");
};
export const updateRole = (user, role, email, message = null) => {
cy.get(groupsSelector.groupLink(user)).click();
cy.get(groupsSelector.usersLink).click();
@ -897,25 +174,6 @@ export const inviteUserBasedOnRole = (firstName, email, role = "end-user") => {
cy.get(commonSelectors.dashboardIcon).click();
};
export const verifyBasicPermissions = (canCreate = true) => {
cy.get(commonSelectors.dashboardAppCreateButton).should(
canCreate ? "be.enabled" : "be.disabled"
);
cy.get(commonSelectors.createNewFolderButton).should(
canCreate ? "exist" : "not.exist"
);
cy.get('[data-cy="database-icon"]').should(canCreate ? "exist" : "not.exist");
cy.get(commonSelectors.workspaceConstantsIcon).should(
canCreate ? "exist" : "not.exist"
);
cy.ifEnv("Enterprise", () => {
cy.get(commonSelectors.globalDataSourceIcon).should(
canCreate ? "exist" : "not.exist"
);
});
};
export const setupWorkspaceAndInviteUser = (
workspaceName,
workspaceSlug,
@ -934,13 +192,6 @@ export const setupWorkspaceAndInviteUser = (
cy.wait(2000);
};
export const verifySettingsAccess = (shouldExist = true) => {
cy.get(commonSelectors.settingsIcon).click();
cy.get(commonSelectors.workspaceSettings).should(
shouldExist ? "exist" : "not.exist"
);
};
export const verifyUserPrivileges = (
expectedButtonState,
shouldHaveWorkspaceSettings
@ -974,3 +225,35 @@ export const verifyUserRole = (userIdAlias, expectedRole, expectedGroups) => {
});
});
};
export const apiAddUserToGroup = (groupId, email) => {
return cy.getAuthHeaders().then((headers) => {
return cy
.request({
method: "GET",
url: `${Cypress.env("server_host")}/api/organization-users`,
headers: headers,
log: false,
})
.then((response) => {
expect(response.status).to.equal(200);
const user = response.body.users.find((u) => u.email === email);
const userId = user.user_id;
return cy
.request({
method: "POST",
url: `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}/users`,
headers: headers,
body: {
userIds: [userId],
groupId: groupId,
},
log: false,
})
.then((addResponse) => {
expect(addResponse.status).to.equal(201);
return userId;
});
});
});
};

View file

@ -1,8 +1,8 @@
import { commonSelectors, cyParamName } from "Selectors/common";
import { ssoSelector } from "Selectors/manageSSO";
import { ssoText } from "Texts/manageSSO";
import * as common from "Support/utils/common";
import { commonText } from "Texts/common";
import { ssoText } from "Texts/manageSSO";
export const generalSettings = () => {
cy.get(ssoSelector.enableSignUpToggle).check();

View file

@ -1,13 +1,13 @@
import { commonSelectors, cyParamName } from "Selectors/common";
import { usersText } from "Texts/manageUsers";
import { usersSelector } from "Selectors/manageUsers";
import { ssoSelector } from "Selectors/manageSSO";
import { ssoText } from "Texts/manageSSO";
import * as common from "Support/utils/common";
import { commonText } from "Texts/common";
import { usersSelector } from "Selectors/manageUsers";
import { onboardingSelectors } from "Selectors/onboarding";
const envVar = Cypress.env("environment");
import * as common from "Support/utils/common";
import { fillInputField } from "Support/utils/common";
import { commonText } from "Texts/common";
import { ssoText } from "Texts/manageSSO";
import { usersText } from "Texts/manageUsers";
const envVar = Cypress.env("environment");
export const manageUsersElements = () => {
cy.get(
@ -480,7 +480,11 @@ export const openEditUserDetails = (
verifyUserStatusAndMetadata(email, activeStatusText, expectedMetadata);
cy.contains("td", email)
navigateToEditUser(email);
};
export const navigateToEditUser = (email) => {
cy.contains("td", email)
.parent()
.within(() => {
cy.get('[data-cy="user-actions-button"]').click();
@ -489,6 +493,3 @@ export const openEditUserDetails = (
.verifyVisibleElement("have.text", "Edit user details")
.click();
};

View file

@ -1,15 +1,10 @@
import { commonSelectors } from "Selectors/common";
import { commonText } from "Texts/common";
import { dashboardText } from "Texts/dashboard";
import {
verifyandModifyUserRole,
verifyandModifySizeOftheCompany,
} from "Support/utils/selfHostSignUp";
import { navigateToManageUsers, logout } from "Support/utils/common";
import { ssoSelector } from "Selectors/manageSSO";
import { ssoText } from "Texts/manageSSO";
import { onboardingSelectors } from "Selectors/onboarding";
import { logout, navigateToManageUsers } from "Support/utils/common";
import { fetchAndVisitInviteLink } from "Support/utils/manageUsers";
import { commonText } from "Texts/common";
import { ssoText } from "Texts/manageSSO";
import { onboardingText } from "Texts/onboarding";
export const verifyConfirmEmailPage = (email) => {

View file

@ -0,0 +1,173 @@
import { commonSelectors } from "Selectors/common";
import { commonEeSelectors } from "Selectors/eeCommon";
import { cyParamName, groupsSelector } from "Selectors/manageGroups";
import { groupsText } from "Texts/manageGroups";
export const createGroupViaUI = (groupName) => {
cy.get(groupsSelector.createNewGroupButton).click();
cy.get(groupsSelector.addNewGroupModalTitle).verifyVisibleElement(
"have.text",
groupsText.cardTitle
);
cy.clearAndType(groupsSelector.groupNameInput, groupName);
cy.get(groupsSelector.createGroupButton).should("be.enabled").click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.groupCreatedToast
);
};
export const verifyGroupCreatedInSidebar = (groupName) => {
cy.get(groupsSelector.groupLink(groupName))
.should("be.visible")
.and("contain.text", groupName);
};
export const renameGroupViaUI = (oldName, newName) => {
cy.get(groupsSelector.groupLink(oldName)).click();
cy.get(groupsSelector.groupNameUpdateLink).should("be.visible").click();
cy.clearAndType(groupsSelector.groupNameInput, newName);
cy.get(groupsSelector.createGroupButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.groupNameUpdateSucessToast
);
};
export const deleteGroupViaUI = (groupName) => {
cy.get(groupsSelector.groupLink(groupName)).click();
cy.get(groupsSelector.groupLink(groupName)).realHover();
cy.wait(2000).then(() => {
cy.get(
`[data-cy="${cyParamName(groupName)}-list-item"] > :nth-child(2) > .tj-base-btn`
).click({ force: true });
});
cy.get(groupsSelector.deleteGroupOption).click();
cy.get(commonSelectors.buttonSelector("Yes")).click();
};
export const verifyGroupRemovedFromSidebar = (groupName) => {
cy.get(groupsSelector.groupLink(groupName)).should("not.exist");
};
export const addGranularPermissionViaUI = (permissionName, options = {}) => {
const {
resourceType = "app",
permission = "edit",
scope = "all",
resources = [],
} = options;
cy.ifEnv("Community", () => {
cy.get(groupsSelector.addAppsButton).click();
});
cy.ifEnv("Enterprise", () => {
cy.get(groupsSelector.addPermissionButton).click();
if (resourceType === "app") {
cy.get(groupsSelector.addAppButton).click();
} else if (resourceType === "workflow") {
cy.get(groupsSelector.addWorkflowButton).click();
} else if (resourceType === "datasource") {
cy.get(groupsSelector.addDatasourceButton).click();
}
});
cy.clearAndType(groupsSelector.permissionNameInput, permissionName);
if (resourceType === "app") {
if (permission === "view") {
cy.get(groupsSelector.viewPermissionRadio).check();
} else if (permission === "edit") {
cy.get(groupsSelector.editPermissionRadio).check();
}
} else if (resourceType === "workflow") {
if (permission === "execute") {
cy.get(groupsSelector.executeWorkflowradio).check();
} else if (permission === "build") {
cy.get(groupsSelector.buildWorkflowradio).check();
}
} else if (resourceType === "datasource") {
if (permission === "buildWith") {
cy.get(groupsSelector.buildWithDatasourceRadio).check();
} else if (permission === "configure") {
cy.get(groupsSelector.configureDatasourceradio).check();
}
}
if (scope === "custom") {
cy.get(groupsSelector.customRadio).check();
if (resources.length > 0) {
resources.forEach((resource) => {
cy.get(groupsSelector.resourceSelector).click();
cy.get(groupsSelector.searchBoxOptions).contains(resource).click();
});
}
} else {
cy.get(groupsSelector.allAppsRadio).check();
}
cy.get(groupsSelector.confimButton).click();
};
export const switchBetweenAllAndCustom = (targetScope) => {
if (targetScope === "all") {
cy.get(groupsSelector.allAppsRadio).check();
cy.get(groupsSelector.allAppsRadio).should("be.checked");
cy.get(groupsSelector.customRadio).should("not.be.checked");
} else if (targetScope === "custom") {
cy.get(groupsSelector.customRadio).check();
cy.get(groupsSelector.customRadio).should("be.checked");
cy.get(groupsSelector.allAppsRadio).should("not.be.checked");
cy.get(".css-b62m3t-container").should("be.visible");
}
};
export const openGroupThreeDotMenu = (groupName) => {
cy.get(groupsSelector.groupLink(groupName)).realHover()
cy.get(groupsSelector.groupLink(groupName)).then(() => {
cy.get('[datacy="groups-list-option-button"]').click();
});
};
export const verifyDuplicateModal = (originalGroupName) => {
cy.get('[data-cy="modal-title"]')
.should("be.visible")
.and("contain.text", "Duplicate group");
cy.verifyElement(
'[data-cy="modal-message"]',
"Duplicate the following parts of the group"
);
cy.verifyElement('[data-cy="users-label"]', "Users");
cy.get('[data-cy="users-check-input"]')
.should("be.visible")
.and("be.checked");
cy.verifyElement('[data-cy="permissions-label"]', "Permissions");
cy.get('[data-cy="permissions-check-input"]')
.should("be.visible")
.and("be.checked");
cy.verifyElement('[data-cy="apps-label"]', "Apps");
cy.get('[data-cy="apps-check-input"]').should("be.visible").and("be.checked");
cy.ifEnv("Enterprise", () => {
cy.verifyElement('[data-cy="workflows-label"]', "Workflows");
cy.get('[data-cy="workflows-check-input"]')
.should("be.visible")
.and("be.checked");
cy.verifyElement('[data-cy="datasources-label"]', "Datasources");
cy.get('[data-cy="datasources-check-input"]')
.should("be.visible")
.and("be.checked");
});
cy.verifyElement(groupsSelector.cancelButton, "Cancel");
cy.get(groupsSelector.cancelButton).should("be.visible").and("be.enabled");
cy.verifyElement(commonEeSelectors.confirmButton, "Duplicate");
cy.get(commonEeSelectors.confirmButton)
.should("be.visible")
.and("be.enabled");
};

View file

@ -0,0 +1,855 @@
import { commonSelectors } from "Selectors/common";
import { groupsSelector } from "Selectors/manageGroups";
import { groupsText } from "Texts/manageGroups";
export const verifyAdminHelperText = (index = 0) => {
cy.get(groupsSelector.helperTextAdminAppAccess)
.eq(index)
.should("be.visible")
.and("contain.text", "Admin has all permissions. This is not editable");
cy.get(groupsSelector.helperTextAdminAppAccess)
.eq(index)
.find("a")
.should("be.visible")
.and("have.text", "read documentation")
.and("have.attr", "href")
.and("include", "docs.tooljet.ai/docs/tutorial/manage-users-groups");
};
export const verifyEditUserRoleModal = (userEmail) => {
cy.get('[data-cy="modal-title"]')
.last()
.within(() => {
cy.get("span").should("be.visible").and("contain.text", "Edit user role");
cy.get('[data-cy="user-email"]')
.should("be.visible")
.and("have.text", userEmail);
});
cy.get(groupsSelector.userRoleLabel)
.should("be.visible")
.and("have.text", groupsText.userRole);
cy.get(groupsSelector.warningText)
.should("be.visible")
.and("have.text", groupsText.warningText);
cy.get(".react-select__control").should("be.visible");
cy.get(".react-select__placeholder")
.should("be.visible")
.and("contain.text", "Select new role of user");
cy.get(groupsSelector.cancelButton)
.should("be.visible")
.and("have.text", groupsText.cancelButton)
.and("be.enabled");
cy.get(groupsSelector.confimButton)
.should("be.visible")
.and("have.text", groupsText.continueButtonText)
.and("be.disabled");
cy.get('[data-cy="modal-close-button"]').should("be.visible");
};
export const toggleAllPermissions = (status = ["uncheck", "check"]) => {
permissions.forEach((permissionSelector) => {
cy.get(permissionSelector).should("be.visible")[status[0]]();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
cy.get(permissionSelector).should("be.visible")[status[1]]();
cy.verifyToastMessage(
commonSelectors.toastMessage,
groupsText.permissionUpdatedToast
);
});
};
export const verifyDeleteConfirmationModal = () => {
cy.get(".confirm-dialogue-modal").should("be.visible");
cy.verifyElement(groupsSelector.deleteMessage, groupsText.deleteMessage);
cy.get(groupsSelector.yesButton).should("be.visible").and("be.enabled");
cy.get(groupsSelector.cancelButton).should("be.visible").and("be.enabled");
};
export const verifyGranularEditModal = (role) => {
cy.get(groupsSelector.granularAccessPermission).realHover();
cy.get('[data-cy="edit-apps-granular-access"]').click();
cy.get(".modal-base").should("be.visible");
cy.get(groupsSelector.deletePermissionIcon)
.should("be.visible")
.and("be.enabled");
cy.get(groupsSelector.deletePermissionIcon).click();
verifyDeleteConfirmationModal();
cy.contains("Cancel").click();
cy.get(groupsSelector.granularAccessPermission)
.realHover()
.click({ force: true });
cy.verifyElement(
`${groupsSelector.addEditPermissionModalTitle}:eq(2)`,
groupsText.editPermissionModalTitle
);
permissionModal();
if (role === "builder" || role === "enduser") {
cy.get(groupsSelector.customRadio).should("be.disabled");
} else {
cy.get(groupsSelector.customRadio).should("be.enabled");
}
cy.verifyElement(groupsSelector.customLabel, groupsText.customLabel);
cy.verifyElement(
groupsSelector.customHelperText,
groupsText.customHelperText
);
cy.verifyElement(groupsSelector.confimButton, groupsText.updateButtonText);
cy.get(groupsSelector.confimButton).should("be.disabled");
cy.verifyElement(groupsSelector.cancelButton, groupsText.cancelButton);
cy.get(groupsSelector.cancelButton).click();
};
export const verifyGranularAddModal = (role) => {
cy.ifEnv("Community", () => {
cy.get(groupsSelector.addAppsButton)
.should("be.visible")
.and("be.enabled")
.click();
});
cy.ifEnv("Enterprise", () => {
cy.get(groupsSelector.addPermissionButton)
.should("be.visible")
.and("be.enabled")
.click();
cy.get(groupsSelector.addAppButton).click();
});
cy.verifyElement(
`${groupsSelector.addEditPermissionModalTitle}:eq(2)`,
groupsText.addPermissionModalTitle
);
permissionModal();
if (role === "builder" || role === "enduser") {
cy.get(groupsSelector.customRadio).should("be.disabled");
} else {
cy.get(groupsSelector.customRadio).should("be.enabled");
}
cy.verifyElement(groupsSelector.customLabel, groupsText.customLabel);
cy.verifyElement(
groupsSelector.customHelperText,
groupsText.customHelperText
);
cy.verifyElement(groupsSelector.confimButton, groupsText.addButtonText);
cy.get(groupsSelector.confimButton).should("be.disabled");
cy.verifyElement(groupsSelector.cancelButton, groupsText.cancelButton);
cy.get(groupsSelector.cancelButton).click();
};
export const verifyEnduserHelperText = (index = 0) => {
cy.get(groupsSelector.helperTextAdminAppAccess)
.eq(index)
.should("be.visible")
.and("contain.text", "End-user can only have permission to view apps");
cy.get(groupsSelector.helperTextAdminAppAccess)
.eq(index)
.find("a")
.should("be.visible")
.and("have.text", "read documentation")
.and("have.attr", "href")
.and("include", "docs.tooljet.ai/docs/tutorial/manage-users-groups");
};
export const verifyGranularPermissionModalUI = (
resourceType,
isEdit = false,
permissionName = ""
) => {
// Permission name section
cy.get(groupsSelector.permissionNameLabel).verifyVisibleElement(
"have.text",
groupsText.permissionNameLabel
);
cy.get(groupsSelector.permissionNameInput).should("be.visible");
if (isEdit) {
cy.get(groupsSelector.permissionNameInput).should(
"have.value",
permissionName
);
} else {
cy.get(groupsSelector.permissionNameInput).should(
"have.attr",
"placeholder"
);
}
cy.get(groupsSelector.permissionNameHelperText).verifyVisibleElement(
"have.text",
groupsText.permissionNameHelperText
);
// Permission section
cy.get(groupsSelector.permissionLabel).verifyVisibleElement(
"have.text",
groupsText.permissionLabel
);
if (resourceType === "app") {
cy.verifyElement(
groupsSelector.editPermissionLabel,
groupsText.editPermissionLabel
);
cy.verifyElement(
groupsSelector.editPermissionHelperText,
groupsText.editPermissionHelperText
);
cy.verifyElement(
groupsSelector.viewPermissionLabel,
groupsText.viewPermissionLabel
);
cy.verifyElement(
groupsSelector.viewPermissionHelperText,
groupsText.viewPermissionHelperText
);
cy.get(groupsSelector.hidePermissionInput).should("be.visible");
cy.verifyElement(
groupsSelector.appHidePermissionModalLabel,
groupsText.appHideLabel
);
cy.verifyElement(
groupsSelector.appHidePermissionModalHelperText,
groupsText.appHideHelperText
);
}
if (resourceType === "workflow") {
cy.verifyElement(groupsSelector.workflowsBuildLabel, "Build");
cy.verifyElement(
groupsSelector.workflowsBuildHelperText,
"Access to workflow builder"
);
cy.verifyElement(groupsSelector.workflowsExecuteLabel, "Execute");
cy.verifyElement(
groupsSelector.workflowsExecuteHelperText,
"Only able to execute the workflow"
);
}
if (resourceType === "datasource") {
cy.verifyElement(groupsSelector.datasourcesConfigureLabel, "Configure");
cy.verifyElement(
groupsSelector.datasourcesConfigureHelperText,
"Access and edit connection detail"
);
cy.verifyElement(groupsSelector.datasourcesBuildWithLabel, "Build with");
cy.verifyElement(
groupsSelector.datasourcesBuildWithHelperText,
"Use in apps & workflows"
);
}
// Resources section
cy.get(groupsSelector.resourceLabel).verifyVisibleElement(
"have.text",
groupsText.resourcesheader
);
cy.get(groupsSelector.allAppsRadio).should("be.visible");
if (isEdit) {
cy.verifyElement(groupsSelector.allAppsLabel, groupsText.allAppsLabel);
} else {
cy.verifyElement(groupsSelector.allAppsLabel, groupsText.groupChipText);
}
cy.verifyElement(
groupsSelector.allAppsHelperText,
groupsText.allAppsHelperText
);
cy.get(groupsSelector.customRadio).should("be.visible");
cy.verifyElement(groupsSelector.customLabel, groupsText.customLabel);
cy.verifyElement(
groupsSelector.customHelperText,
groupsText.customHelperText
);
cy.verifyElement(
groupsSelector.confimButton,
isEdit ? groupsText.updateButtonText : groupsText.addButtonText
);
cy.verifyElement(groupsSelector.cancelButton, groupsText.cancelButton);
if (isEdit) {
cy.get(groupsSelector.deletePermissionIcon).should("be.visible");
}
};
export const verifyGranularPermissionModalStates = (
resourceType,
role,
customStateOverride = null
) => {
const stateConfig = {
app: {
builder: {
editRadio: { checked: true, enabled: true },
viewRadio: { checked: false, enabled: true },
hideCheckbox: { enabled: false },
allAppsRadio: { checked: true, enabled: false },
customRadio: { checked: false, enabled: false },
},
enduser: {
editRadio: { checked: false, enabled: false },
viewRadio: { checked: true, enabled: false },
hideCheckbox: { enabled: true },
allAppsRadio: { checked: true, enabled: false },
customRadio: { checked: false, enabled: false },
},
custom: {
editRadio: { checked: true, enabled: true },
viewRadio: { checked: false, enabled: true },
hideCheckbox: { enabled: false },
allAppsRadio: { checked: true, enabled: true },
customRadio: { checked: false, enabled: true },
},
},
workflow: {
builder: {
buildRadio: { checked: true, enabled: true },
executeRadio: { checked: false, enabled: true },
allAppsRadio: { checked: true, enabled: false },
customRadio: { checked: false, enabled: false },
},
enduser: {
buildRadio: { checked: false, enabled: false },
executeRadio: { checked: true, enabled: false },
allAppsRadio: { checked: true, enabled: false },
customRadio: { checked: false, enabled: false },
},
custom: {
buildRadio: { checked: true, enabled: true },
executeRadio: { checked: false, enabled: true },
allAppsRadio: { checked: true, enabled: true },
customRadio: { checked: false, enabled: true },
},
},
datasource: {
builder: {
configureRadio: { checked: true, enabled: true },
buildWithRadio: { checked: false, enabled: true },
allAppsRadio: { checked: true, enabled: false },
customRadio: { checked: false, enabled: false },
},
custom: {
configureRadio: { checked: true, enabled: true },
buildWithRadio: { checked: false, enabled: true },
allAppsRadio: { checked: true, enabled: true },
customRadio: { checked: false, enabled: true },
},
},
};
// Get the base config
let config = stateConfig[resourceType][role];
// If customStateOverride is provided and role is 'custom', merge it with the default
if (customStateOverride && role === "custom") {
config = { ...config, ...customStateOverride };
}
if (resourceType === "app") {
cy.get(groupsSelector.editPermissionRadio)
.should("be.visible")
.and(config.editRadio.checked ? "be.checked" : "not.be.checked")
.and(config.editRadio.enabled ? "be.enabled" : "be.disabled");
cy.get(groupsSelector.viewPermissionRadio)
.should("be.visible")
.and(config.viewRadio.checked ? "be.checked" : "not.be.checked")
.and(config.viewRadio.enabled ? "be.enabled" : "be.disabled");
cy.get(groupsSelector.hidePermissionInput)
.should("be.visible")
.and(config.hideCheckbox.enabled ? "be.enabled" : "be.disabled");
}
if (resourceType === "workflow") {
cy.get(groupsSelector.buildWorkflowradio)
.should("be.visible")
.and(config.buildRadio.checked ? "be.checked" : "not.be.checked")
.and(config.buildRadio.enabled ? "be.enabled" : "be.disabled");
cy.get(groupsSelector.executeWorkflowradio)
.should("be.visible")
.and(config.executeRadio.checked ? "be.checked" : "not.be.checked")
.and(config.executeRadio.enabled ? "be.enabled" : "be.disabled");
}
if (resourceType === "datasource") {
cy.get(groupsSelector.configureDatasourceradio)
.should("be.visible")
.and(config.configureRadio.checked ? "be.checked" : "not.be.checked")
.and(config.configureRadio.enabled ? "be.enabled" : "be.disabled");
cy.get(groupsSelector.buildWithDatasourceRadio)
.should("be.visible")
.and(config.buildWithRadio.checked ? "be.checked" : "not.be.checked")
.and(config.buildWithRadio.enabled ? "be.enabled" : "be.disabled");
}
cy.get(groupsSelector.allAppsRadio)
.should("be.visible")
.and(config.allAppsRadio.checked ? "be.checked" : "not.be.checked")
.and(config.allAppsRadio.enabled ? "be.enabled" : "be.disabled");
cy.get(groupsSelector.customRadio)
.should("be.visible")
.and(config.customRadio.checked ? "be.checked" : "not.be.checked")
.and(config.customRadio.enabled ? "be.enabled" : "be.disabled");
};
export const verifyEmptyStates = (customGroup = false) => {
// Users empty state
cy.get(groupsSelector.usersLink).click();
cy.get('[data-cy="user-group-search-btn"]').should("be.visible");
cy.verifyElement(
groupsSelector.nameTableHeader,
groupsText.userNameTableHeader
);
cy.verifyElement(
groupsSelector.emailTableHeader,
groupsText.emailTableHeader
);
cy.get(groupsSelector.userEmptyPageIcon).should("be.visible");
cy.get(groupsSelector.userEmptyPageTitle).verifyVisibleElement(
"have.text",
groupsText.userEmptyPageTitle
);
cy.get(groupsSelector.userEmptyPageHelperText).verifyVisibleElement(
"have.text",
groupsText.userEmptyPageHelperText
);
if (customGroup) {
// Granular permissions empty state
cy.get(groupsSelector.granularLink).click();
cy.get(groupsSelector.granularEmptyPageIcon).should("be.visible");
cy.get(groupsSelector.emptyPagePermissionTitle).verifyVisibleElement(
"have.text",
groupsText.emptyPagePermissionTitle
);
cy.get(groupsSelector.emptyPagePermissionHelperText).verifyVisibleElement(
"have.text",
groupsText.emptyPagePermissionHelperText
);
}
};
export const verifyGroupLinks = () => {
const links = [
{ selector: groupsSelector.usersLink, text: groupsText.usersLink },
{
selector: groupsSelector.permissionsLink,
text: groupsText.permissionsLink,
},
{ selector: groupsSelector.granularLink, text: "Granular access" },
];
links.forEach(({ selector, text }) => {
cy.get(selector).verifyVisibleElement("have.text", text);
});
};
export const commonGroupVerification = () => {
cy.verifyElement(
groupsSelector.textDefaultGroup,
groupsText.textDefaultGroup
);
cy.verifyElement(groupsSelector.usersLink, groupsText.usersLink);
cy.verifyElement(groupsSelector.permissionsLink, groupsText.permissionsLink);
cy.verifyElement(groupsSelector.granularLink, "Granular access");
cy.get(groupsSelector.usersLink).click();
cy.verifyElement(
groupsSelector.nameTableHeader,
groupsText.userNameTableHeader
);
cy.verifyElement(
groupsSelector.emailTableHeader,
groupsText.emailTableHeader
);
};
export const permissions =
Cypress.env("environment") === "Community"
? [
groupsSelector.appsCreateCheck,
groupsSelector.appsDeleteCheck,
groupsSelector.foldersCreateCheck,
groupsSelector.workspaceVarCheckbox,
]
: [
groupsSelector.appsCreateCheck,
groupsSelector.appsDeleteCheck,
groupsSelector.appPromoteCheck,
groupsSelector.appReleaseCheck,
groupsSelector.workflowsCreateCheck,
groupsSelector.workflowsDeleteCheck,
groupsSelector.datasourcesCreateCheck,
groupsSelector.datasourcesDeleteCheck,
groupsSelector.foldersCreateCheck,
groupsSelector.workspaceVarCheckbox,
];
export const verifyCheckPermissionStates = (roleType, action = null) => {
const roleConfig = {
admin: { checked: true, enabled: false },
enduser: { checked: false, enabled: false },
builder: { checked: true, enabled: true },
custom: { checked: false, enabled: true },
};
const config = roleConfig[roleType];
permissions.forEach((permissionSelector) => {
cy.get(permissionSelector)
.should("be.visible")
.and(config.checked ? "be.checked" : "not.be.checked")
.and(config.enabled ? "be.enabled" : "be.disabled");
});
};
export const verifyPermissionCheckBoxLabelsAndHelperTexts = () => {
const commonPermissions = [
{ selector: groupsSelector.resourcesApps, text: groupsText.resourcesApps },
{
selector: groupsSelector.permissionsTableHeader,
text: groupsText.permissionsTableHeader,
},
{ selector: groupsSelector.appsCreateLabel, text: groupsText.createLabel },
{
selector: groupsSelector.appCreateHelperText,
text: groupsText.appCreateHelperText,
},
{ selector: groupsSelector.appsDeleteLabel, text: groupsText.deleteLabel },
{
selector: groupsSelector.appDeleteHelperText,
text: groupsText.appDeleteHelperText,
},
{
selector: groupsSelector.resourcesFolders,
text: groupsText.resourcesFolders,
},
{
selector: groupsSelector.foldersCreateLabel,
text: groupsText.folderCreateLabel,
},
{
selector: groupsSelector.foldersHelperText,
text: groupsText.folderHelperText,
},
{
selector: groupsSelector.resourcesWorkspaceVar,
text: groupsText.resourcesWorkspaceVar,
},
{
selector: groupsSelector.workspaceCreateLabel,
text: groupsText.workspaceCreateLabel,
},
{
selector: groupsSelector.workspaceHelperText,
text: groupsText.workspaceHelperText,
},
];
commonPermissions.forEach(({ selector, text }) => {
cy.verifyElement(selector, text);
});
cy.ifEnv("Enterprise", () => {
const enterprisePermissions = [
{ selector: groupsSelector.appPromoteLabel, text: "Promote" },
{
selector: groupsSelector.appPromoteHelperText,
text: "Promote any app in this workspace",
},
{ selector: groupsSelector.appReleaseLabel, text: "Release" },
{
selector: groupsSelector.appReleaseHelperText,
text: "Release any app in this workspace",
},
{ selector: groupsSelector.workflowsCreateLabel, text: "Create" },
{
selector: groupsSelector.workflowsCreateHelperText,
text: "Create workflow in this workspace",
},
{ selector: groupsSelector.workflowsDeleteLabel, text: "Delete" },
{
selector: groupsSelector.workflowsDeleteHelperText,
text: "Delete any workflow in this workspace",
},
{ selector: groupsSelector.datasourcesCreateLabel, text: "Create" },
{
selector: groupsSelector.datasourcesCreateHelperText,
text: "Create data source connections in this workspace",
},
{ selector: groupsSelector.datasourcesDeleteLabel, text: "Delete" },
{
selector: groupsSelector.datasourcesDeleteHelperText,
text: "Delete any data source in this workspace",
},
];
enterprisePermissions.forEach(({ selector, text }) => {
cy.verifyElement(selector, text);
});
});
};
export const verifyGranularAccessByRole = (role) => {
const roleConfig = {
admin: {
appEditRadio: { checked: true, enabled: false },
appViewRadio: { checked: false, enabled: false },
appHideCheckbox: { enabled: false },
workflowBuildRadio: { checked: true, enabled: false },
workflowExecuteRadio: { checked: false, enabled: false },
datasourceConfigureRadio: { checked: true, enabled: false },
datasourceBuildWithRadio: { checked: false, enabled: false },
addButtonEnabled: false,
verifyHelperTexts: true,
hasDatasource: true,
},
builder: {
appEditRadio: { checked: true, enabled: true },
appViewRadio: { checked: false, enabled: true },
appHideCheckbox: { enabled: false },
workflowBuildRadio: { checked: true, enabled: true },
workflowExecuteRadio: { checked: false, enabled: true },
datasourceConfigureRadio: { checked: true, enabled: true },
datasourceBuildWithRadio: { checked: false, enabled: true },
addButtonEnabled: true,
verifyHelperTexts: false,
hasDatasource: true,
},
enduser: {
appEditRadio: { checked: false, enabled: false },
appViewRadio: { checked: true, enabled: false },
appHideCheckbox: { enabled: true },
workflowBuildRadio: { checked: false, enabled: false },
workflowExecuteRadio: { checked: true, enabled: false },
addButtonEnabled: true,
verifyHelperTexts: false,
hasDatasource: false,
},
};
const config = roleConfig[role];
cy.get(groupsSelector.granularLink).click();
if (role === "admin") {
cy.verifyElement(
groupsSelector.nameTableHeader,
groupsText.nameTableHeader
);
cy.verifyElement(
groupsSelector.permissionsTableHeader,
groupsText.granularAccessPermissionHeader
);
cy.verifyElement(
`${groupsSelector.resourceHeader}:eq(1)`,
groupsText.resourcesTableHeader
);
}
cy.verifyElement(groupsSelector.appsText, " Apps");
cy.get(groupsSelector.appEditRadio)
.should("be.visible")
.and(config.appEditRadio.checked ? "be.checked" : "not.be.checked")
.and(
config.appEditRadio.enabled ? "be.enabled" : "have.attr",
config.appEditRadio.enabled ? "" : "disabled"
);
cy.get(groupsSelector.appViewRadio)
.should("be.visible")
.and(
config.appViewRadio.enabled ? "be.enabled" : "have.attr",
config.appViewRadio.enabled ? "" : "disabled"
);
cy.get(groupsSelector.appHideCheckbox)
.should("be.visible")
.and(config.appHideCheckbox.enabled ? "be.enabled" : "be.disabled");
if (config.verifyHelperTexts) {
cy.verifyElement(groupsSelector.appEditLabel, groupsText.appEditLabelText);
cy.verifyElement(
groupsSelector.appEditHelperText,
groupsText.appEditHelperText
);
cy.verifyElement(groupsSelector.appViewLabel, groupsText.appViewLabel);
cy.verifyElement(
groupsSelector.appViewHelperText,
groupsText.appViewHelperText
);
cy.verifyElement(
groupsSelector.appHideHelperText,
groupsText.appHideHelperText
);
}
cy.verifyElement(groupsSelector.groupChip("All apps"), "All apps");
cy.ifEnv("Community", () => {
cy.get(groupsSelector.addAppButton).should(
config.addButtonEnabled ? "be.enabled" : "be.disabled"
);
});
cy.ifEnv("Enterprise", () => {
cy.verifyElement(groupsSelector.workflowsText, "Workflows");
cy.get(groupsSelector.workflowsBuildRadio)
.should("be.visible")
.and(config.workflowBuildRadio.checked ? "be.checked" : "not.be.checked")
.and(
config.workflowBuildRadio.enabled ? "be.enabled" : "have.attr",
config.workflowBuildRadio.enabled ? "" : "disabled"
);
cy.get(groupsSelector.workflowsExecuteRadio)
.should("be.visible")
.and(
config.workflowExecuteRadio.enabled ? "be.enabled" : "have.attr",
config.workflowExecuteRadio.enabled ? "" : "disabled"
);
if (config.verifyHelperTexts) {
cy.verifyElement(groupsSelector.workflowsBuildLabel, "Build");
cy.verifyElement(
groupsSelector.workflowsBuildHelperText,
"Access to workflow builder"
);
cy.verifyElement(groupsSelector.workflowsExecuteLabel, "Execute");
cy.verifyElement(
groupsSelector.workflowsExecuteHelperText,
"Only able to execute the workflow"
);
}
cy.verifyElement(
groupsSelector.groupChip("All workflows"),
"All workflows"
);
if (config.hasDatasource) {
cy.verifyElement(groupsSelector.datasourcesText, " Data sources");
cy.get(groupsSelector.datasourcesConfigureRadio)
.should("be.visible")
.and(
config.datasourceConfigureRadio.checked
? "be.checked"
: "not.be.checked"
)
.and(
config.datasourceConfigureRadio.enabled ? "be.enabled" : "have.attr",
config.datasourceConfigureRadio.enabled ? "" : "disabled"
);
cy.get(groupsSelector.datasourcesBuildWithRadio)
.should("be.visible")
.and(
config.datasourceBuildWithRadio.enabled ? "be.enabled" : "have.attr",
config.datasourceBuildWithRadio.enabled ? "" : "disabled"
);
if (config.verifyHelperTexts) {
cy.verifyElement(groupsSelector.datasourcesConfigureLabel, "Configure");
cy.verifyElement(
groupsSelector.datasourcesConfigureHelperText,
"Access & edit connection details"
);
cy.verifyElement(
groupsSelector.datasourcesBuildWithLabel,
"Build with"
);
cy.verifyElement(
groupsSelector.datasourcesBuildWithHelperText,
"Use in apps & workflows"
);
}
cy.verifyElement(
groupsSelector.groupChip("All data source"),
"All data sources"
);
}
cy.verifyElement(groupsSelector.addPermissionButton, "Add permission");
cy.get(groupsSelector.addPermissionButton).should(
config.addButtonEnabled ? "be.enabled" : "be.disabled"
);
});
};
export const permissionModal = () => {
cy.verifyElement(
groupsSelector.permissionNameLabel,
groupsText.permissionNameLabel
);
cy.verifyElement(
groupsSelector.permissionNameHelperText,
groupsText.permissionNameHelperText
);
cy.verifyElement(groupsSelector.permissionLabel, groupsText.permissionLabel);
cy.verifyElement(
groupsSelector.editPermissionLabel,
groupsText.editPermissionLabel
);
cy.verifyElement(
groupsSelector.editPermissionHelperText,
groupsText.editPermissionHelperText
);
cy.verifyElement(
groupsSelector.viewPermissionLabel,
groupsText.viewPermissionLabel
);
cy.verifyElement(
groupsSelector.viewPermissionHelperText,
groupsText.viewPermissionHelperText
);
cy.get(groupsSelector.hidePermissionInput).should("be.visible");
cy.verifyElement(groupsSelector.resourceLabel, groupsText.resourcesheader);
cy.get(groupsSelector.resourceContainer).should("be.visible");
cy.get(groupsSelector.allAppsRadio).should("be.visible").and("be.checked");
cy.verifyElement(groupsSelector.allAppsLabel, groupsText.allAppsLabel);
cy.verifyElement(
groupsSelector.allAppsHelperText,
groupsText.allAppsHelperText
);
};
export const verifyUserRow = (name, email) => {
cy.get('[data-cy="avatar-image"]').should("be.visible");
cy.get('[data-cy="user-name"]')
.should("be.visible")
.and("contain.text", name);
cy.get('[data-cy="user-email"]').should("be.visible").and("have.text", email);
};

View file

@ -109,6 +109,7 @@ export const selectEnv = (envName) => {
};
if (isValidEnvName(envName)) {
cy.wait(1000)
cy.get('[data-cy="list-current-env-name"]').click();
cy.wait(500)
const envSelector = `${multiEnvSelector.envNameList}:eq(${envIndex})`;

View file

@ -0,0 +1,206 @@
import { commonSelectors } from "Selectors/common";
import { workflowSelector } from "Selectors/workflows";
import { deleteFolder } from "Support/utils/common";
import {
addAndVerifyConstants,
deleteConstant,
} from "Support/utils/workspaceConstants";
import { commonText } from "Texts/common";
export const uiCreateApp = (appName) => {
cy.createApp(appName);
cy.wait(2000);
cy.go("back");
};
export const uiVerifyAppCreated = (appName, shouldExist = true) => {
const assertion = shouldExist ? "exist" : "not.exist";
cy.get(commonSelectors.appCard(appName)).should(assertion);
};
export const uiDeleteApp = (appName) => {
cy.deleteApp(appName);
};
export const uiVerifyAppDeleted = (appName) => {
cy.get(commonSelectors.appCard(appName)).should("not.exist");
};
export const uiVerifyAppCreatePrivilege = (hasPrivilege = true) => {
const assertion = hasPrivilege ? "be.enabled" : "be.disabled";
cy.get(commonSelectors.dashboardAppCreateButton).should(assertion);
};
export const uiCreateFolder = (folderName) => {
cy.get(commonSelectors.createNewFolderButton).click();
cy.clearAndType(commonSelectors.folderNameInput, folderName);
cy.get(commonSelectors.createFolderButton).click();
};
export const uiVerifyFolderCreated = (folderName) => {
cy.get(commonSelectors.folderListcard(folderName)).should("exist");
};
export const uiVerifyFolderDeleted = (folderName) => {
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonText.folderDeletedToast
);
cy.get(commonSelectors.folderListcard(folderName)).should("not.exist");
};
export const uiVerifyFolderCreatePrivilege = (hasPrivilege = true) => {
const assertion = hasPrivilege ? "exist" : "not.exist";
cy.get(commonSelectors.createNewFolderButton).should(assertion);
};
export const uiVerifyWorkspaceConstantCreatePrivilege = (
hasPrivilege = true
) => {
const assertion = hasPrivilege ? "exist" : "not.exist";
cy.get(commonSelectors.workspaceConstantsIcon).should(assertion);
};
export const uiCreateDataSource = (
datasourceName,
datasourceType = "restapi"
) => {
cy.get(commonSelectors.globalDataSourceIcon).click();
// cy.get(commonSelectors.addNewDataSourceButton).click();
cy.get('[data-cy="rest-api-add-button"]').eq(0).click({ force: true });
};
export const uiVerifyDataSourceCreated = (datasourceName) => {
cy.verifyToastMessage(commonSelectors.toastMessage, "Data Source Added");
cy.get('[data-cy="restapi-button"]').should("exist");
};
export const uiDeleteDataSource = (datasourceName) => {
cy.get('[data-cy="restapi-delete-button"]').click({ force: true });
cy.get(commonSelectors.yesButton).click();
};
export const uiVerifyDataSourceDeleted = (datasourceName) => {
cy.verifyToastMessage(commonSelectors.toastMessage, "Data Source Deleted");
cy.get('[data-cy="restapi-button"]').should("not.exist");
};
export const uiVerifyDataSourceCreatePrivilege = (hasPrivilege = true) => {
const assertion = hasPrivilege ? "exist" : "not.exist";
cy.get(commonSelectors.globalDataSourceIcon).should(assertion);
};
export const uiCreateWorkflow = (workflowName) => {
cy.get(workflowSelector.globalWorkFlowsIcon).click();
cy.get('[data-cy="button-new-workflow-from-scratch"]').click();
cy.get(workflowSelector.workFlowNameInputField).type(workflowName);
cy.get(workflowSelector.createWorkFlowsButton).click();
cy.wait(2000);
cy.go("back");
};
export const uiVerifyWorkflowCreated = (workflowName) => {
cy.get(commonSelectors.globalWorkFlowsIcon).click();
cy.get(`[data-cy="${workflowName.toLowerCase()}-card"]`)
.contains(workflowName)
.should("exist");
};
export const uiDeleteWorkflow = () => {
cy.get(".homepage-app-card .home-app-card-header .menu-ico").then(($el) => {
$el[0].style.setProperty("visibility", "visible", "important");
});
cy.get('[data-cy="app-card-menu-icon"]').click();
cy.get(workflowSelector.deleteWorkFlowOption).click();
cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click();
};
export const uiVerifyWorkflowDeleted = (workflowName) => {
cy.get(`[data-cy="${workflowName.toLowerCase()}-card"]`).should("not.exist");
};
export const uiVerifyWorkflowCreatePrivilege = (hasPrivilege = true) => {
const assertion = hasPrivilege ? "exist" : "not.exist";
cy.get(commonSelectors.globalWorkFlowsIcon).should(assertion);
};
export const uiVerifyAllCreatePrivileges = (
hasAppCreate = true,
hasFolderCreate = true,
hasConstantCreate = true,
hasDataSourceCreate = true,
hasWorkflowCreate = true
) => {
uiVerifyAppCreatePrivilege(hasAppCreate);
uiVerifyFolderCreatePrivilege(hasFolderCreate);
uiVerifyWorkspaceConstantCreatePrivilege(hasConstantCreate);
cy.ifEnv("Enterprise", () => {
uiVerifyDataSourceCreatePrivilege(hasDataSourceCreate);
uiVerifyWorkflowCreatePrivilege(hasWorkflowCreate);
});
};
export const uiVerifyBuilderPrivileges = () => {
uiVerifyAllCreatePrivileges(true, true, true, true, true);
};
export const uiVerifyAdminPrivileges = () => {
uiVerifyAllCreatePrivileges(true, true, true, true, true);
cy.get(commonSelectors.settingsIcon).click();
cy.get(commonSelectors.workspaceSettings).should("exist");
cy.get(commonSelectors.dashboardIcon).click();
};
export const uiAppCRUDWorkflow = (appName) => {
uiCreateApp(appName);
uiVerifyAppCreated(appName, true);
uiDeleteApp(appName);
uiVerifyAppDeleted(appName);
};
export const uiFolderCRUDWorkflow = (folderName) => {
uiCreateFolder(folderName);
uiVerifyFolderCreated(folderName);
deleteFolder(folderName);
uiVerifyFolderDeleted(folderName);
};
export const uiWorkspaceConstantCRUDWorkflow = (
constantName,
constantValue
) => {
cy.get(commonSelectors.workspaceConstantsIcon).click();
addAndVerifyConstants(constantName, constantValue);
deleteConstant(constantName);
cy.get(commonSelectors.dashboardIcon).click();
};
export const uiDataSourceCRUDWorkflow = (
datasourceName,
datasourceType = "restapi"
) => {
cy.ifEnv("Enterprise", () => {
uiCreateDataSource(datasourceName, datasourceType);
uiVerifyDataSourceCreated(datasourceName);
uiDeleteDataSource(datasourceName);
uiVerifyDataSourceDeleted(datasourceName);
cy.get(commonSelectors.dashboardIcon).click();
});
};
export const uiWorkflowCRUDWorkflow = (workflowName) => {
cy.ifEnv("Enterprise", () => {
uiCreateWorkflow(workflowName);
uiVerifyWorkflowCreated(workflowName);
uiDeleteWorkflow();
uiVerifyWorkflowDeleted(workflowName);
});
};

View file

@ -1,42 +1,8 @@
import { commonSelectors } from "Selectors/common";
import { commonText } from "Texts/common";
import { workspaceConstantsSelectors } from "Selectors/workspaceConstants";
import { createFolder, deleteFolder } from "Support/utils/common";
import { addAndVerifyConstants } from "Support/utils/workspaceConstants";
const appOperations = {
createApp: (appName) => {
cy.createApp(appName);
cy.backToApps();
},
deleteApp: (appName) => {
cy.deleteApp(appName);
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonText.appDeletedToast
);
},
cloneApp: (appName) => {
cy.get(commonSelectors.appCard(appName))
.trigger("mouseover")
.find(commonSelectors.cloneButton)
.click();
},
};
const folderOperations = {
createFolder: (folderName) => {
createFolder(folderName);
},
deleteFolder: (folderName) => {
deleteFolder(folderName);
},
};
const constantsOperations = {
export const constantsOperations = {
createConstant: (name, value) => {
cy.get(commonSelectors.workspaceConstantsIcon).click();
addAndVerifyConstants(name, value);
@ -49,7 +15,7 @@ const constantsOperations = {
};
// Permission verification helpers
const verifyPermissions = {
export const verifyPermissions = {
checkAppPermissions: (shouldExist = true) => {
const assertion = shouldExist ? "exist" : "not.exist";
cy.get(commonSelectors.appCreateButton).should(assertion);
@ -73,41 +39,89 @@ const verifyPermissions = {
},
};
// Helper function to perform all verifications
const verifyAllPermissions = (shouldHaveAccess = true) => {
verifyPermissions.checkAppPermissions(shouldHaveAccess);
verifyPermissions.checkFolderPermissions(shouldHaveAccess);
verifyPermissions.checkConstantsPermissions(shouldHaveAccess);
verifyPermissions.checkSettingsAccess(shouldHaveAccess);
export const getGroupPermissionInput = (isEnterprise, flag) => {
return isEnterprise
? {
appCreate: flag,
appDelete: flag,
appPromote: flag,
appRelease: flag,
workflowCreate: flag,
workflowDelete: flag,
dataSourceCreate: flag,
dataSourceDelete: flag,
folderCRUD: flag,
orgConstantCRUD: flag,
}
: {
appCreate: flag,
appDelete: flag,
folderCRUD: flag,
orgConstantCRUD: flag,
};
};
// Role-based permission sets
const rolePermissions = {
admin: {
name: "Admin",
hasFullAccess: true,
canManageWorkspace: true,
canManageUsers: true,
},
builder: {
name: "Builder",
hasFullAccess: true,
canManageWorkspace: false,
canManageUsers: false,
},
endUser: {
name: "End User",
hasFullAccess: false,
canManageWorkspace: false,
canManageUsers: false,
},
export const verifyBuilderPermissions = (
appName,
folderName,
constName,
constValue,
isAdmin = false
) => {
verifyBasicPermissions(true);
// App operations
cy.apiCreateApp(appName);
cy.apiDeleteApp();
// Folder operations
cy.apiCreateFolder(folderName);
cy.apiDeleteFolder();
// Constants management
cy.get(commonSelectors.workspaceConstantsIcon).click();
addAndVerifyConstants(constName, constValue);
cy.get(workspaceConstantsSelectors.constDeleteButton(constName)).click();
cy.get(commonSelectors.yesButton).click();
cy.ifEnv("Enterprise", () => {
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
appName,
"restapi",
[{ key: "url", value: "https://jsonplaceholder.typicode.com/users" }]
);
cy.apiDeleteGDS(appName);
cy.apiCreateWorkflow(appName);
cy.apiDeleteWorkflow(appName);
});
verifySettingsAccess(isAdmin);
};
export {
appOperations,
folderOperations,
constantsOperations,
verifyPermissions,
verifyAllPermissions,
rolePermissions,
export const verifyBasicPermissions = (canCreate = true) => {
cy.get(commonSelectors.dashboardAppCreateButton).should(
canCreate ? "be.enabled" : "be.disabled"
);
cy.get(commonSelectors.createNewFolderButton).should(
canCreate ? "exist" : "not.exist"
);
cy.get('[data-cy="database-icon"]').should(canCreate ? "exist" : "not.exist");
cy.ifEnv("Enterprise", () => {
cy.get(commonSelectors.globalDataSourceIcon).should(
canCreate ? "exist" : "not.exist"
);
cy.get(commonSelectors.workspaceConstantsIcon).should(
canCreate ? "exist" : "not.exist"
);
});
};
export const verifySettingsAccess = (shouldExist = true) => {
cy.get(commonSelectors.settingsIcon).click();
cy.get(commonSelectors.workspaceSettings).should(
shouldExist ? "exist" : "not.exist"
);
};

View file

@ -1,20 +1,19 @@
import { appVersionText } from "Texts/exportImport";
import { appVersionSelectors } from "Selectors/exportImport";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { commonText } from "Texts/common";
import { verifyModal, closeModal } from "Support/utils/common";
import { appVersionSelectors } from "Selectors/exportImport";
import {
confirmVersionModalSelectors,
editVersionSelectors,
} from "Selectors/version";
import { closeModal } from "Support/utils/common";
import { commonText } from "Texts/common";
import { appVersionText } from "Texts/exportImport";
import { deleteVersionText, releasedVersionText } from "Texts/version";
import { verifyComponent } from "Support/utils/basicComponents";
import { appPromote } from "./platform/multiEnv";
export const navigateToCreateNewVersionModal = (value) => {
cy.get(appVersionSelectors.appVersionLabel).click();
cy.contains(appVersionText.createNewVersion).should("be.visible");
cy.contains(appVersionText.createNewVersion).click();
cy.contains(appVersionText.createNewVersion).first().should("be.visible");
cy.contains(appVersionText.createNewVersion).first().click();
};
export const navigateToEditVersionModal = (value) => {
@ -48,7 +47,7 @@ export const verifyElementsOfCreateNewVersionModal = (version = []) => {
);
cy.get(
commonSelectors.buttonSelector(appVersionText.createNewVersion)
).verifyVisibleElement("have.text", appVersionText.createNewVersion);
).first().verifyVisibleElement("have.text", appVersionText.createNewVersion);
cy.get(commonSelectors.buttonSelector(commonText.cancelButton))
.should("be.visible")
.and("have.text", commonText.cancelButton);
@ -113,7 +112,7 @@ export const verifyDuplicateVersion = (newVersion = [], version) => {
cy.get(appVersionSelectors.createVersionInputField).click();
cy.contains(`[id*="react-select-"]`, version).click();
cy.clearAndType(appVersionSelectors.versionNameInputField, newVersion[0]);
cy.get(appVersionSelectors.createNewVersionButton).click();
cy.get(appVersionSelectors.createNewVersionButton).first().click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
appVersionText.versionNameAlreadyExists
@ -160,4 +159,10 @@ export const switchVersionAndVerify = (currentVersion, newVersion) => {
.should("be.visible")
.click();
cy.get(".app-version-name").contains(newVersion).click();
cy.wait('@appDs')
};
export const openPreviewSettings = () => {
cy.get(commonSelectors.previewSettings).should("be.visible").click();
};

View file

@ -40,23 +40,23 @@ export const revealWorkflowToken = (selectors) => {
};
export const importWorkflowApp = (
wfName,
workflowName,
fixturePath = "cypress/fixtures/exportedApp.json"
) => {
cy.get(workflowSelector.importWorkFlowsOption).click();
cy.get(workflowSelector.importWorkFlowsLabel).click();
cy.get('input[type="file"]').first().selectFile(fixturePath, { force: true });
cy.wait(2000);
cy.get(workflowSelector.workFlowNameInputField).clear().type(wfName);
cy.get(workflowSelector.workFlowNameInputField).clear().type(workflowName);
cy.get(workflowSelector.importWorkFlowsButton).click();
};
export const deleteAppandWorkflowAfterExecution = (wfName, appName) => {
export const deleteAppandWorkflowAfterExecution = (workflowName, appName) => {
cy.backToApps();
cy.deleteApp(appName);
cy.get(workflowSelector.globalWorkFlowsIcon).click();
cy.intercept("DELETE", "/api/apps/*").as("appDeleted");
cy.get(commonSelectors.appCard(wfName))
cy.get(commonSelectors.appCard(workflowName))
.realHover()
.find(commonSelectors.appCardOptionsButton)
.realHover()

File diff suppressed because it is too large Load diff

View file

@ -10,13 +10,14 @@
"@cypress/code-coverage": "^3.12.12",
"@cypress/webpack-preprocessor": "^5.12.0",
"@faker-js/faker": "^7.3.0",
"cypress": "^15.0.0",
"cypress": "^15.4.0",
"cypress-mailhog": "^2.5.0"
},
"dependencies": {
"cypress-real-events": "^1.15.0",
"moment": "^2.29.4",
"node-xlsx": "^0.4.0",
"pdf-parse": "^1.1.1",
"pg": "^8.8.0"
}
}
}

View file

@ -170,7 +170,7 @@ fi
TOOLJET_EDTION=ee npm --prefix server run db:setup
if sudo -E systemctl start nest && && sudo journalctl -u nest -f
if sudo -E systemctl start nest && sudo journalctl -u nest -f
then
echo "The app will be served at ${TOOLJET_HOST}"
else

View file

@ -168,6 +168,12 @@ RUN mkdir -p /home/appuser \
&& chmod -R g=u /home/appuser \
&& npm cache clean --force
# Create rsyslog directory for audit logs with proper permissions
RUN mkdir -p /home/appuser/rsyslog \
&& chown -R appuser:0 /home/appuser/rsyslog \
&& chmod g+s /home/appuser/rsyslog \
&& chmod -R g=u /home/appuser/rsyslog
# Create directory /tmp/.npm/npm-cache/ and set ownership to appuser
RUN mkdir -p /tmp/.npm/npm-cache/ \
&& chown -R appuser:0 /tmp/.npm/npm-cache/ \

View file

@ -164,6 +164,12 @@ RUN mkdir -p /home/appuser \
&& chmod -R g=u /home/appuser \
&& npm cache clean --force
# Create rsyslog directory for audit logs with proper permissions
RUN mkdir -p /home/appuser/rsyslog \
&& chown -R appuser:0 /home/appuser/rsyslog \
&& chmod g+s /home/appuser/rsyslog \
&& chmod -R g=u /home/appuser/rsyslog
# Create directory /tmp/.npm/npm-cache/ and set ownership to appuser
RUN mkdir -p /tmp/.npm/npm-cache/ \
&& chown -R appuser:0 /tmp/.npm/npm-cache/ \

View file

@ -1,14 +1,19 @@
// storybookDecorators.js
import React from 'react';
import React from "react";
import { MemoryRouter } from "react-router-dom";
export function withColorScheme(story, context) {
const darkMode = context?.globals?.backgrounds?.value === '#333333'; // Access theme mode from globals
const className = darkMode ? 'dark-theme' : '';
const darkMode = context?.globals?.backgrounds?.value === "#333333"; // Access theme mode from globals
const className = darkMode ? "dark-theme" : "";
return (
<div className={className} style={{ backgroundColor: 'transparent' }}>
<div className={className} style={{ backgroundColor: "transparent" }}>
{story()}
</div>
);
}
export function withRouter(story) {
return <MemoryRouter>{story()}</MemoryRouter>;
}

View file

@ -1,32 +1,77 @@
import customWebpackConfig from '../webpack.config';
import path from 'path';
import customWebpackConfig from "../webpack.config";
import path from "path";
const config = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-onboarding",
"@storybook/addon-interactions",
"@storybook/addon-docs",
],
framework: {
name: "@storybook/react-webpack5",
options: {},
},
docs: {
autodocs: "tag",
},
webpackFinal: async (storybookConfig) => {
// Filter out the babel-loader rule from custom config to avoid conflicts
const customRules = customWebpackConfig.module.rules.filter((rule) => {
if (rule.test && rule.test.toString().includes("js|jsx")) {
return false; // Skip the babel-loader rule that includes react-refresh
}
return true;
});
// Add a custom babel-loader rule for JSX files without react-refresh
const babelRule = {
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
plugins: [
[
"import",
{
libraryName: "lodash",
libraryDirectory: "",
camel2DashComponentName: false,
},
"lodash",
],
],
},
},
};
return {
...storybookConfig,
module: { ...storybookConfig.module, rules: [...storybookConfig.module.rules, ...customWebpackConfig.module.rules] },
module: {
...storybookConfig.module,
rules: [...storybookConfig.module.rules, ...customRules, babelRule],
},
resolve: {
...storybookConfig.resolve,
alias: {
...storybookConfig.resolve.alias,
'@': path.resolve(__dirname, '../src/')
}
}
"@": path.resolve(__dirname, "../src/"),
"@ee": path.resolve(__dirname, "../ee/"),
"@cloud": path.resolve(__dirname, "../cloud/"),
"@assets": path.resolve(__dirname, "../assets/"),
"@white-label": path.resolve(
__dirname,
"../src/_helpers/white-label"
),
},
fallback: {
...storybookConfig.resolve.fallback,
process: require.resolve("process/browser.js"),
path: require.resolve("path-browserify"),
},
},
};
},
};

View file

@ -1,8 +1,8 @@
/** @type { import('@storybook/react').Preview } */
/** @type { import('@storybook/react-webpack5').Preview } */
import '../src/_styles/theme.scss';
import './preview.scss';
import { withColorScheme } from './decorators'; // Import the decorator
import "../src/_styles/theme.scss";
import "./preview.scss";
import { withColorScheme, withRouter } from "./decorators"; // Import the decorators
const preview = {
parameters: {
@ -14,7 +14,7 @@ const preview = {
},
},
},
decorators: [withColorScheme], // Adding the decorator to the decorators array
decorators: [withRouter, withColorScheme], // Adding the decorators to the decorators array
};
export default preview;

View file

@ -1,2 +1,6 @@
@import '~bootstrap/scss/bootstrap';
@import '../src/_styles/componentdesign.scss'
@import '../src/_styles/componentdesign.scss';
body {
overflow-y: scroll !important;
}

View file

@ -1 +1 @@
3.20.21-lts
3.20.30-lts

View file

@ -1,17 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"style": "new-york",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/styles/theme.scss",
"baseColor": "zinc",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
"prefix": "tw-"
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

@ -1 +1 @@
Subproject commit 4c9743f485d77aa186c29729abd743e708536e79
Subproject commit 1e40a2e500636d5f900f707cccd0d9ee1f372069

View file

@ -22,11 +22,11 @@
"@radix-ui/colors": "^0.1.8",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.0.3",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-toggle-group": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
@ -49,8 +49,9 @@
"axios": "^1.3.3",
"bootstrap": "^5.2.3",
"buffer": "^6.0.3",
"class-variance-authority": "^0.7.0",
"class-variance-authority": "^0.7.1",
"classnames": "^2.3.2",
"clsx": "^2.1.1",
"cron-validator": "^1.3.1",
"cronstrue": "^2.51.0",
"deep-object-diff": "^1.1.9",
@ -146,11 +147,13 @@
"semver": "^7.3.8",
"string-hash": "^1.1.3",
"superstruct": "^1.0.3",
"tailwind-merge": "^2.2.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"tinycolor2": "^1.6.0",
"tw-animate-css": "^1.3.7",
"url-join": "^5.0.0",
"use-react-router-breadcrumbs": "^4.0.1",
"util": "^0.12.5",
"uuid": "9.0.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"y-websocket": "^1.4.5",
@ -165,13 +168,10 @@
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"@storybook/addon-essentials": "^7.2.1",
"@storybook/addon-interactions": "^7.2.1",
"@storybook/addon-links": "^7.2.1",
"@storybook/addon-onboarding": "^1.0.8",
"@storybook/blocks": "^7.2.1",
"@storybook/react": "^7.2.1",
"@storybook/react-webpack5": "^7.2.1",
"@storybook/addon-docs": "^9.1.5",
"@storybook/addon-links": "^9.1.5",
"@storybook/addon-onboarding": "^9.1.5",
"@storybook/react-webpack5": "^9.1.5",
"@storybook/testing-library": "^0.2.0",
"@svgr/webpack": "^6.5.1",
"@testing-library/jest-dom": "^5.16.5",
@ -193,7 +193,7 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-storybook": "^0.6.13",
"eslint-plugin-storybook": "^9.1.5",
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.0",
"jest": "^29.4.2",
@ -204,13 +204,14 @@
"react-refresh": "^0.17.0",
"sass": "^1.93.2",
"sass-loader": "^16.0.5",
"storybook": "^7.2.1",
"storybook": "^9.1.5",
"style-loader": "^3.3.1",
"tailwindcss": "^3.4.1",
"terser-webpack-plugin": "^5.3.6",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^5.2.2"
"webpack-dev-server": "^5.2.2",
"@storybook/addon-docs": "^9.1.5"
},
"overrides": {
"react-dates": {

View file

@ -1,4 +1,4 @@
import React, { Suspense } from 'react';
import React, { Suspense, useEffect } from 'react';
import useStore from '@/AppBuilder/_stores/store';
import useAppData from '@/AppBuilder/_hooks/useAppData';
import { TJLoader } from '@/_ui/TJLoader/TJLoader';
@ -16,6 +16,7 @@ import Popups from './Popups';
import { ModuleProvider } from '@/AppBuilder/_contexts/ModuleContext';
import RightSidebarToggle from '@/AppBuilder/RightSideBar/RightSidebarToggle';
import { shallow } from 'zustand/shallow';
import { useNavigate } from 'react-router-dom';
import ArtifactPreview from './ArtifactPreview';
@ -30,10 +31,13 @@ export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod
useAppData(appId, moduleId, darkMode);
const isEditorLoading = useStore((state) => state.loaderStore.modules[moduleId].isEditorLoading, shallow);
const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
const hasModuleAccess = useStore((state) => state.license.featureAccess?.modulesEnabled);
const isModuleEditor = appType === 'module';
console.log("isModuleEditor", isModuleEditor);
const updateIsTJDarkMode = useStore((state) => state.updateIsTJDarkMode, shallow);
const appBuilderMode = useStore((state) => state.appStore.modules[moduleId]?.app?.appBuilderMode ?? 'visual');
const navigate = useNavigate();
const isUserInZeroToOneFlow = appBuilderMode === 'ai';
@ -42,6 +46,12 @@ export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod
switchDarkMode(newMode);
};
useEffect(() => {
if (hasModuleAccess === false && isModuleEditor) {
navigate('/error/restricted');
}
}, [hasModuleAccess, isModuleEditor]);
//TODO: This can be added to the mode slice and set based on the mode
if (isEditorLoading) {
return (

View file

@ -26,10 +26,14 @@ import PagesSidebarNavigation from '../RightSideBar/PageSettingsTab/PageMenu/Pag
import { DragGhostWidget, ResizeGhostWidget } from './GhostWidgets';
import AppCanvasBanner from '../../AppBuilder/Header/AppCanvasBanner';
import { debounce } from 'lodash';
import { RIGHT_SIDE_BAR_TAB } from '../RightSideBar/rightSidebarConstants';
export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => {
const { moduleId, isModuleMode, appType } = useModuleContext();
const canvasContainerRef = useRef();
const scrollTimeoutRef = useRef(null);
const canvasContentRef = useRef(null);
const [isScrolling, setIsScrolling] = useState(false);
const handleCanvasContainerMouseUp = useStore((state) => state.handleCanvasContainerMouseUp, shallow);
const resolveReferences = useStore((state) => state.resolveReferences);
const canvasHeight = useStore((state) => state.appStore.modules[moduleId].canvasHeight);
@ -52,6 +56,7 @@ export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => {
const editorMarginLeft = useSidebarMargin(canvasContainerRef);
const getPageId = useStore((state) => state.getCurrentPageId, shallow);
const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen, shallow);
const draggingComponentId = useStore((state) => state.draggingComponentId, shallow);
const isSidebarOpen = useStore((state) => state.isSidebarOpen, shallow);
const selectedSidebarItem = useStore((state) => state.selectedSidebarItem);
const currentPageId = useStore((state) => state.modules[moduleId].currentPageId);
@ -71,7 +76,6 @@ export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => {
}),
shallow
);
const showHeader = !globalSettings?.hideHeader;
const { definition: { properties = {} } = {} } = pageSettings ?? {};
const { position, disableMenu, showOnDesktop } = properties ?? {};
@ -121,8 +125,8 @@ export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => {
currentMode === 'view'
? computeViewerBackgroundColor(isAppDarkMode, canvasBgColor)
: !isAppDarkMode
? '#EBEBEF'
: '#2F3C4C';
? '#EBEBEF'
: '#2F3C4C';
if (isModuleMode) {
return {
@ -151,6 +155,34 @@ export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => {
localStorage.setItem('isPagesSidebarPinned', JSON.stringify(newValue));
}, [isViewerSidebarPinned]);
useEffect(() => {
const handleScroll = () => {
setIsScrolling(true);
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
scrollTimeoutRef.current = setTimeout(() => {
setIsScrolling(false);
}, 600);
};
const canvasContent = canvasContentRef.current;
if (canvasContent) {
canvasContent.addEventListener('scroll', handleScroll);
}
return () => {
if (canvasContent) {
canvasContent.removeEventListener('scroll', handleScroll);
}
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
};
}, []);
function getMinWidth() {
if (isModuleMode) return '100%';
@ -227,22 +259,29 @@ export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => {
/>
)}
<div
ref={canvasContentRef}
style={{
minWidth: getMinWidth(),
scrollbarWidth: 'none',
overflow: 'auto',
width: currentMode === 'view' ? `calc(100% - ${isViewerSidebarPinned ? '0px' : '0px'})` : '100%',
...(appType === 'module' && isModuleMode && { height: 'inherit' }),
}}
className={`app-${appId} _tooljet-page-${getPageId()} canvas-content`}
className={cx(`app-${appId} _tooljet-page-${getPageId()} canvas-content scrollbar`, {
'scrollbar-hidden': !isScrolling,
})}
>
{currentMode === 'edit' && (
<AutoComputeMobileLayoutAlert currentLayout={currentLayout} darkMode={isAppDarkMode} />
)}
<DeleteWidgetConfirmation darkMode={isAppDarkMode} />
<HotkeyProvider mode={currentMode} canvasMaxWidth={canvasMaxWidth} currentLayout={currentLayout}>
<HotkeyProvider
mode={currentMode}
canvasMaxWidth={canvasMaxWidth}
currentLayout={currentLayout}
isModuleMode={isModuleMode}
>
{environmentLoadingState !== 'loading' && (
<div>
<div className={cx({ 'h-100': isModuleMode })}>
<Container
id={moduleId}
gridWidth={gridWidth}

View file

@ -28,7 +28,7 @@ export const ConfigHandle = ({
subContainerIndex,
}) => {
const { moduleId } = useModuleContext();
const isLicenseValid = useStore((state) => state.isLicenseValid(), shallow);
const isModulesEnabled = useStore((state) => state.license.featureAccess?.modulesEnabled, shallow);
const shouldFreeze = useStore((state) => state.getShouldFreeze());
const componentName = useStore((state) => state.getComponentDefinition(id, moduleId)?.component?.name || '', shallow);
const isMultipleComponentsSelected = useStore(
@ -219,11 +219,11 @@ export const ConfigHandle = ({
)}
</span>
{/* Tooltip for invalid license on ModuleViewer */}
{!isLicenseValid && componentType === 'ModuleViewer' && (
{(componentType === 'ModuleViewer' || componentType === 'ModuleContainer') && !isModulesEnabled && (
<Tooltip
id={`invalid-license-modules-${componentName?.toLowerCase()}`}
className="tooltip"
isOpen={_showHandle && componentType === 'ModuleViewer'}
isOpen={_showHandle && (componentType === 'ModuleViewer' || componentType === 'ModuleContainer')}
style={{ textAlign: 'center' }}
/>
)}

View file

@ -76,10 +76,6 @@ const Container = React.memo(
const setCurrentDragCanvasId = useGridStore((state) => state.actions.setCurrentDragCanvasId);
const { handleDrop } = useCanvasDropHandler({
appType,
});
const [{ isOverCurrent }, drop] = useDrop({
accept: 'box',
hover: (item, monitor) => {

View file

@ -33,6 +33,7 @@ import { useGroupedTargetsScrollHandler } from './hooks/useGroupedTargetsScrollH
import { DROPPABLE_PARENTS, NO_OF_GRIDS, SUBCONTAINER_WIDGETS } from '../appCanvasConstants';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import { useElementGuidelines } from './hooks/useElementGuidelines';
import { RIGHT_SIDE_BAR_TAB } from '../../RightSideBar/rightSidebarConstants';
import MentionComponentInChat from '../ConfigHandle/MentionComponentInChat';
const CANVAS_BOUNDS = { left: 0, top: 0, right: 0, position: 'css' };
@ -79,6 +80,7 @@ export default function Grid({ gridWidth, currentLayout }) {
const virtualTarget = useGridStore((state) => state.virtualTarget, shallow);
const currentDragCanvasId = useGridStore((state) => state.currentDragCanvasId, shallow);
const groupedTargets = [...findHighestLevelofSelection().map((component) => '.ele-' + component.id)];
const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab);
const isWidgetResizable = useMemo(() => {
if (virtualTarget) {
@ -339,7 +341,7 @@ export default function Grid({ gridWidth, currentLayout }) {
if (moveableRef.current) {
safeUpdateMoveable();
}
}, [temporaryHeight, boxList]);
}, [temporaryHeight, boxList, selectedComponents]);
useEffect(() => {
reloadGrid();
@ -465,7 +467,8 @@ export default function Grid({ gridWidth, currentLayout }) {
const handleDragGroupEnd = (e) => {
try {
hideGridLines();
// setIsGroupDragging(false);
handleDeactivateTargets();
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
const { events, clientX, clientY } = e;
const initialParent = events[0].target.closest('.real-canvas');
// Get potential new parent using same logic as onDragEnd
@ -775,7 +778,7 @@ export default function Grid({ gridWidth, currentLayout }) {
onResizeGroup={({ events }) => {
const parentElm = events[0].target.closest('.real-canvas');
const parentWidth = parentElm?.clientWidth;
const parentHeight = parentElm?.clientHeight;
const parentHeight = parentElm?.scrollHeight;
handleActivateTargets(parentElm?.id?.replace('canvas-', ''));
const { posRight, posLeft, posTop, posBottom } = getPositionForGroupDrag(events, parentWidth, parentHeight);
events.forEach((ev) => {
@ -783,7 +786,6 @@ export default function Grid({ gridWidth, currentLayout }) {
ev.target.style.height = `${ev.height}px`;
ev.target.style.transform = ev.drag.transform;
});
if (!(posLeft < 0 || posTop < 0 || posRight < 0 || posBottom < 0)) {
groupResizeDataRef.current = events;
}
@ -859,10 +861,12 @@ export default function Grid({ gridWidth, currentLayout }) {
if (e.target.id === 'moveable-virtual-ghost-element') {
return true;
}
// This is to prevent parent component from being dragged and the stop the propagation of the event
if (getHoveredComponentForGrid() !== e.target.id) {
return false;
}
newDragParentId.current = boxList.find((box) => box.id === e.target.id)?.parent;
e?.moveable?.controlBox?.removeAttribute('data-off-screen');
@ -953,6 +957,10 @@ export default function Grid({ gridWidth, currentLayout }) {
}
try {
if (isDraggingRef.current) {
// setTimeout(() => {
// useStore.getState().setRightSidebarOpen(true);
// }, 100);
useStore.getState().setDraggingComponentId(null);
isDraggingRef.current = false;
}
@ -1012,6 +1020,25 @@ export default function Grid({ gridWidth, currentLayout }) {
hideGridLines();
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
toggleCanvasUpdater();
// Move this to common function
const canvas = document.querySelector('.canvas-container');
const sidebar = document.querySelector('.editor-sidebar');
const droppedElem = document.getElementById(e.target.id);
if (!canvas || !sidebar || !droppedElem) return;
const droppedRect = droppedElem.getBoundingClientRect();
const sidebarRect = sidebar.getBoundingClientRect();
const isOverlapping = droppedRect.right > sidebarRect.left && droppedRect.left < sidebarRect.right;
if (isOverlapping) {
const overlap = droppedRect.right - sidebarRect.left;
canvas.scrollTo({
left: canvas.scrollLeft + overlap + 15,
behavior: 'smooth',
});
}
}}
onDrag={(e) => {
if (e.target.id === 'moveable-virtual-ghost-element') {
@ -1088,9 +1115,6 @@ export default function Grid({ gridWidth, currentLayout }) {
onDragGroup={(ev) => {
const { events } = ev;
const parentElm = events[0]?.target?.closest('.real-canvas');
if (parentElm && !parentElm.classList.contains('show-grid')) {
parentElm?.classList?.add('show-grid');
}
events.forEach((ev) => {
const currentWidget = boxList.find(({ id }) => id === ev.target.id);
@ -1112,8 +1136,6 @@ export default function Grid({ gridWidth, currentLayout }) {
}}
onDragGroupEnd={(e) => {
handleDragGroupEnd(e);
handleDeactivateTargets();
clearActiveTargetClassNamesAfterSnapping(selectedComponents);
toggleCanvasUpdater();
}}
onClickGroup={(e) => {
@ -1168,6 +1190,10 @@ export default function Grid({ gridWidth, currentLayout }) {
}
}}
snapGridAll={true}
onClick={(e) => {
useStore.getState().setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.CONFIGURATION);
useStore.getState().setRightSidebarOpen(true);
}}
/>
</>
);

View file

@ -6,7 +6,7 @@ import useKeyHooks from '@/_hooks/useKeyHooks';
import { shallow } from 'zustand/shallow';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth }) => {
export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth, isModuleMode }) => {
const { isModuleEditor } = useModuleContext();
const canvasRef = useRef(null);
const focusedParentId = useStore((state) => state.focusedParentId, shallow);
@ -138,6 +138,7 @@ export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth }
maxWidth: canvasMaxWidth,
margin: '0 auto',
transform: 'translateZ(0)',
...(isModuleMode && { height: '100%' }),
}}
>
{children}

View file

@ -10,6 +10,7 @@ import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
export const EditorSelecto = () => {
const { moduleId } = useModuleContext();
const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab);
const setRightSidebarOpen = useStore((state) => state.setRightSidebarOpen);
const setSelectedComponents = useStore((state) => state.setSelectedComponents);
const getSelectedComponents = useStore((state) => state.getSelectedComponents, shallow);
const getComponentDefinition = useStore((state) => state.getComponentDefinition);
@ -119,6 +120,7 @@ export const EditorSelecto = () => {
}
}
}
return false;
},
[setSelectedComponents, setActiveRightSideBarTab, getSelectedComponents]

View file

@ -1,5 +1,5 @@
.canvas-container{
&:focus-visible{
.canvas-container {
&:focus-visible {
outline: none;
}
@ -23,12 +23,12 @@
// }
// }
.empty-box-cont{
.empty-box-cont {
display: flex;
justify-content: center;
margin: unset !important;
.dotted-cont{
.dotted-cont {
border: 1px dashed var(--indigo8);
border-radius: 6px;
margin: 0 10px 0 10px;
@ -38,20 +38,20 @@
margin-bottom: 10px;
}
.title-text{
.title-text {
margin-top: 10px;
font-size: 16px;
font-weight: 600px;
}
.title-desc{
.title-desc {
font-size: 14px;
margin-top: 5px;
font-weight: 400;
}
.box-link{
.box-link {
display: flex;
margin-top: 10px;
@ -60,8 +60,8 @@
width: auto;
}
.link-but{
.link-but {
font-weight: 500;
font-size: 14px;
color: #3E63DD;
@ -70,26 +70,40 @@
}
*:focus-visible {
outline: none; // Rewriting it to replace outline coming from browser styles
}
outline: none; // Rewriting it to replace outline coming from browser styles
}
//[Container-widget]Show scrollbar only on hover
.real-canvas:not(.has-no-scroll) {
.real-canvas:not(.has-no-scroll) {
overflow: hidden auto;
scrollbar-width: none;
&:hover {
scrollbar-width: auto;
scrollbar-width: auto;
}
}
}
// This is required to maintain the height of the subcontainer when dragging a widget inside it
// This is required to maintain the height of the subcontainer when dragging a widget inside it
.real-canvas.is-child-being-dragged:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 500%;
overflow: hidden;
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 500%;
overflow: hidden;
}
.scrollbar-hidden {
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
/* IE and Edge */
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}

View file

@ -612,6 +612,12 @@ export function pasteComponents(targetParentId, copiedComponentObj) {
// Adjust width if parent changed
let width = component.layouts[currentLayout].width;
if (targetParentId !== component.component?.parent) {
const containerWidth = useGridStore.getState().subContainerWidths[targetParentId || 'canvas'];
const oldContainerWidth = useGridStore.getState().subContainerWidths[component?.component?.parent || 'canvas'];
width = Math.round((width * oldContainerWidth) / containerWidth);
}
component.layouts[currentLayout] = {
...component.layouts[currentLayout],
width,

View file

@ -98,7 +98,7 @@ const Portal = ({ children, ...restProps }) => {
const PopupIcon = ({ callback, icon, tip, position, isMultiEditor = false, isQueryManager = false }) => {
const size = 16;
const topRef = isNumber(position?.height) ? Math.floor(position?.height) - 30 : 32;
let top = isMultiEditor ? 270 : topRef > 32 ? topRef : 0;
let top = topRef > 32 ? topRef : 0;
// for query manager we allow the height of query manager to be dynamic, so we need to render the popup icon at the bottom of code editor
const renderAtBottom = isQueryManager && (isMultiEditor || topRef > 32);

View file

@ -62,14 +62,14 @@ const MultiLineCodeEditor = (props) => {
const wrapperRef = useRef(null);
const getSuggestions = useStore((state) => state.getSuggestions, shallow);
const getServerSideGlobalResolveSuggestions = useStore(
(state) => state.getServerSideGlobalResolveSuggestions,
shallow
(state) => state.getServerSideGlobalResolveSuggestions,
shallow
);
const isInsideQueryPane = !!document.querySelector('.code-hinter-wrapper')?.closest('.query-details');
const isInsideQueryManager = useMemo(
() => isInsideParent(wrapperRef?.current, 'query-manager'),
[wrapperRef.current]
() => isInsideParent(wrapperRef?.current, 'query-manager'),
[wrapperRef.current]
);
const context = useContext(CodeHinterContext);
@ -93,22 +93,22 @@ const MultiLineCodeEditor = (props) => {
// Intersection observer to detect when current line goes out of view
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.intersectionRatio < 1) {
setShowSuggestions(false);
isObserverTriggeredRef.current = true;
// Close autocomplete dropdown by dispatching a selection change
if (editorView) {
editorView.dispatch({
selection: editorView.state.selection,
});
([entry]) => {
if (entry.intersectionRatio < 1) {
setShowSuggestions(false);
isObserverTriggeredRef.current = true;
// Close autocomplete dropdown by dispatching a selection change
if (editorView) {
editorView.dispatch({
selection: editorView.state.selection,
});
}
} else {
setShowSuggestions(true);
isObserverTriggeredRef.current = false;
}
} else {
setShowSuggestions(true);
isObserverTriggeredRef.current = false;
}
},
{ root: null, threshold: [1] }
},
{ root: null, threshold: [1] }
);
currentLineObserverRef.current = observer;
@ -153,7 +153,7 @@ const MultiLineCodeEditor = (props) => {
function autoCompleteExtensionConfig(context) {
const hasWorkflowSuggestions =
workflowSuggestions?.appHints?.length > 0 || workflowSuggestions?.jsHints?.length > 0;
workflowSuggestions?.appHints?.length > 0 || workflowSuggestions?.jsHints?.length > 0;
const hints = hasWorkflowSuggestions ? workflowSuggestions : getSuggestions();
const serverHints = getServerSideGlobalResolveSuggestions(isInsideQueryManager);
@ -201,8 +201,8 @@ const MultiLineCodeEditor = (props) => {
const initialValueWithReplacedIds = useMemo(() => {
if (
typeof initialValue === 'string' &&
(initialValue?.includes('components') || initialValue?.includes('queries'))
typeof initialValue === 'string' &&
(initialValue?.includes('components') || initialValue?.includes('queries'))
) {
return replaceIdsWithName(initialValue);
}
@ -230,122 +230,123 @@ const MultiLineCodeEditor = (props) => {
};
return (
<div
className={`code-hinter-wrapper position-relative ${isInsideQueryPane ? 'code-editor-query-panel' : ''}`}
style={{ width: '100%' }}
ref={wrapperRef}
>
<div className={`${className} ${darkMode && 'cm-codehinter-dark-themed'}`}>
<CodeHinterBtns
view={editorView}
isPanelOpen={isSearchPanelOpen}
renderCopilot={() =>
renderCopilot?.({
darkMode,
language: lang,
editorRef,
onAiSuggestionAccept,
})
}
/>
<div
className={`code-hinter-wrapper position-relative ${isInsideQueryPane ? 'code-editor-query-panel' : ''}`}
style={{ width: '100%' }}
ref={wrapperRef}
>
<div className={`${className} ${darkMode && 'cm-codehinter-dark-themed'}`}>
<CodeHinterBtns
view={editorView}
isPanelOpen={isSearchPanelOpen}
renderCopilot={() =>
renderCopilot?.({
darkMode,
language: lang,
editorRef,
onAiSuggestionAccept,
})
}
/>
<CodeHinter.PopupIcon
callback={handleTogglePopupExapand}
icon="portal-open"
tip="Pop out code editor into a new window"
isMultiEditor={true}
isQueryManager={isInsideQueryPane}
/>
<CodeHinter.PopupIcon
callback={handleTogglePopupExapand}
icon="portal-open"
tip="Pop out code editor into a new window"
isMultiEditor={true}
isQueryManager={isInsideQueryPane}
position={{height: height}}
/>
<CodeHinter.Portal
isCopilotEnabled={false}
isOpen={isOpen}
callback={setIsOpen}
componentName={componentName}
key={componentName}
forceUpdate={forceUpdate}
optionalProps={{ styles: { height: 300 }, cls: '' }}
darkMode={darkMode}
selectors={{ className: 'preview-block-portal' }}
dragResizePortal={true}
callgpt={null}
>
<ErrorBoundary>
<div className="codehinter-container w-100 " data-cy={`${cyLabel}-input-field`} style={{ height: '100%' }}>
<CodeMirror
ref={editorRef}
value={initialValueWithReplacedIds}
placeholder={placeholder}
height={'100%'}
minHeight={heightInPx}
{...(isInsideQueryPane ? { maxHeight: '100%' } : {})}
width="100%"
theme={theme}
extensions={[
langExtention,
search({
createPanel: handleSearchPanel,
}),
javascriptLanguage.data.of({
autocomplete: overRideFunction,
}),
python().language.data.of({
autocomplete: overRideFunction,
}),
sql().language.data.of({
autocomplete: overRideFunction,
}),
sass().language.data.of({
autocomplete: sassCompletionSource,
}),
autocompletion({
override: [overRideFunction],
activateOnTyping: true,
compareCompletions: (a, b) => {
return a.section.rank - b.section.rank && a.label.localeCompare(b.label);
},
}),
customTabKeymap,
keymap.of([...customKeyMaps]),
]}
onChange={handleChange}
onBlur={handleOnBlur}
basicSetup={setupConfig}
style={{
overflowY: 'auto',
}}
className={`codehinter-multi-line-input ${isInsideQueryPane ? 'code-editor-query-panel' : ''}`}
indentWithTab={false}
readOnly={readOnly}
editable={editable} //for transformations in query manager
onCreateEditor={(view) => {
setEditorView(view);
if (setCodeEditorView) {
setCodeEditorView(view);
}
}}
onUpdate={(view) => {
setIsSearchPanelOpen(searchPanelOpen(view.state));
updateCurrentLineObserver(view);
}}
/>
</div>
{showPreview && (
<div className="multiline-previewbox-wrapper">
<PreviewBox
currentValue={currentValueRef.current}
validationSchema={null}
setErrorStateActive={() => null}
componentId={null}
setErrorMessage={() => null}
<CodeHinter.Portal
isCopilotEnabled={false}
isOpen={isOpen}
callback={setIsOpen}
componentName={componentName}
key={componentName}
forceUpdate={forceUpdate}
optionalProps={{ styles: { height: 300 }, cls: '' }}
darkMode={darkMode}
selectors={{ className: 'preview-block-portal' }}
dragResizePortal={true}
callgpt={null}
>
<ErrorBoundary>
<div className="codehinter-container w-100 " data-cy={`${cyLabel}-input-field`} style={{ height: '100%' }}>
<CodeMirror
ref={editorRef}
value={initialValueWithReplacedIds}
placeholder={placeholder}
height={heightInPx}
minHeight={heightInPx}
{...(isInsideQueryPane ? { maxHeight: '100%' } : {})}
width="100%"
theme={theme}
extensions={[
langExtention,
search({
createPanel: handleSearchPanel,
}),
javascriptLanguage.data.of({
autocomplete: overRideFunction,
}),
python().language.data.of({
autocomplete: overRideFunction,
}),
sql().language.data.of({
autocomplete: overRideFunction,
}),
sass().language.data.of({
autocomplete: sassCompletionSource,
}),
autocompletion({
override: [overRideFunction],
activateOnTyping: true,
compareCompletions: (a, b) => {
return a.section.rank - b.section.rank && a.label.localeCompare(b.label);
},
}),
customTabKeymap,
keymap.of([...customKeyMaps]),
]}
onChange={handleChange}
onBlur={handleOnBlur}
basicSetup={setupConfig}
style={{
overflowY: 'auto',
}}
className={`codehinter-multi-line-input ${isInsideQueryPane ? 'code-editor-query-panel' : ''}`}
indentWithTab={false}
readOnly={readOnly}
editable={editable} //for transformations in query manager
onCreateEditor={(view) => {
setEditorView(view);
if (setCodeEditorView) {
setCodeEditorView(view);
}
}}
onUpdate={(view) => {
setIsSearchPanelOpen(searchPanelOpen(view.state));
updateCurrentLineObserver(view);
}}
/>
</div>
)}
</ErrorBoundary>
</CodeHinter.Portal>
{showPreview && (
<div className="multiline-previewbox-wrapper">
<PreviewBox
currentValue={currentValueRef.current}
validationSchema={null}
setErrorStateActive={() => null}
componentId={null}
setErrorMessage={() => null}
/>
</div>
)}
</ErrorBoundary>
</CodeHinter.Portal>
</div>
</div>
</div>
);
};
export default MultiLineCodeEditor;
export default MultiLineCodeEditor;

View file

@ -299,8 +299,8 @@
height: 100%;
.cm-editor {
min-height: 300px;
height: 300px;
// min-height: 300px;
// height: 300px;
max-height: fit-content !important;
.cm-gutters {
@ -362,11 +362,6 @@
}
}
.cm-scroller {
overflow-y: auto !important;
overflow-x: hidden !important;
@ -390,6 +385,13 @@
}
}
.codehinter-popup{
.cm-editor{
border-radius: 0 0 4px 4px !important;
//box-shadow: none !important;
}
}
.rest-api-tab-content {
.fields-container {
.rest-api-codehinter-key-field {
@ -413,9 +415,9 @@
}
}
.runjs-editor .cm-editor {
border: none !important;
}
//.runjs-editor .cm-editor {
// border: none !important;
//}
.preview-alert-banner {
height: fit-content;
@ -488,8 +490,6 @@
.codehinter-input {
height: 100%;
border: none !important;
}
}

View file

@ -45,7 +45,9 @@ function EditAppName() {
<button
className="edit-app-name-button tw-h-8 tw-rounded-lg tw-pr-1 tw-w-auto tw-font-medium tw-cursor-pointer tw-outline-none tw-bg-transparent tw-border tw-border-transparent hover:tw-border-border-strong tw-shadow-none tw-group tw-transition-all tw-duration-300 tw-flex tw-items-center tw-relative"
type="button"
data-cy="edit-app-name-button"
onClick={() => setShowRenameModal(true)}
data-cy="editor-app-name-input"
>
<span className="tw-font-title-large tw-truncate tw-w-full tw-block group-hover:tw-w-[calc(100%-24px)]">
{appName}

View file

@ -41,7 +41,7 @@ const AppExport = ({ darkMode }) => {
setIsExportingApp(true);
document.getElementById('maintenance-app-modal').click();
}}
data-cy="button-user-status-change"
data-cy="export-app-button"
>
Export app
</Button>

View file

@ -35,9 +35,8 @@ export const SidebarItem = forwardRef(
const content = (
<Button
{...rest}
className={`${className} ${
selectedSidebarItem === icon && selectedSidebarItem !== 'comments' && 'sidebar-item--active'
} ${icon}-icon`}
className={`${className} ${selectedSidebarItem === icon && selectedSidebarItem !== 'comments' && 'sidebar-item--active'
} ${icon}-icon`}
onClick={onClick && onClick}
ref={ref}
type="button"
@ -45,6 +44,7 @@ export const SidebarItem = forwardRef(
variant="ghost"
size="default"
iconOnly
data-cy={`left-sidebar-${icon?.toLowerCase() || 'unknown'}-button`}
>
{children && (
<div className={'sidebar-svg-icon position-relative'}>

View file

@ -163,6 +163,22 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
updateDataQuery(options);
};
let docLinkStatic = '';
switch (selectedDataSource?.kind) {
case 'restapi':
docLinkStatic = `https://docs.tooljet.com/docs/data-sources/restapi/querying-rest-api`;
break;
case 'tooljetdb':
docLinkStatic = `https://docs.tooljet.com/docs/tooljet-db/querying-tooljet-db`;
break;
case 'runjs':
docLinkStatic = `https://docs.tooljet.com/docs/data-sources/run-js`;
break;
case 'runpy':
docLinkStatic = `https://docs.tooljet.com/docs/data-sources/run-py`;
break;
}
const renderQueryElement = () => {
return (
<div
@ -176,14 +192,28 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
selectedDataSource?.kind === 'runpy' ||
selectedDataSource?.kind === 'tooljetdb' ||
(selectedDataSource?.kind === 'restapi' && selectedDataSource?.type !== 'default')) && (
<ParameterList
parameters={options.parameters}
handleAddParameter={handleAddParameter}
handleParameterChange={handleParameterChange}
handleParameterRemove={handleParameterRemove}
darkMode={darkMode}
containerRef={paramListContainerRef}
/>
<>
<div style={{ marginBottom: '2px' }}>
{`To know more about querying ${selectedDataSource?.kind} data,`}
&nbsp;
<a
href={docLinkStatic}
target="_blank"
style={{ marginLeft: '0px !important', color: 'hsl(226, 70.0%, 55.5%)', textDecoration: 'underline' }}
rel="noreferrer"
>
{t('globals.readDocumentation', 'read documentation').toLowerCase()}
</a>
</div>
<ParameterList
parameters={options.parameters}
handleAddParameter={handleAddParameter}
handleParameterChange={handleParameterChange}
handleParameterRemove={handleParameterRemove}
darkMode={darkMode}
containerRef={paramListContainerRef}
/>
</>
)}
</div>
<ElementToRender
@ -314,7 +344,7 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
changeDataQuery(newDataSource);
}}
/>
<div>
<div style={{ marginBottom: '2px' }}>
{`To know more about querying ${selectedDataSource?.kind} data,`}
&nbsp;
<a

View file

@ -13,7 +13,7 @@ const Runjs = (props) => {
}, [props.options]);
return (
<Card className="runjs-editor mb-3 !tw-mb-0">
<div className="runjs-editor mb-3 !tw-mb-0 ">
<CodeHinter
renderCopilot={props.renderCopilot}
type="multiline"
@ -29,7 +29,7 @@ const Runjs = (props) => {
cyLabel={`runjs`}
delayOnChange={false}
/>
</Card>
</div>
);
};

View file

@ -8,9 +8,11 @@ import useStore from '@/AppBuilder/_stores/store';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
import useWorkflowStore from '@/_stores/workflowStore';
import { useTranslation } from 'react-i18next';
export function Workflows({ options, optionsChanged, currentState }) {
const { moduleId } = useModuleContext();
const { t } = useTranslation();
const [workflowOptions, setWorkflowOptions] = useState([]);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [_selectedWorkflowId, setSelectedWorkflowId] = useState(undefined);
@ -51,6 +53,18 @@ export function Workflows({ options, optionsChanged, currentState }) {
return (
<>
<div style={{ marginBottom: '2px' }}>
{`To know more about querying workflows data,`}
&nbsp;
<a
href={'https://docs.tooljet.ai/docs/workflows/how-to/trigger-workflow-from-app'}
target="_blank"
style={{ marginLeft: '0px !important', color: 'hsl(226, 70.0%, 55.5%)', textDecoration: 'underline' }}
rel="noreferrer"
>
{t('globals.readDocumentation', 'read documentation').toLowerCase()}
</a>
</div>
<label className="mb-1">Workflow</label>
<div data-cy="workflow-dropdown"></div>
<Select

View file

@ -122,11 +122,7 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
</div>
<div className="col query-row-query-name">
{isRenaming ? (
<QueryRenameInput
dataQuery={dataQuery}
darkMode={darkMode}
onUpdate={updateQueryName}
/>
<QueryRenameInput dataQuery={dataQuery} darkMode={darkMode} onUpdate={updateQueryName} />
) : (
<div className="query-name" data-cy={`list-query-${dataQuery.name.toLowerCase()}`}>
<span
@ -163,18 +159,20 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
</div>
)}
</div>
{!shouldFreeze && <div className={`col-auto query-rename-delete-btn ${isQuerySelected ? 'd-flex' : 'd-none'}`}>
<ButtonComponent
iconOnly
leadingIcon="morevertical01"
onClick={(e) => toggleQueryHandlerMenu(true, `query-handler-menu-${dataQuery?.id}`)}
size="small"
variant="outline"
className=""
id={`query-handler-menu-${dataQuery?.id}`}
data-cy={`delete-query-${dataQuery.name.toLowerCase()}`}
/>
</div>}
{!shouldFreeze && (
<div className={`col-auto query-rename-delete-btn ${isQuerySelected ? 'd-flex' : 'd-none'}`}>
<ButtonComponent
iconOnly
leadingIcon="morevertical01"
onClick={(e) => toggleQueryHandlerMenu(true, `query-handler-menu-${dataQuery?.id}`)}
size="small"
variant="outline"
className=""
id={`query-handler-menu-${dataQuery?.id}`}
data-cy={`query-handler-menu-${dataQuery.name.toLowerCase()}`}
/>
</div>
)}
</div>
<Confirm
show={isDeleting}

View file

@ -149,7 +149,7 @@ const QueryCardMenu = ({ darkMode }) => {
{QUERY_MENU_OPTIONS.map((option) => {
const optionBody = (
<div
data-cy={`component-inspector-${String(option?.value).toLowerCase()}-button`}
data-cy={`query-card-${String(option?.value).toLowerCase()}-button`}
className="list-item-popover-option"
key={option?.value}
onClick={(e) => {

View file

@ -82,7 +82,9 @@ const FxSelect = ({
/** Remove minFileCount and maxFileCount validations if multiple file selection is disabled */
const getValidations = (componentMeta, component) => {
const validations = Object.keys(componentMeta.validation || {});
const enableMultipleValue = resolveReferences(component.component.definition.properties.enableMultiple?.value ?? false);
const enableMultipleValue = resolveReferences(
component.component.definition.properties.enableMultiple?.value ?? false
);
const enableMultipleFxActive = component.component.definition.properties.enableMultiple?.fxActive;
if (!enableMultipleValue && !enableMultipleFxActive) {
@ -110,10 +112,17 @@ const getPropertiesBySection = (propertiesMeta) => {
const getConditionalAccordionItems = (component, renderCustomElement) => {
const parseContent = resolveReferences(component.component.definition.properties.parseContent?.value ?? false);
const parseFileType = resolveReferences(
component.component.definition.properties.parseFileType?.value ?? 'auto-detect'
);
const options = ['parseContent'];
let renderOptions = options.map((option) => renderCustomElement(option));
const conditionalOptions = [{ name: 'parseFileType', condition: parseContent }];
const conditionalOptions = [
{ name: 'parseFileType', condition: parseContent },
{ name: 'delimiter', condition: parseContent && parseFileType === 'csv' },
];
conditionalOptions.forEach(({ name, condition }) => {
if (condition) renderOptions.push(renderCustomElement(name));
});

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