Fix test failures and workload/websocket behavior
This commit is contained in:
252
frontend/e2e/admin-flow.spec.ts
Normal file
252
frontend/e2e/admin-flow.spec.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { expect, test, Page } from 'playwright/test'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const runId = new Date().toISOString().replace(/[-:.TZ]/g, '')
|
||||
const spaceName = `E2E Space ${runId}`
|
||||
const projectName = `E2E Project ${runId}`
|
||||
const taskOneTitle = `E2E Task One ${runId}`
|
||||
const taskTwoTitle = `E2E Task Two ${runId}`
|
||||
const subtaskTitle = `E2E Subtask ${runId}`
|
||||
const commentText = `E2E Comment ${runId}`
|
||||
const customNumberField = `sp_${runId}`
|
||||
const customFormulaField = `double_sp_${runId}`
|
||||
const attachmentPath = path.join(__dirname, 'fixtures', 'attachment.txt')
|
||||
|
||||
const formatDateInput = (date: Date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
const setEnglishLocale = async (page: Page) => {
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('i18nextLng', 'en')
|
||||
})
|
||||
}
|
||||
|
||||
const openProjectFromSpaces = async (page: Page) => {
|
||||
await page.goto('/spaces')
|
||||
await expect(page.getByRole('heading', { name: 'Spaces' })).toBeVisible()
|
||||
await page.getByRole('button', { name: `Spaces: ${spaceName}` }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Projects' })).toBeVisible()
|
||||
await page.getByRole('button', { name: `Projects: ${projectName}` }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Tasks' })).toBeVisible()
|
||||
}
|
||||
|
||||
test.describe.serial('admin e2e flow', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setEnglishLocale(page)
|
||||
})
|
||||
|
||||
test('login and dashboard', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await expect(page.getByRole('heading', { name: /Welcome back/i })).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: 'Quick Actions' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('create space and project', async ({ page }) => {
|
||||
await page.goto('/spaces')
|
||||
await expect(page.getByRole('heading', { name: 'Spaces' })).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: /Create Space/i }).click()
|
||||
const spaceModal = page.getByRole('dialog', { name: 'Create Space' })
|
||||
await spaceModal.getByLabel('Name').fill(spaceName)
|
||||
await spaceModal.getByLabel('Description').fill('E2E space description')
|
||||
await spaceModal.getByRole('button', { name: 'Create' }).click()
|
||||
|
||||
await expect(page.getByRole('button', { name: `Spaces: ${spaceName}` })).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: `Spaces: ${spaceName}` }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Projects' })).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: /New Project/i }).click()
|
||||
const projectModal = page.getByRole('dialog', { name: 'Create Project' })
|
||||
await projectModal.getByText('Blank Project', { exact: true }).click()
|
||||
await projectModal.locator('#project-title').fill(projectName)
|
||||
await projectModal.locator('#project-description').fill('E2E project description')
|
||||
await projectModal.getByRole('button', { name: 'Create' }).click()
|
||||
|
||||
await expect(page.getByRole('button', { name: `Projects: ${projectName}` })).toBeVisible()
|
||||
await page.getByRole('button', { name: `Projects: ${projectName}` }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Tasks' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('project settings: members and custom fields', async ({ page }) => {
|
||||
await openProjectFromSpaces(page)
|
||||
|
||||
await page.getByRole('button', { name: 'Settings' }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Project Settings' })).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Members' }).click()
|
||||
await expect(page.getByText('Member Management')).toBeVisible()
|
||||
await expect(page.getByText('User')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Custom Fields' }).click()
|
||||
await expect(page.getByRole('button', { name: /Add Field/i })).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: /Add Field/i }).click()
|
||||
const customModal = page.getByRole('dialog', { name: 'Create Field' })
|
||||
await customModal.getByPlaceholder('e.g., Story Points, Sprint Number').fill(customNumberField)
|
||||
await customModal.getByText('Number', { exact: true }).click()
|
||||
await customModal.getByRole('button', { name: 'Create Field' }).click()
|
||||
await expect(page.getByText(customNumberField)).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: /Add Field/i }).click()
|
||||
const formulaModal = page.getByRole('dialog', { name: 'Create Field' })
|
||||
await formulaModal.getByPlaceholder('e.g., Story Points, Sprint Number').fill(customFormulaField)
|
||||
await formulaModal.getByText('Formula', { exact: true }).click()
|
||||
await formulaModal.getByPlaceholder('e.g., {time_spent} / {original_estimate} * 100')
|
||||
.fill(`{${customNumberField}} * 2`)
|
||||
await formulaModal.getByRole('button', { name: 'Create Field' }).click()
|
||||
|
||||
await expect(page.getByText(customFormulaField)).toBeVisible()
|
||||
})
|
||||
|
||||
test('tasks flow: create, views, detail, attachments, comments, subtasks, dependencies', async ({ page }) => {
|
||||
await openProjectFromSpaces(page)
|
||||
|
||||
const today = new Date()
|
||||
const startDate = formatDateInput(today)
|
||||
const dueDate = formatDateInput(new Date(today.getTime() + 3 * 24 * 60 * 60 * 1000))
|
||||
|
||||
await page.getByRole('button', { name: /Create Task/i }).click()
|
||||
const createModal = page.getByRole('dialog', { name: 'Create Task' })
|
||||
await createModal.locator('#task-title').fill(taskOneTitle)
|
||||
await createModal.locator('#task-description').fill('E2E task description')
|
||||
await createModal.locator('select').first().selectOption('low')
|
||||
const dateInputs = createModal.locator('input[type="date"]')
|
||||
await dateInputs.nth(0).fill(startDate)
|
||||
await dateInputs.nth(1).fill(dueDate)
|
||||
const customFieldContainer = createModal.locator('label', { hasText: customNumberField }).locator('..')
|
||||
await customFieldContainer.locator('input[type="number"]').fill('5')
|
||||
await createModal.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(page.getByText(taskOneTitle)).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: /Create Task/i }).click()
|
||||
const createModalTwo = page.getByRole('dialog', { name: 'Create Task' })
|
||||
await createModalTwo.locator('#task-title').fill(taskTwoTitle)
|
||||
await createModalTwo.locator('#task-description').fill('E2E task description 2')
|
||||
const dateInputsTwo = createModalTwo.locator('input[type="date"]')
|
||||
await dateInputsTwo.nth(0).fill(startDate)
|
||||
await dateInputsTwo.nth(1).fill(dueDate)
|
||||
await createModalTwo.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(page.getByText(taskTwoTitle)).toBeVisible()
|
||||
await expect(page.getByText(`${customNumberField}: 5`, { exact: true })).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Kanban' }).click()
|
||||
await expect(page.getByText(taskOneTitle)).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Calendar' }).click()
|
||||
await expect(page.getByText(new RegExp(taskOneTitle))).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Gantt' }).click()
|
||||
await expect(page.getByText('Task Dependencies')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Manage Dependencies' }).first().click()
|
||||
const depsHeading = page.getByRole('heading', {
|
||||
name: /Manage Dependencies for/i,
|
||||
})
|
||||
await expect(depsHeading).toBeVisible()
|
||||
const depsDialog = depsHeading.locator('..')
|
||||
const dependencySelect = depsDialog.locator('select').first()
|
||||
await dependencySelect.selectOption({ index: 1 })
|
||||
await depsDialog.getByRole('button', { name: 'Add Dependency' }).click()
|
||||
await expect(page.locator('text=Depends on:').first()).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'List' }).click()
|
||||
await page.getByText(taskOneTitle).first().click()
|
||||
|
||||
const taskModal = page.getByRole('dialog', { name: taskOneTitle })
|
||||
await expect(taskModal.getByRole('heading', { name: taskOneTitle })).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Edit' }).click()
|
||||
const descriptionField = page.locator('label', { hasText: 'Description' }).locator('..').locator('textarea')
|
||||
await descriptionField.fill('Updated task description')
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
await expect(page.getByText('Updated task description')).toBeVisible()
|
||||
|
||||
await page.getByPlaceholder('Add a comment... Use @name to mention someone').fill(commentText)
|
||||
await page.getByRole('button', { name: 'Post Comment' }).click()
|
||||
await expect(page.getByText(commentText)).toBeVisible()
|
||||
|
||||
await page.locator('input[type="file"]').setInputFiles(attachmentPath)
|
||||
await expect(page.getByText('attachment.txt')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: /Add Subtask/i }).click()
|
||||
await page.locator('#new-subtask-title').fill(subtaskTitle)
|
||||
await page.getByRole('button', { name: 'Add' }).click()
|
||||
await expect(page.getByText(subtaskTitle)).toBeVisible()
|
||||
|
||||
await page.getByLabel('Close').click()
|
||||
})
|
||||
|
||||
test('notifications and my settings', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.getByRole('button', { name: 'Notifications' }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Notifications' })).toBeVisible()
|
||||
|
||||
await page.goto('/my-settings')
|
||||
await expect(page.getByRole('heading', { name: 'My Settings' })).toBeVisible()
|
||||
|
||||
const capacityInput = page.locator('input[type="number"]').first()
|
||||
const currentCapacity = await capacityInput.inputValue()
|
||||
const nextCapacity = String((Number(currentCapacity) || 40) + 1)
|
||||
await capacityInput.fill(nextCapacity)
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
|
||||
await capacityInput.fill(currentCapacity || '40')
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
|
||||
const weeklyCard = page.getByRole('heading', { name: 'Weekly Report' }).locator('..')
|
||||
const subscriptionToggle = weeklyCard.locator('input[type="checkbox"]')
|
||||
await expect(subscriptionToggle).toBeEnabled()
|
||||
const isChecked = await subscriptionToggle.isChecked()
|
||||
await subscriptionToggle.click()
|
||||
if (isChecked) {
|
||||
await expect(subscriptionToggle).not.toBeChecked()
|
||||
} else {
|
||||
await expect(subscriptionToggle).toBeChecked()
|
||||
}
|
||||
await subscriptionToggle.click()
|
||||
})
|
||||
|
||||
test('workload, project health, audit pages', async ({ page }) => {
|
||||
await page.goto('/workload')
|
||||
await expect(page.getByRole('heading', { name: 'Workload' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Next week' }).click()
|
||||
|
||||
await page.goto('/project-health')
|
||||
await expect(page.getByRole('heading', { name: 'Project Health Dashboard' })).toBeVisible()
|
||||
await page.getByLabel('Sort by:').selectOption('name')
|
||||
|
||||
await page.goto('/audit')
|
||||
await expect(page.getByRole('heading', { name: 'Audit Log' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('cleanup: delete project and space', async ({ page }) => {
|
||||
await page.goto('/spaces')
|
||||
await expect(page.getByRole('heading', { name: 'Spaces' })).toBeVisible()
|
||||
await page.getByRole('button', { name: `Spaces: ${spaceName}` }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Projects' })).toBeVisible()
|
||||
|
||||
const projectCard = page.getByRole('button', { name: `Projects: ${projectName}` })
|
||||
await projectCard.getByRole('button', { name: 'Delete Project' }).click()
|
||||
const deleteProjectModal = page.getByRole('dialog', { name: 'Delete Project' })
|
||||
await deleteProjectModal.getByRole('button', { name: 'Delete' }).click()
|
||||
await expect(page.getByRole('button', { name: `Projects: ${projectName}` })).toHaveCount(0)
|
||||
|
||||
await page.goto('/spaces')
|
||||
await expect(page.getByRole('heading', { name: 'Spaces' })).toBeVisible()
|
||||
|
||||
const spaceCard = page.getByRole('button', { name: `Spaces: ${spaceName}` })
|
||||
await spaceCard.getByRole('button', { name: 'Delete Space' }).click()
|
||||
const deleteSpaceModal = page.getByRole('dialog', { name: 'Delete Space' })
|
||||
await deleteSpaceModal.getByRole('button', { name: 'Delete' }).click()
|
||||
await expect(page.getByRole('button', { name: `Spaces: ${spaceName}` })).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
1
frontend/e2e/fixtures/attachment.txt
Normal file
1
frontend/e2e/fixtures/attachment.txt
Normal file
@@ -0,0 +1 @@
|
||||
Playwright attachment fixture for Project Control e2e tests.
|
||||
43
frontend/e2e/global-setup.ts
Normal file
43
frontend/e2e/global-setup.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { chromium, FullConfig } from 'playwright/test'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const storageStatePath = path.join(__dirname, '.auth', 'admin.json')
|
||||
const baseUrl = process.env.E2E_BASE_URL || 'http://localhost:3000'
|
||||
|
||||
const requireEnv = (name: string) => {
|
||||
const value = process.env[name]
|
||||
if (!value) {
|
||||
throw new Error(`Missing required env var: ${name}`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
async function globalSetup(_config: FullConfig) {
|
||||
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true })
|
||||
|
||||
const browser = await chromium.launch()
|
||||
const context = await browser.newContext()
|
||||
const page = await context.newPage()
|
||||
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('i18nextLng', 'en')
|
||||
})
|
||||
|
||||
const email = requireEnv('E2E_EMAIL')
|
||||
const password = requireEnv('E2E_PASSWORD')
|
||||
|
||||
await page.goto(`${baseUrl}/login`)
|
||||
await page.getByLabel('Email').fill(email)
|
||||
await page.getByLabel('Password').fill(password)
|
||||
await page.getByRole('button', { name: 'Sign in' }).click()
|
||||
await page.getByRole('button', { name: 'Logout' }).waitFor()
|
||||
|
||||
await context.storageState({ path: storageStatePath })
|
||||
await browser.close()
|
||||
}
|
||||
|
||||
export default globalSetup
|
||||
6
frontend/e2e/smoke.spec.ts
Normal file
6
frontend/e2e/smoke.spec.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { expect, test } from 'playwright/test';
|
||||
|
||||
test('smoke: basic rendering works', async ({ page }) => {
|
||||
await page.setContent('<main><h1>Smoke</h1><p>Playwright ready</p></main>');
|
||||
await expect(page.getByRole('heading', { name: 'Smoke' })).toBeVisible();
|
||||
});
|
||||
Reference in New Issue
Block a user