0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-06-29 22:57:20 +02:00
gitea/tests/e2e/project-workflow.test.ts
Lunny Xiao cc11cdaf4e
update
2026-05-16 18:36:45 -07:00

362 lines
15 KiB
TypeScript

// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
import {env} from 'node:process';
import {test, expect} from '@playwright/test';
import {login, apiCreateRepo, apiDeleteRepo, createProject, createProjectColumn, randomString} from './utils.ts';
import type {Page} from '@playwright/test';
// ── helpers ─────────────────────────────────────────────────────────────────
/** Create a minimal project + two columns and navigate to its workflows page. */
async function setupWorkflowPage(page: Page, repoName: string) {
const user = env.GITEA_TEST_E2E_USER;
const project = await createProject(page, {owner: user, repo: repoName, title: 'WF Project'});
await Promise.all([
createProjectColumn(page.request, user, repoName, String(project.id), 'Backlog'),
createProjectColumn(page.request, user, repoName, String(project.id), 'Done'),
]);
await page.goto(`/${user}/${repoName}/projects/${project.id}/workflows`);
await expect(page.locator('.workflow-sidebar')).toBeVisible();
return project;
}
/** Click the first sidebar item and save it with the first column option. */
async function configureFirstWorkflow(page: Page) {
const firstItem = page.locator('.workflow-item').first();
await firstItem.click();
await expect(editorActionButton(page, 'Save')).toBeVisible();
// Use the "Move to column" action field specifically; the first select in the form
// is "Apply to" (issue-type filter), not the column action select.
await moveToColumnSelect(page).selectOption({index: 1});
await clickEditorAction(page, 'Save');
await expect(page.locator('.workflow-editor .workflow-status.status-enabled')).toBeVisible();
}
/** Returns the "Move to column" action select inside the workflow editor. */
function moveToColumnSelect(page: Page) {
return page.locator('.workflow-editor .field').filter({hasText: 'Move to column'}).locator('select');
}
/** Returns the "Apply to" filter select inside the workflow editor. */
function applyToSelect(page: Page) {
return page.locator('.workflow-editor .field').filter({hasText: 'Apply to'}).locator('select');
}
function editorActionButton(page: Page, text: string) {
return page.locator('.editor-actions-header button', {hasText: text});
}
async function clickEditorAction(page: Page, text: string) {
const button = editorActionButton(page, text);
await expect(button).toBeVisible();
await button.click();
// Edit/Clone use deferred emits (setTimeout), so wait for the mode switch to complete.
if (text === 'Edit' || text === 'Cancel') {
await expect(editorActionButton(page, text)).toBeHidden();
} else if (text === 'Clone') {
// Clone switches to edit mode; wait for Cancel button to confirm.
await expect(editorActionButton(page, 'Cancel')).toBeVisible();
}
}
function moveToColumnReadonlyValue(page: Page) {
return page.locator('.workflow-editor .field').filter({hasText: 'Move to column'}).locator('.readonly-value');
}
test('project workflow: configure and toggle enable/disable', async ({page}) => {
const repoName = `e2e-workflow-${randomString(8)}`;
const user = env.GITEA_TEST_E2E_USER;
test.setTimeout(30000);
await Promise.all([
login(page),
apiCreateRepo(page.request, {name: repoName}),
]);
try {
const project = await createProject(page, {owner: user, repo: repoName, title: 'Workflow Project'});
await Promise.all([
createProjectColumn(page.request, user, repoName, String(project.id), 'Backlog'),
createProjectColumn(page.request, user, repoName, String(project.id), 'Done'),
]);
await page.goto(`/${user}/${repoName}/projects/${project.id}/workflows`);
// Sidebar and first workflow item should be visible after Vue mounts
const sidebar = page.locator('.workflow-sidebar');
await expect(sidebar).toBeVisible();
const firstItem = page.locator('.workflow-item').first();
await expect(firstItem).toBeVisible();
// Click the first workflow; unconfigured events auto-enter edit mode
await firstItem.click();
const editor = page.locator('.workflow-editor');
await expect(editor).toBeVisible();
// Save button visible means we are in edit mode
const saveBtn = editorActionButton(page, 'Save');
await expect(saveBtn).toBeVisible();
// Select the "Done" column in the "Move to column" action select
await moveToColumnSelect(page).selectOption({label: 'Done'});
// Save the workflow configuration
await saveBtn.click();
// After save, view mode is active and status badge shows "Enabled"
await expect(editor.locator('.workflow-status.status-enabled')).toBeVisible();
await expect(editor.locator('.editor-actions-header button', {hasText: 'Edit'})).toBeVisible();
// Disable the workflow
await clickEditorAction(page, 'Disable');
await expect(editor.locator('.workflow-status.status-disabled')).toBeVisible();
// Re-enable the workflow
await clickEditorAction(page, 'Enable');
await expect(editor.locator('.workflow-status.status-enabled')).toBeVisible();
} finally {
await apiDeleteRepo(page.request, user, repoName);
}
});
// ── new tests ────────────────────────────────────────────────────────────────
test('project workflow: sidebar lists all 9 event types with inactive dots', async ({page}) => {
const repoName = `e2e-wf-sidebar-${randomString(8)}`;
const user = env.GITEA_TEST_E2E_USER;
test.setTimeout(30000);
await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]);
try {
const project = await createProject(page, {owner: user, repo: repoName, title: 'Sidebar Test'});
await page.goto(`/${user}/${repoName}/projects/${project.id}/workflows`);
await expect(page.locator('.workflow-sidebar')).toBeVisible();
// All 9 event types must be visible and each should start with an inactive dot.
const items = page.locator('.workflow-item');
await expect(items).toHaveCount(9);
const inactiveDots = page.locator('.workflow-item .status-inactive');
await expect(inactiveDots).toHaveCount(9);
} finally {
await apiDeleteRepo(page.request, user, repoName);
}
});
test('project workflow: status dot colour changes on configure / disable', async ({page}) => {
const repoName = `e2e-wf-dot-${randomString(8)}`;
const user = env.GITEA_TEST_E2E_USER;
test.setTimeout(30000);
await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]);
try {
await setupWorkflowPage(page, repoName);
// Before configuration: first item dot is inactive (grey).
await expect(page.locator('.workflow-item').first().locator('.status-inactive')).toBeVisible();
// Configure the first workflow.
await configureFirstWorkflow(page);
// After save the first item's dot must switch to active (green).
await expect(page.locator('.workflow-item').first().locator('.status-active')).toBeVisible();
// All other items remain inactive.
await expect(page.locator('.workflow-item .status-inactive')).toHaveCount(8);
// Disable the workflow — dot becomes disabled (red).
await clickEditorAction(page, 'Disable');
await expect(page.locator('.workflow-item').first().locator('.status-disabled')).toBeVisible();
// Re-enable — back to active (green).
await clickEditorAction(page, 'Enable');
await expect(page.locator('.workflow-item').first().locator('.status-active')).toBeVisible();
} finally {
await apiDeleteRepo(page.request, user, repoName);
}
});
test('project workflow: cancel clone removes pending clone and restores original', async ({page}) => {
const repoName = `e2e-wf-cancel-clone-${randomString(8)}`;
const user = env.GITEA_TEST_E2E_USER;
test.setTimeout(30000);
await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]);
try {
await setupWorkflowPage(page, repoName);
await configureFirstWorkflow(page);
// The configured workflow is now shown in view mode.
await expect(page.locator('.editor-actions-header button', {hasText: 'Edit'})).toBeVisible();
// Clone it — a new (10th) sidebar entry appears and we enter edit mode.
await clickEditorAction(page, 'Clone');
await expect(page.locator('.workflow-item')).toHaveCount(10);
await expect(editorActionButton(page, 'Save')).toBeVisible();
// Cancel the clone — the pending entry must be removed.
await clickEditorAction(page, 'Cancel');
await expect(page.locator('.workflow-item')).toHaveCount(9);
// The original workflow should be selected (active) and in view mode.
await expect(page.locator('.workflow-item').first()).toHaveClass(/active/);
await expect(page.locator('.editor-actions-header button', {hasText: 'Edit'})).toBeVisible();
await expect(page.locator('.editor-actions-header button', {hasText: 'Save'})).toBeHidden();
} finally {
await apiDeleteRepo(page.request, user, repoName);
}
});
test('project workflow: saving without any action shows validation error', async ({page}) => {
const repoName = `e2e-wf-validate-${randomString(8)}`;
const user = env.GITEA_TEST_E2E_USER;
test.setTimeout(30000);
await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]);
try {
await setupWorkflowPage(page, repoName);
// Click an unconfigured workflow — it auto-enters edit mode.
await page.locator('.workflow-item').first().click();
await expect(page.locator('.editor-actions-header button', {hasText: 'Save'})).toBeVisible();
// Deliberately leave all selects at their default empty values, then save.
await clickEditorAction(page, 'Save');
// A Toastify error notification must appear containing the validation text.
// Note: .toast-body always has a hidden "1" span prefix, so we use a regex.
await expect(page.locator('.toastify.on .toast-body')).toContainText(/at least one action/i);
// The editor must remain in edit mode (not have been navigated away).
await expect(page.locator('.editor-actions-header button', {hasText: 'Save'})).toBeVisible();
} finally {
await apiDeleteRepo(page.request, user, repoName);
}
});
test('project workflow: "Apply to" filter persists across save and re-open', async ({page}) => {
const repoName = `e2e-wf-filter-${randomString(8)}`;
const user = env.GITEA_TEST_E2E_USER;
test.setTimeout(30000);
await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]);
try {
await setupWorkflowPage(page, repoName);
// "Item opened" (first item) supports the issue-type filter and column action.
await page.locator('.workflow-item').first().click();
await expect(page.locator('.editor-actions-header button', {hasText: 'Save'})).toBeVisible();
// Set "Apply to" → "Issues only".
await applyToSelect(page).selectOption({label: 'Issues only'});
// Set the required column action too.
await moveToColumnSelect(page).selectOption({index: 1});
await clickEditorAction(page, 'Save');
await expect(page.locator('.workflow-editor .workflow-status.status-enabled')).toBeVisible();
// Re-open in edit mode and verify the saved "Apply to" value is restored.
await clickEditorAction(page, 'Edit');
await expect(applyToSelect(page)).toHaveValue('issue');
} finally {
await apiDeleteRepo(page.request, user, repoName);
}
});
test('project workflow: editing a saved workflow updates its configuration', async ({page}) => {
const repoName = `e2e-wf-edit-${randomString(8)}`;
const user = env.GITEA_TEST_E2E_USER;
test.setTimeout(30000);
await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]);
try {
await setupWorkflowPage(page, repoName);
await configureFirstWorkflow(page); // saves with 'Backlog' (index 1)
// Edit the workflow and switch to the second column ('Done', index 2).
await clickEditorAction(page, 'Edit');
await moveToColumnSelect(page).selectOption({index: 2});
await clickEditorAction(page, 'Save');
// After save, view mode should reflect the updated column title.
await expect(page.locator('.workflow-editor .workflow-status.status-enabled')).toBeVisible();
await expect(moveToColumnReadonlyValue(page)).toContainText('Done');
} finally {
await apiDeleteRepo(page.request, user, repoName);
}
});
test('project workflow: direct URL navigation selects the correct workflow', async ({page}) => {
const repoName = `e2e-wf-url-${randomString(8)}`;
const user = env.GITEA_TEST_E2E_USER;
test.setTimeout(30000);
await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]);
try {
const project = await setupWorkflowPage(page, repoName);
await configureFirstWorkflow(page);
// Capture the URL that was set after save (contains the numeric workflow ID).
const savedUrl = page.url();
// Navigate away then back via the saved URL.
await page.goto(`/${user}/${repoName}/projects/${project.id}/workflows`);
await expect(page.locator('.workflow-sidebar')).toBeVisible();
await page.goto(savedUrl);
// The saved workflow should be pre-selected and in view mode.
await expect(page.locator('.workflow-item.active')).toBeVisible();
await expect(page.locator('.workflow-editor .workflow-status.status-enabled')).toBeVisible();
await expect(page.locator('.editor-actions-header button', {hasText: 'Edit'})).toBeVisible();
} finally {
await apiDeleteRepo(page.request, user, repoName);
}
});
test('project workflow: clone and delete', async ({page}) => {
const repoName = `e2e-workflow-clone-${randomString(8)}`;
const user = env.GITEA_TEST_E2E_USER;
test.setTimeout(30000);
await Promise.all([
login(page),
apiCreateRepo(page.request, {name: repoName}),
]);
try {
const project = await createProject(page, {owner: user, repo: repoName, title: 'Clone Workflow Project'});
await createProjectColumn(page.request, user, repoName, String(project.id), 'In Progress');
await page.goto(`/${user}/${repoName}/projects/${project.id}/workflows`);
const firstItem = page.locator('.workflow-item').first();
await expect(firstItem).toBeVisible();
await firstItem.click();
const editor = page.locator('.workflow-editor');
const saveBtn = editorActionButton(page, 'Save');
await expect(saveBtn).toBeVisible();
// Configure the workflow: pick a column and save
await moveToColumnSelect(page).selectOption({label: 'In Progress'});
await saveBtn.click();
await expect(editor.locator('.workflow-status.status-enabled')).toBeVisible();
// Verify the sidebar now shows all 9 event types
await expect(page.locator('.workflow-item')).toHaveCount(9);
// Clone the configured workflow
await clickEditorAction(page, 'Clone');
// A new entry for the same event type appears in the sidebar
await expect(page.locator('.workflow-item')).toHaveCount(10);
// Save the clone (pre-filled from the original)
await clickEditorAction(page, 'Save');
await expect(editor.locator('.workflow-status.status-enabled')).toBeVisible();
// Delete the cloned workflow
await clickEditorAction(page, 'Edit');
await clickEditorAction(page, 'Delete');
// Confirm deletion in the modal
await page.locator('.ui.g-modal-confirm .ui.red.ok.button').click();
// Back to 9 items
await expect(page.locator('.workflow-item')).toHaveCount(9);
} finally {
await apiDeleteRepo(page.request, user, repoName);
}
});