mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 10:06:51 +08:00
test: add P0 workflow run, publish, and share scenarios (#35559)
This commit is contained in:
parent
e6ef774fd5
commit
f00512dd5d
19
e2e/features/apps/share-app.feature
Normal file
19
e2e/features/apps/share-app.feature
Normal file
@ -0,0 +1,19 @@
|
||||
@apps @authenticated @core
|
||||
Feature: Share app publicly
|
||||
|
||||
Scenario: Enable public share for a published workflow app
|
||||
Given I am signed in as the default E2E admin
|
||||
And a "workflow" app has been created via API
|
||||
And a minimal runnable workflow draft has been synced
|
||||
When I open the app from the app list
|
||||
And I open the publish panel
|
||||
And I publish the app
|
||||
And I navigate to the app overview page
|
||||
And I enable the Web App share
|
||||
Then the Web App should be in service
|
||||
|
||||
@unauthenticated
|
||||
Scenario: Access a shared workflow app without authentication
|
||||
Given a workflow app has been published and shared via API
|
||||
When I open the shared app URL
|
||||
Then the shared app page should be accessible
|
||||
13
e2e/features/apps/workflow-run-publish.feature
Normal file
13
e2e/features/apps/workflow-run-publish.feature
Normal file
@ -0,0 +1,13 @@
|
||||
@apps @authenticated @core @mode-matrix
|
||||
Feature: Workflow run and publish
|
||||
|
||||
Scenario: Run and publish a minimal workflow app
|
||||
Given I am signed in as the default E2E admin
|
||||
And a "workflow" app has been created via API
|
||||
And a minimal runnable workflow draft has been synced
|
||||
When I open the app from the app list
|
||||
And I run the workflow
|
||||
Then the workflow run should succeed
|
||||
When I open the publish panel
|
||||
And I publish the app
|
||||
Then the app should be marked as published
|
||||
39
e2e/features/step-definitions/apps/share-app.steps.ts
Normal file
39
e2e/features/step-definitions/apps/share-app.steps.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import type { DifyWorld } from '../../support/world'
|
||||
import { Given, Then, When } from '@cucumber/cucumber'
|
||||
import { expect } from '@playwright/test'
|
||||
import { createTestApp, enableAppSiteAndGetURL, publishWorkflowApp, syncRunnableWorkflowDraft } from '../../../support/api'
|
||||
|
||||
When('I enable the Web App share', async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
const appName = this.lastCreatedAppName
|
||||
if (!appName)
|
||||
throw new Error('No app name available. Run "a \\"workflow\\" app has been created via API" first.')
|
||||
|
||||
await page.locator('button').filter({ hasText: appName }).filter({ hasText: 'Workflow' }).click()
|
||||
await expect(page.getByRole('switch').first()).toBeEnabled({ timeout: 15_000 })
|
||||
await page.getByRole('switch').first().click()
|
||||
})
|
||||
|
||||
Then('the Web App should be in service', async function (this: DifyWorld) {
|
||||
await expect(this.getPage().getByText('In Service').first()).toBeVisible({ timeout: 10_000 })
|
||||
})
|
||||
|
||||
Given('a workflow app has been published and shared via API', async function (this: DifyWorld) {
|
||||
const app = await createTestApp(`E2E Share ${Date.now()}`, 'workflow')
|
||||
this.createdAppIds.push(app.id)
|
||||
this.lastCreatedAppName = app.name
|
||||
await syncRunnableWorkflowDraft(app.id)
|
||||
await publishWorkflowApp(app.id)
|
||||
this.shareURL = await enableAppSiteAndGetURL(app.id)
|
||||
})
|
||||
|
||||
When('I open the shared app URL', async function (this: DifyWorld) {
|
||||
if (!this.shareURL)
|
||||
throw new Error('No share URL available. Run "a workflow app has been published and shared via API" first.')
|
||||
await this.getPage().goto(this.shareURL, { timeout: 20_000 })
|
||||
})
|
||||
|
||||
Then('the shared app page should be accessible', async function (this: DifyWorld) {
|
||||
await expect(this.getPage()).toHaveURL(/\/(workflow|chat)\/[a-zA-Z0-9]+/, { timeout: 15_000 })
|
||||
await expect(this.getPage().locator('body')).toBeVisible({ timeout: 10_000 })
|
||||
})
|
||||
23
e2e/features/step-definitions/apps/workflow-run.steps.ts
Normal file
23
e2e/features/step-definitions/apps/workflow-run.steps.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import type { DifyWorld } from '../../support/world'
|
||||
import { Given, Then, When } from '@cucumber/cucumber'
|
||||
import { expect } from '@playwright/test'
|
||||
import { syncRunnableWorkflowDraft } from '../../../support/api'
|
||||
|
||||
Given('a minimal runnable workflow draft has been synced', async function (this: DifyWorld) {
|
||||
const appId = this.createdAppIds.at(-1)
|
||||
if (!appId)
|
||||
throw new Error('No app ID found. Run "a \\"workflow\\" app has been created via API" first.')
|
||||
await syncRunnableWorkflowDraft(appId)
|
||||
})
|
||||
|
||||
When('I run the workflow', async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
await page.getByText('Test Run').click()
|
||||
await expect(page.getByText('Running').first()).toBeVisible({ timeout: 15_000 })
|
||||
})
|
||||
|
||||
Then('the workflow run should succeed', async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
await page.getByText('DETAIL').click()
|
||||
await expect(page.getByText('SUCCESS').first()).toBeVisible({ timeout: 55_000 })
|
||||
})
|
||||
@ -15,6 +15,7 @@ export class DifyWorld extends World {
|
||||
lastCreatedAppName: string | undefined
|
||||
createdAppIds: string[] = []
|
||||
capturedDownloads: Download[] = []
|
||||
shareURL: string | undefined
|
||||
|
||||
constructor(options: IWorldOptions) {
|
||||
super(options)
|
||||
@ -27,6 +28,7 @@ export class DifyWorld extends World {
|
||||
this.lastCreatedAppName = undefined
|
||||
this.createdAppIds = []
|
||||
this.capturedDownloads = []
|
||||
this.shareURL = undefined
|
||||
}
|
||||
|
||||
async startSession(browser: Browser, authenticated: boolean) {
|
||||
|
||||
@ -67,11 +67,20 @@ const main = async () => {
|
||||
logFilePath: path.join(logDir, 'cucumber-api.log'),
|
||||
})
|
||||
|
||||
const celeryProcess = await startLoggedProcess({
|
||||
command: 'npx',
|
||||
args: ['tsx', './scripts/setup.ts', 'celery'],
|
||||
cwd: e2eDir,
|
||||
label: 'celery worker',
|
||||
logFilePath: path.join(logDir, 'cucumber-celery.log'),
|
||||
})
|
||||
|
||||
let cleanupPromise: Promise<void> | undefined
|
||||
const cleanup = async () => {
|
||||
if (!cleanupPromise) {
|
||||
cleanupPromise = (async () => {
|
||||
await stopWebServer()
|
||||
await stopManagedProcess(celeryProcess)
|
||||
await stopManagedProcess(apiProcess)
|
||||
|
||||
if (startMiddlewareForRun) {
|
||||
|
||||
@ -202,6 +202,32 @@ export const startApi = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
export const startCelery = async () => {
|
||||
const env = await getApiEnvironment()
|
||||
|
||||
await runForegroundProcess({
|
||||
command: 'uv',
|
||||
args: [
|
||||
'run',
|
||||
'--project',
|
||||
'.',
|
||||
'--no-sync',
|
||||
'celery',
|
||||
'-A',
|
||||
'app.celery',
|
||||
'worker',
|
||||
'--pool',
|
||||
'solo',
|
||||
'--loglevel',
|
||||
'INFO',
|
||||
'-Q',
|
||||
'workflow_based_app_execution',
|
||||
],
|
||||
cwd: apiDir,
|
||||
env,
|
||||
})
|
||||
}
|
||||
|
||||
export const stopMiddleware = async () => {
|
||||
await runCommandOrThrow({
|
||||
command: 'docker',
|
||||
@ -308,7 +334,7 @@ export const startMiddleware = async () => {
|
||||
}
|
||||
|
||||
const printUsage = () => {
|
||||
console.log('Usage: tsx ./scripts/setup.ts <reset|middleware-up|middleware-down|api|web>')
|
||||
console.log('Usage: tsx ./scripts/setup.ts <reset|middleware-up|middleware-down|api|celery|web>')
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
@ -318,6 +344,9 @@ const main = async () => {
|
||||
case 'api':
|
||||
await startApi()
|
||||
return
|
||||
case 'celery':
|
||||
await startCelery()
|
||||
return
|
||||
case 'middleware-down':
|
||||
await stopMiddleware()
|
||||
return
|
||||
|
||||
@ -80,3 +80,83 @@ export async function deleteTestApp(id: string): Promise<void> {
|
||||
await ctx.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncRunnableWorkflowDraft(appId: string): Promise<void> {
|
||||
const ctx = await createApiContext()
|
||||
try {
|
||||
await ctx.post(`/console/api/apps/${appId}/workflows/draft`, {
|
||||
data: {
|
||||
graph: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'start',
|
||||
type: 'custom',
|
||||
position: { x: 80, y: 282 },
|
||||
data: { id: 'start', type: 'start', title: 'Start', variables: [] },
|
||||
},
|
||||
{
|
||||
id: 'end',
|
||||
type: 'custom',
|
||||
position: { x: 480, y: 282 },
|
||||
data: {
|
||||
id: 'end',
|
||||
type: 'end',
|
||||
title: 'End',
|
||||
outputs: [{ variable: 'result', value_selector: ['sys', 'workflow_run_id'] }],
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'start-end',
|
||||
type: 'custom',
|
||||
source: 'start',
|
||||
target: 'end',
|
||||
sourceHandle: 'source',
|
||||
targetHandle: 'target',
|
||||
},
|
||||
],
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
},
|
||||
features: {},
|
||||
environment_variables: [],
|
||||
conversation_variables: [],
|
||||
},
|
||||
})
|
||||
}
|
||||
finally {
|
||||
await ctx.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
export async function publishWorkflowApp(appId: string): Promise<void> {
|
||||
const ctx = await createApiContext()
|
||||
try {
|
||||
await ctx.post(`/console/api/apps/${appId}/workflows/publish`, {
|
||||
data: { marked_name: '', marked_comment: '' },
|
||||
})
|
||||
}
|
||||
finally {
|
||||
await ctx.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
type AppDetailWithSite = {
|
||||
site: { access_token: string, app_base_url: string, enable_site: boolean }
|
||||
}
|
||||
|
||||
export async function enableAppSiteAndGetURL(appId: string): Promise<string> {
|
||||
const ctx = await createApiContext()
|
||||
try {
|
||||
await ctx.post(`/console/api/apps/${appId}/site-enable`, {
|
||||
data: { enable_site: true },
|
||||
})
|
||||
const res = await ctx.get(`/console/api/apps/${appId}`)
|
||||
const body = (await res.json()) as AppDetailWithSite
|
||||
const { app_base_url, access_token } = body.site
|
||||
return `${app_base_url}/workflow/${access_token}`
|
||||
}
|
||||
finally {
|
||||
await ctx.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user