Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce cypress e2e tests #9520

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,21 @@ FROM mcr.microsoft.com/devcontainers/python:3.10

# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# && apt-get -y install --no-install-recommends <your-package-list-here>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@takatost The frontend team rarely use devcontainer, and backend team don't care about e2e test, maybe remove these change will be better?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your feedback! I understand the concerns about adding Cypress dependencies to the Dockerfile for the devcontainer. However, In my opinion, we have enough reasons to keep them:

  • We have frontend developers in the open-source community who may want to use the devcontainer for their contributions.
  • The devcontainer serves as the base image for running the e2e tests in the CI pipeline.
  • Even though the backend team may not prioritize end-to-end tests, it's essential for them to run these tests when making significant changes.
  • Installing these dependencies doesn't take up much space or time, and since the devcontainer is rarely rebuilt, the impact on development workflows is negligible.
  • Removing these dependencies adds another hurdle for contributors when it comes to running tests after making changes, which goes against the purpose of devcontainers. They are meant to provide a seamless development environment that just works, making it easier for everyone to contribute effectively.

# Cypress dependencies - Cypress is our end-to-end testing tool
# https://docs.cypress.io/guides/continuous-integration/introduction#Dependencies
RUN apt-get update && \
export DEBIAN_FRONTEND=noninteractive && \
apt-get -y install --no-install-recommends \
libgtk2.0-0 \
libgtk-3-0 \
libgbm-dev \
libnotify-dev \
libnss3 \
libxss1 \
libasound2 \
libxtst6 \
xauth \
xvfb \
x11vnc
16 changes: 13 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,34 @@
"version": "latest",
"dockerDashComposeVersion": "v2"
}
// "ghcr.io/devcontainers/features/desktop-lite:1": {
// // Required to runs a GUI for Cypress (yarn cy:open)
// }
},
"customizations": {
"vscode": {
"extensions": [
"ms-python.pylint",
"GitHub.copilot",
"ms-python.python"
"ms-python.python",
"alexkrechik.cucumberautocomplete"
]
}
},
"postStartCommand": "./.devcontainer/post_start_command.sh",
"postCreateCommand": "./.devcontainer/post_create_command.sh"
"postCreateCommand": "./.devcontainer/post_create_command.sh",


// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
"forwardPorts": [
// 6080 // VNC client for Cypress
],
"portsAttributes": {
"6080": { "label": "desktop" } // VNC client for Cypress
}

// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "python --version",
Expand Down
1 change: 1 addition & 0 deletions .devcontainer/post_create_command.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/bin/bash

cd web && npm install
npm i -g cypress
pipx install poetry

echo 'alias start-api="cd /workspaces/dify/api && poetry run python -m flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc
Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/cypress-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Cypress Tests

on:
workflow_dispatch:
pull_request:
push:
branches:
- main

jobs:
e2e-tests:
runs-on: ubuntu-latest
env:
CYPRESS_COMMAND_TIMEOUT: ${{ vars.CYPRESS_COMMAND_TIMEOUT }}
steps:
- name: Checkout (GitHub)
uses: actions/checkout@v4

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.CYPRESS_CONTAINER_REGISTRY_TOKEN }}

- name: Build and run dev container task
uses: devcontainers/[email protected]
with:
imageName: ghcr.io/${{ vars.DOCKER_NAMESPACE }}/${{ vars.DEVCONTAINER_IMAGE_NAME }}
cacheFrom: ghcr.io/${{ vars.DOCKER_NAMESPACE }}/${{ vars.DEVCONTAINER_IMAGE_NAME }}
push: always
runCmd: cd web && yarn test:e2e:ci

- name: Upload Cypress videos
uses: actions/upload-artifact@v4
with:
name: cypress-videos
path: web/cypress/videos/**/*.mp4
if: failure()

- name: Upload Cypress screenshots
uses: actions/upload-artifact@v4
with:
name: cypress-screenshots
path: web/cypress/screenshots/**/*.png
if: failure()
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ docker/volumes/unstructured/*
docker/volumes/pgvector/data/*
docker/volumes/pgvecto_rs/data/*

docker/volumes/cypress/*

docker/nginx/conf.d/default.conf
docker/nginx/ssl/*
!docker/nginx/ssl/.gitkeep
Expand All @@ -189,4 +191,4 @@ pyrightconfig.json
api/.vscode

.idea/
.vscode
.vscode
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ The website is bootstrapped on [Next.js](https://nextjs.org/) boilerplate in Typ
├── types // descriptions of function params and return values
└── utils // Shared utility functions
```
#### Frontend Docs
- [E2E Testing](web/cypress/README.md)

## Submitting your PR

Expand Down
32 changes: 32 additions & 0 deletions docker/docker-compose.cypress.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# This Docker Compose file extends services from docker-compose.yaml
# specifically for running end-to-end (e2e) tests.
# It simplifies configuration by overriding certain values here.
#
# Additionally, the volume mounts defined in docker-compose.yaml
# are customizable to ensure that test data is stored in docker/volumes/cypress.

name: dify-cypress
services:
api:
extends:
file: docker-compose.yaml
service: api
ports:
- 5001:5001

db:
extends:
file: docker-compose.yaml
service: db
ports:
- 54321:5432

redis:
extends:
file: docker-compose.yaml
service: redis

networks:
ssrf_proxy_network:
driver: bridge
internal: true
8 changes: 4 additions & 4 deletions docker/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ services:
- redis
volumes:
# Mount the storage directory to the container, for storing user files.
- ./volumes/app/storage:/app/api/storage
- ${API_STORAGE_PATH:-./volumes/app/storage}:/app/api/storage
networks:
- ssrf_proxy_network
- default
Expand All @@ -273,7 +273,7 @@ services:
- redis
volumes:
# Mount the storage directory to the container, for storing user files.
- ./volumes/app/storage:/app/api/storage
- ${API_STORAGE_PATH:-./volumes/app/storage}:/app/api/storage
networks:
- ssrf_proxy_network
- default
Expand Down Expand Up @@ -306,7 +306,7 @@ services:
-c 'maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-64MB}'
-c 'effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB}'
volumes:
- ./volumes/db/data:/var/lib/postgresql/data
- ${POSTGRES_DATA_PATH:-./volumes/db/data}:/var/lib/postgresql/data
healthcheck:
test: ['CMD', 'pg_isready']
interval: 1s
Expand All @@ -319,7 +319,7 @@ services:
restart: always
volumes:
# Mount the redis data directory to the container.
- ./volumes/redis/data:/data
- ${REDIS_DATA_PATH:-./volumes/redis/data}:/data
# Set the redis password when startup redis server.
command: redis-server --requirepass ${REDIS_PASSWORD:-difyai123456}
healthcheck:
Expand Down
6 changes: 5 additions & 1 deletion web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@ package-lock.json
pnpm-lock.yaml

.favorites.json
*storybook.log
*storybook.log

# Cypress
cypress/videos
cypress/screenshots
4 changes: 2 additions & 2 deletions web/app/components/base/toast/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const Toast = ({
if (typeof message !== 'string')
return null

return <div className={classNames(
return <div data-testid="toast" className={classNames(
className,
'fixed rounded-md p-4 my-4 mx-8 z-[9999]',
'top-0',
Expand All @@ -53,7 +53,7 @@ const Toast = ({
{type === 'info' && <InformationCircleIcon className="w-5 h-5 text-blue-400" aria-hidden="true" />}
</div>
<div className="ml-3">
<h3 className={
<h3 data-testid={`toast-message-${type}`} className={
classNames(
'text-sm font-medium',
type === 'success' ? 'text-green-800' : '',
Expand Down
2 changes: 1 addition & 1 deletion web/app/components/header/nav/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const Nav = ({
${isActivated && 'bg-components-main-nav-nav-button-bg-active shadow-md font-semibold'}
${!curNav && !isActivated && 'hover:bg-components-main-nav-nav-button-bg-hover'}
`}>
<Link href={link}>
<Link href={link} data-testid={`nav-item${link}`}>
<div
onClick={() => setAppDetail()}
className={classNames(`
Expand Down
8 changes: 6 additions & 2 deletions web/app/install/installForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const InstallForm = () => {
<input
{...register('email')}
placeholder={t('login.emailPlaceholder') || ''}
data-testid="email-input"
className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm'}
/>
{errors.email && <span className='text-red-400 text-sm'>{t(`${errors.email?.message}`)}</span>}
Expand All @@ -114,6 +115,7 @@ const InstallForm = () => {
<input
{...register('name')}
placeholder={t('login.namePlaceholder') || ''}
data-testid="name-input"
className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'}
/>
</div>
Expand All @@ -129,6 +131,7 @@ const InstallForm = () => {
{...register('password')}
type={showPassword ? 'text' : 'password'}
placeholder={t('login.passwordPlaceholder') || ''}
data-testid="password-input"
className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'}
/>

Expand All @@ -137,6 +140,7 @@ const InstallForm = () => {
type="button"
onClick={() => setShowPassword(!showPassword)}
className="text-gray-400 hover:text-gray-500 focus:outline-none focus:text-gray-500"
data-testid="toggle-password-visibility"
>
{showPassword ? '👀' : '😝'}
</button>
Expand All @@ -149,14 +153,14 @@ const InstallForm = () => {
</div>

<div>
<Button variant='primary' className='w-full' onClick={handleSetting}>
<Button variant='primary' className='w-full' onClick={handleSetting} data-testid="install-button">
{t('login.installBtn')}
</Button>
</div>
</form>
<div className="block w-full mt-2 text-xs text-gray-600">
{t('login.license.tip')}
&nbsp;
&nbsp;
<Link
className='text-primary-600'
target='_blank' rel='noopener noreferrer'
Expand Down
3 changes: 3 additions & 0 deletions web/app/signin/components/mail-and-password-auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export default function MailAndPasswordAuth({ isInvite, allowRegistration }: Mai
id="email"
type="email"
autoComplete="email"
data-testid="email-input"
placeholder={t('login.emailPlaceholder') || ''}
tabIndex={1}
/>
Expand All @@ -139,6 +140,7 @@ export default function MailAndPasswordAuth({ isInvite, allowRegistration }: Mai
}}
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
data-testid="password-input"
placeholder={t('login.passwordPlaceholder') || ''}
tabIndex={2}
/>
Expand All @@ -158,6 +160,7 @@ export default function MailAndPasswordAuth({ isInvite, allowRegistration }: Mai
<Button
tabIndex={2}
variant='primary'
data-testid="login-button"
onClick={handleEmailPasswordLogin}
disabled={isLoading || !email || !password}
className="w-full"
Expand Down
54 changes: 54 additions & 0 deletions web/cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { addCucumberPreprocessorPlugin } from '@badeball/cypress-cucumber-preprocessor'
import { createEsbuildPlugin } from '@badeball/cypress-cucumber-preprocessor/esbuild'
import createBundler from '@bahmutov/cypress-esbuild-preprocessor'
import { defineConfig } from 'cypress'
import { PostgresClient } from './cypress/support/db'

export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
specPattern: '**/*.feature',
video: true,
env: {
SLOW_DOWN: false,
},
// it might take a while for nextjs to compile the page on first visit
defaultCommandTimeout: Number(process.env.CYPRESS_COMMAND_TIMEOUT) || 10000,
async setupNodeEvents(
on: Cypress.PluginEvents,
config: Cypress.PluginConfigOptions,
): Promise<Cypress.PluginConfigOptions> {
await addCucumberPreprocessorPlugin(on, config)

on(
'file:preprocessor',
createBundler({
plugins: [createEsbuildPlugin(config)],
}),
)

const dbInstance = new PostgresClient()

on('task', {
runQuery: async ({ query, params }: { query: string; params?: any[] }) => {
// This will only create a new connection if it's not already connected
await dbInstance.connect({
user: 'postgres',
host: 'localhost',
database: 'dify',
password: 'difyai123456',
port: 54321,
})
return dbInstance.query(query, params)
},
})

on('after:run', async () => {
// This will be called once after all tests
await dbInstance.close()
})

return config
},
},
})
Loading
Loading