test: add P0 workflow run, publish, and share scenarios (#35559)

This commit is contained in:
Jingyi 2026-04-24 21:48:17 -07:00 committed by GitHub
parent e6ef774fd5
commit f00512dd5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 215 additions and 1 deletions

View 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

View 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

View 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 })
})

View 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 })
})

View File

@ -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) {

View File

@ -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) {

View File

@ -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

View File

@ -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()
}
}