mirror of
https://github.com/langgenius/dify.git
synced 2026-04-11 11:02:43 +00:00
Compare commits
4 Commits
verify-dat
...
docs/e2e-w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0cc019f7e | ||
|
|
7237aa5eb8 | ||
|
|
66183c1f0a | ||
|
|
130ad295d0 |
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@@ -7,6 +7,7 @@
|
||||
## Summary
|
||||
|
||||
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
||||
<!-- If this PR was created by an automated agent, add `From <Tool Name>` as the final line of the description. Example: `From Codex`. -->
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
||||
@@ -97,8 +97,3 @@ Feel free to reach out if you encounter any issues during the setup process.
|
||||
## Getting Help
|
||||
|
||||
If you ever get stuck or get a burning question while contributing, simply shoot your queries our way via the related GitHub issue, or hop onto our [Discord](https://discord.gg/8Tpq4AcN9c) for a quick chat.
|
||||
|
||||
## Automated Agent Contributions
|
||||
|
||||
> [!NOTE]
|
||||
> If you are an automated agent, just add 🤖🤖🤖 to the end of the PR title to opt-in.
|
||||
|
||||
@@ -14,9 +14,15 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import redis
|
||||
from redis.cluster import RedisCluster
|
||||
from redis.exceptions import LockNotOwnedError, RedisError
|
||||
from redis.lock import Lock
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from extensions.ext_redis import RedisClientWrapper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,21 +44,21 @@ class DbMigrationAutoRenewLock:
|
||||
primary error/exit code.
|
||||
"""
|
||||
|
||||
_redis_client: Any
|
||||
_redis_client: redis.Redis | RedisCluster | RedisClientWrapper
|
||||
_name: str
|
||||
_ttl_seconds: float
|
||||
_renew_interval_seconds: float
|
||||
_log_context: str | None
|
||||
_logger: logging.Logger
|
||||
|
||||
_lock: Any
|
||||
_lock: Lock | None
|
||||
_stop_event: threading.Event | None
|
||||
_thread: threading.Thread | None
|
||||
_acquired: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
redis_client: Any,
|
||||
redis_client: redis.Redis | RedisCluster | RedisClientWrapper,
|
||||
name: str,
|
||||
ttl_seconds: float = 60,
|
||||
renew_interval_seconds: float | None = None,
|
||||
@@ -127,7 +133,7 @@ class DbMigrationAutoRenewLock:
|
||||
)
|
||||
self._thread.start()
|
||||
|
||||
def _heartbeat_loop(self, lock: Any, stop_event: threading.Event) -> None:
|
||||
def _heartbeat_loop(self, lock: Lock, stop_event: threading.Event) -> None:
|
||||
while not stop_event.wait(self._renew_interval_seconds):
|
||||
try:
|
||||
lock.reacquire()
|
||||
|
||||
134
e2e/AGENTS.md
134
e2e/AGENTS.md
@@ -165,3 +165,137 @@ Open the HTML report locally with:
|
||||
```bash
|
||||
open cucumber-report/report.html
|
||||
```
|
||||
|
||||
## Writing new scenarios
|
||||
|
||||
### Workflow
|
||||
|
||||
1. Create a `.feature` file under `features/<capability>/`
|
||||
2. Add step definitions under `features/step-definitions/<capability>/`
|
||||
3. Reuse existing steps from `common/` and other definition files before writing new ones
|
||||
4. Run with `pnpm -C e2e e2e -- --tags @your-tag` to verify
|
||||
5. Run `pnpm -C e2e check` before committing
|
||||
|
||||
### Feature file conventions
|
||||
|
||||
Tag every feature with a capability tag and an auth tag:
|
||||
|
||||
```gherkin
|
||||
@datasets @authenticated
|
||||
Feature: Create dataset
|
||||
Scenario: Create a new empty dataset
|
||||
Given I am signed in as the default E2E admin
|
||||
When I open the datasets page
|
||||
...
|
||||
```
|
||||
|
||||
- Capability tags (`@apps`, `@auth`, `@datasets`, …) group related scenarios for selective runs
|
||||
- Auth tags control the `Before` hook behavior:
|
||||
- `@authenticated` — injects the shared auth storageState into the BrowserContext
|
||||
- `@unauthenticated` — uses a clean BrowserContext with no cookies or storage
|
||||
- `@fresh` — only runs in `e2e:full` mode (requires uninitialized instance)
|
||||
- `@skip` — excluded from all runs
|
||||
|
||||
Keep scenarios short and declarative. Each step should describe **what** the user does, not **how** the UI works.
|
||||
|
||||
### Step definition conventions
|
||||
|
||||
```typescript
|
||||
import { When, Then } from '@cucumber/cucumber'
|
||||
import { expect } from '@playwright/test'
|
||||
import type { DifyWorld } from '../../support/world'
|
||||
|
||||
When('I open the datasets page', async function (this: DifyWorld) {
|
||||
await this.getPage().goto('/datasets')
|
||||
})
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Always type `this` as `DifyWorld` for proper context access
|
||||
- Use `async function` (not arrow functions — Cucumber binds `this`)
|
||||
- One step = one user-visible action or one assertion
|
||||
- Keep steps stateless across scenarios; use `DifyWorld` properties for in-scenario state
|
||||
|
||||
### Locator priority
|
||||
|
||||
Follow the Playwright recommended locator strategy, in order of preference:
|
||||
|
||||
| Priority | Locator | Example | When to use |
|
||||
|---|---|---|---|
|
||||
| 1 | `getByRole` | `getByRole('button', { name: 'Create' })` | Default choice — accessible and resilient |
|
||||
| 2 | `getByLabel` | `getByLabel('App name')` | Form inputs with visible labels |
|
||||
| 3 | `getByPlaceholder` | `getByPlaceholder('Enter name')` | Inputs without visible labels |
|
||||
| 4 | `getByText` | `getByText('Welcome')` | Static text content |
|
||||
| 5 | `getByTestId` | `getByTestId('workflow-canvas')` | Only when no semantic locator works |
|
||||
|
||||
Avoid raw CSS/XPath selectors. They break when the DOM structure changes.
|
||||
|
||||
### Assertions
|
||||
|
||||
Use `@playwright/test` `expect` — it auto-waits and retries until the condition is met or the timeout expires:
|
||||
|
||||
```typescript
|
||||
// URL assertion
|
||||
await expect(page).toHaveURL(/\/datasets\/[a-f0-9-]+\/documents/)
|
||||
|
||||
// Element visibility
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible()
|
||||
|
||||
// Element state
|
||||
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled()
|
||||
|
||||
// Negation
|
||||
await expect(page.getByText('Loading')).not.toBeVisible()
|
||||
```
|
||||
|
||||
Do not use manual `waitForTimeout` or polling loops. If you need a longer wait for a specific assertion, pass `{ timeout: 30_000 }` to the assertion.
|
||||
|
||||
### Cucumber expressions
|
||||
|
||||
Use Cucumber expression parameter types to extract values from Gherkin steps:
|
||||
|
||||
| Type | Pattern | Example step |
|
||||
|---|---|---|
|
||||
| `{string}` | Quoted string | `I select the "Workflow" app type` |
|
||||
| `{int}` | Integer | `I should see {int} items` |
|
||||
| `{float}` | Decimal | `the progress is {float} percent` |
|
||||
| `{word}` | Single word | `I click the {word} tab` |
|
||||
|
||||
Prefer `{string}` for UI labels, names, and text content — it maps naturally to Gherkin's quoted values.
|
||||
|
||||
### Scoping locators
|
||||
|
||||
When the page has multiple similar elements, scope locators to a container:
|
||||
|
||||
```typescript
|
||||
When('I fill in the app name in the dialog', async function (this: DifyWorld) {
|
||||
const dialog = this.getPage().getByRole('dialog')
|
||||
await dialog.getByPlaceholder('Give your app a name').fill('My App')
|
||||
})
|
||||
```
|
||||
|
||||
### Failure diagnostics
|
||||
|
||||
The `After` hook automatically captures on failure:
|
||||
|
||||
- Full-page screenshot (PNG)
|
||||
- Page HTML dump
|
||||
- Console errors and page errors
|
||||
|
||||
Artifacts are saved to `cucumber-report/artifacts/` and attached to the HTML report. No extra code needed in step definitions.
|
||||
|
||||
## Reusing existing steps
|
||||
|
||||
Before writing a new step definition, check what already exists. Steps in `common/` are designed for broad reuse across all features.
|
||||
|
||||
List all registered step patterns:
|
||||
|
||||
```bash
|
||||
grep -rn "Given\|When\|Then" e2e/features/step-definitions/ --include='*.ts' | grep -oP "'[^']+'"
|
||||
```
|
||||
|
||||
Or browse the step definition files directly:
|
||||
|
||||
- `features/step-definitions/common/` — auth guards and navigation assertions shared by all features
|
||||
- `features/step-definitions/<capability>/` — domain-specific steps scoped to a single feature area
|
||||
|
||||
Reference in New Issue
Block a user