Skip to content

Commit

Permalink
feat: setup cypress e2e tests
Browse files Browse the repository at this point in the history
  • Loading branch information
perzeuss committed Oct 23, 2024
1 parent 4003922 commit b19a88c
Show file tree
Hide file tree
Showing 33 changed files with 3,627 additions and 1,712 deletions.
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>

# 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={`${type}-toast-message`} 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

0 comments on commit b19a88c

Please sign in to comment.