316 lines
13 KiB
TypeScript
316 lines
13 KiB
TypeScript
import {test, expect} from '../../support/fixtures'
|
|
import {BucketFactory} from '../../factories/bucket'
|
|
import {ProjectFactory} from '../../factories/project'
|
|
import {TaskFactory} from '../../factories/task'
|
|
import {ProjectViewFactory} from '../../factories/project_view'
|
|
import {TaskBucketFactory} from '../../factories/task_buckets'
|
|
import {createTasksWithPriorities, createTasksWithSearch} from '../../support/filterTestHelpers'
|
|
|
|
async function createSingleTaskInBucket(count = 1, attrs = {}) {
|
|
const projects = await ProjectFactory.create(1)
|
|
const views = await ProjectViewFactory.create(1, {
|
|
id: 1,
|
|
project_id: projects[0].id,
|
|
view_kind: 3,
|
|
bucket_configuration_mode: 1,
|
|
})
|
|
const buckets = await BucketFactory.create(2, {
|
|
project_view_id: views[0].id,
|
|
})
|
|
const tasks = await TaskFactory.create(count, {
|
|
project_id: projects[0].id,
|
|
...attrs,
|
|
})
|
|
await TaskBucketFactory.create(1, {
|
|
task_id: tasks[0].id,
|
|
bucket_id: buckets[0].id,
|
|
project_view_id: views[0].id,
|
|
})
|
|
return {
|
|
task: tasks[0],
|
|
view: views[0],
|
|
project: projects[0],
|
|
}
|
|
}
|
|
|
|
async function createTaskWithBuckets(buckets, count = 1) {
|
|
const data = await TaskFactory.create(count, {
|
|
project_id: 1,
|
|
})
|
|
await TaskBucketFactory.truncate()
|
|
for (const t of data) {
|
|
await TaskBucketFactory.create(1, {
|
|
task_id: t.id,
|
|
bucket_id: buckets[0].id,
|
|
project_view_id: buckets[0].project_view_id,
|
|
}, false)
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
test.describe('Project View Kanban', () => {
|
|
let buckets
|
|
|
|
test.beforeEach(async ({authenticatedPage: page}) => {
|
|
const projects = await ProjectFactory.create(1)
|
|
await ProjectViewFactory.create(1, {
|
|
id: 4,
|
|
project_id: projects[0].id,
|
|
view_kind: 3,
|
|
bucket_configuration_mode: 1,
|
|
})
|
|
buckets = await BucketFactory.create(2, {
|
|
project_view_id: 4,
|
|
})
|
|
})
|
|
|
|
test('Shows all buckets with their tasks', async ({authenticatedPage: page}) => {
|
|
const data = await createTaskWithBuckets(buckets, 10)
|
|
await page.goto('/projects/1/4')
|
|
|
|
await expect(page.locator('.kanban .bucket .title').filter({hasText: buckets[0].title})).toBeVisible()
|
|
await expect(page.locator('.kanban .bucket .title').filter({hasText: buckets[1].title})).toBeVisible()
|
|
await expect(page.locator('.kanban .bucket').first()).toContainText(data[0].title)
|
|
})
|
|
|
|
test('Can add a new task to a bucket', async ({authenticatedPage: page}) => {
|
|
await createTaskWithBuckets(buckets, 2)
|
|
await page.goto('/projects/1/4')
|
|
|
|
await page.locator('.kanban .bucket').filter({hasText: buckets[0].title}).locator('.bucket-footer .button').filter({hasText: 'Add another task'}).click()
|
|
await page.locator('.kanban .bucket').filter({hasText: buckets[0].title}).locator('.bucket-footer .field .control input.input').fill('New Task')
|
|
await page.locator('.kanban .bucket').filter({hasText: buckets[0].title}).locator('.bucket-footer .field .control input.input').press('Enter')
|
|
|
|
await expect(page.locator('.kanban .bucket').first()).toContainText('New Task')
|
|
})
|
|
|
|
test('Can create a new bucket', async ({authenticatedPage: page}) => {
|
|
await page.goto('/projects/1/4')
|
|
|
|
await page.locator('.kanban .bucket.new-bucket .button').click()
|
|
await page.locator('.kanban .bucket.new-bucket input.input').fill('New Bucket')
|
|
await page.locator('.kanban .bucket.new-bucket input.input').press('Enter')
|
|
|
|
await expect(page.locator('.kanban .bucket .title').filter({hasText: 'New Bucket'})).toBeVisible()
|
|
})
|
|
|
|
test('Can set a bucket limit', async ({authenticatedPage: page}) => {
|
|
await page.goto('/projects/1/4')
|
|
|
|
const bucketDropdown = page.locator('.kanban .bucket .bucket-header .dropdown.options').first()
|
|
await bucketDropdown.locator('.dropdown-trigger').click()
|
|
await bucketDropdown.locator('.dropdown-menu .dropdown-item').filter({hasText: 'Limit: Not Set'}).click()
|
|
await bucketDropdown.locator('.dropdown-menu .field input.input').fill('3')
|
|
await bucketDropdown.locator('.dropdown-menu .field .control .button').click()
|
|
|
|
// Wait for the limit to be saved - the dropdown closes and limit is shown
|
|
await expect(page.locator('.global-notification')).toContainText('Success')
|
|
await expect(page.locator('.kanban .bucket .bucket-header span.limit').first()).toBeVisible()
|
|
await expect(page.locator('.kanban .bucket .bucket-header span.limit').first()).toContainText('/3')
|
|
})
|
|
|
|
test('Can rename a bucket', async ({authenticatedPage: page}) => {
|
|
await page.goto('/projects/1/4')
|
|
|
|
const titleElement = page.locator('.kanban .bucket .bucket-header .title').first()
|
|
await titleElement.click()
|
|
await titleElement.fill('New Bucket Title')
|
|
await titleElement.press('Enter')
|
|
await expect(titleElement).toContainText('New Bucket Title')
|
|
})
|
|
|
|
test('Can delete a bucket', async ({authenticatedPage: page}) => {
|
|
await page.goto('/projects/1/4')
|
|
|
|
await page.locator('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger').first().click()
|
|
await page.locator('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item').filter({hasText: 'Delete'}).click()
|
|
await expect(page.locator('.modal-mask .modal-container .modal-content .modal-header')).toContainText('Delete the bucket')
|
|
await page.locator('.modal-mask .modal-container .modal-content .actions .button').filter({hasText: 'Do it!'}).click()
|
|
|
|
await expect(page.locator('.kanban .bucket .title').filter({hasText: buckets[0].title})).not.toBeVisible()
|
|
await expect(page.locator('.kanban .bucket .title').filter({hasText: buckets[1].title})).toBeVisible()
|
|
})
|
|
|
|
test('Can drag tasks around', async ({authenticatedPage: page}) => {
|
|
const tasks = await createTaskWithBuckets(buckets, 2)
|
|
await page.goto('/projects/1/4')
|
|
|
|
const sourceTask = page.locator('.kanban .bucket .tasks .task').filter({hasText: tasks[0].title}).first()
|
|
const targetBucket = page.locator('.kanban .bucket:nth-child(2) .tasks')
|
|
await sourceTask.dragTo(targetBucket)
|
|
|
|
await expect(page.locator('.kanban .bucket:nth-child(2) .tasks')).toContainText(tasks[0].title)
|
|
await expect(page.locator('.kanban .bucket:nth-child(1) .tasks')).not.toContainText(tasks[0].title)
|
|
})
|
|
|
|
test('Should navigate to the task when the task card is clicked', async ({authenticatedPage: page}) => {
|
|
const tasks = await createTaskWithBuckets(buckets, 5)
|
|
await page.goto('/projects/1/4')
|
|
|
|
await expect(page.locator('.kanban .bucket .tasks .task').filter({hasText: tasks[0].title})).toBeVisible()
|
|
await page.locator('.kanban .bucket .tasks .task').filter({hasText: tasks[0].title}).click()
|
|
|
|
await expect(page).toHaveURL(new RegExp(`/tasks/${tasks[0].id}`), {timeout: 1000})
|
|
})
|
|
|
|
test('Should remove a task from the kanban board when moving it to another project', async ({authenticatedPage: page}) => {
|
|
const projects = await ProjectFactory.create(2)
|
|
const views = await ProjectViewFactory.create(2, {
|
|
project_id: '{increment}',
|
|
view_kind: 3,
|
|
bucket_configuration_mode: 1,
|
|
})
|
|
await BucketFactory.create(2)
|
|
const tasks = await TaskFactory.create(5, {
|
|
id: '{increment}',
|
|
project_id: 1,
|
|
})
|
|
await TaskBucketFactory.create(5, {
|
|
project_view_id: 1,
|
|
})
|
|
const task = tasks[0]
|
|
await page.goto('/projects/1/' + views[0].id)
|
|
|
|
await expect(page.locator('.kanban .bucket .tasks .task').filter({hasText: task.title})).toBeVisible()
|
|
await page.locator('.kanban .bucket .tasks .task').filter({hasText: task.title}).click()
|
|
|
|
await page.locator('.task-view .action-buttons .button', {timeout: 3000}).filter({hasText: /^Move$/}).click()
|
|
const multiselectInput = page.locator('.task-view .content.details .field .multiselect.control .input-wrapper input')
|
|
await expect(multiselectInput).toBeVisible({timeout: 5000})
|
|
await multiselectInput.click()
|
|
await multiselectInput.pressSequentially(projects[1].title)
|
|
// Wait for search results to appear before clicking
|
|
const searchResults = page.locator('.task-view .content.details .field .multiselect.control .search-results')
|
|
await searchResults.waitFor({state: 'visible'})
|
|
await searchResults.locator('> *').first().click()
|
|
|
|
await expect(page.locator('.global-notification')).toContainText('Success', {timeout: 1000})
|
|
await page.goBack()
|
|
const bucketCount = await page.locator('.kanban .bucket').count()
|
|
for (let i = 0; i < bucketCount; i++) {
|
|
await expect(page.locator('.kanban .bucket').nth(i)).not.toContainText(task.title)
|
|
}
|
|
})
|
|
|
|
test('Shows a button to filter the kanban board', async ({authenticatedPage: page}) => {
|
|
await page.goto('/projects/1/4')
|
|
|
|
await expect(page.locator('.project-kanban .filter-container .base-button')).toBeVisible()
|
|
})
|
|
|
|
test('Should remove a task from the board when deleting it', async ({authenticatedPage: page}) => {
|
|
const {task, view} = await createSingleTaskInBucket(5)
|
|
await page.goto(`/projects/1/${view.id}`)
|
|
|
|
await expect(page.locator('.kanban .bucket .tasks .task').filter({hasText: task.title})).toBeVisible()
|
|
await page.locator('.kanban .bucket .tasks .task').filter({hasText: task.title}).click()
|
|
await expect(page.locator('.task-view .action-buttons .button').filter({hasText: 'Delete'})).toBeVisible()
|
|
await page.locator('.task-view .action-buttons .button').filter({hasText: 'Delete'}).click()
|
|
await expect(page.locator('.modal-mask .modal-container .modal-content .modal-header')).toContainText('Delete this task')
|
|
await page.locator('.modal-mask .modal-container .modal-content .actions .button').filter({hasText: 'Do it!'}).click()
|
|
|
|
await expect(page.locator('.global-notification')).toContainText('Success')
|
|
|
|
await page.goBack()
|
|
const bucketCount = await page.locator('.kanban .bucket').count()
|
|
for (let i = 0; i < bucketCount; i++) {
|
|
await expect(page.locator('.kanban .bucket').nth(i)).not.toContainText(task.title)
|
|
}
|
|
})
|
|
|
|
test('Should show a task description icon if the task has a description', async ({authenticatedPage: page}) => {
|
|
const {task, view} = await createSingleTaskInBucket(1, {
|
|
description: 'Lorem Ipsum',
|
|
})
|
|
const loadTasksPromise = page.waitForResponse(response =>
|
|
response.url().includes('/projects/1/views/') && response.url().includes('/tasks'),
|
|
)
|
|
|
|
await page.goto(`/projects/${task.project_id}/${view.id}`)
|
|
await loadTasksPromise
|
|
|
|
await expect(page.locator('.bucket .tasks .task .footer .icon svg')).toBeVisible()
|
|
})
|
|
|
|
test('Should not show a task description icon if the task has an empty description', async ({authenticatedPage: page}) => {
|
|
const {task, view} = await createSingleTaskInBucket(1, {
|
|
description: '',
|
|
})
|
|
const loadTasksPromise = page.waitForResponse(response =>
|
|
response.url().includes('/projects/1/views/') && response.url().includes('/tasks'),
|
|
)
|
|
|
|
await page.goto(`/projects/${task.project_id}/${view.id}`)
|
|
await loadTasksPromise
|
|
|
|
await expect(page.locator('.bucket .tasks .task .footer .icon svg')).not.toBeVisible()
|
|
})
|
|
|
|
test('Should not show a task description icon if the task has a description containing only an empty p tag', async ({authenticatedPage: page}) => {
|
|
const {task, view} = await createSingleTaskInBucket(1, {
|
|
description: '<p></p>',
|
|
})
|
|
const loadTasksPromise = page.waitForResponse(response =>
|
|
response.url().includes('/projects/1/views/') && response.url().includes('/tasks'),
|
|
)
|
|
|
|
await page.goto(`/projects/${task.project_id}/${view.id}`)
|
|
await loadTasksPromise
|
|
|
|
await expect(page.locator('.bucket .tasks .task .footer .icon svg')).not.toBeVisible()
|
|
})
|
|
|
|
test('Should respect filter query parameter from URL', async ({authenticatedPage: page}) => {
|
|
// Create buckets first
|
|
const projects = await ProjectFactory.create(1)
|
|
const views = await ProjectViewFactory.create(1, {
|
|
id: 4,
|
|
project_id: 1,
|
|
view_kind: 3,
|
|
})
|
|
const buckets = await BucketFactory.create(2, {
|
|
project_view_id: 4,
|
|
})
|
|
|
|
const {highPriorityTasks, lowPriorityTasks} = await createTasksWithPriorities(buckets)
|
|
|
|
await page.goto('/projects/1/4?filter=priority%20>=%204')
|
|
|
|
await expect(page).toHaveURL(/filter=priority/)
|
|
|
|
// Wait for tasks to load and verify high priority tasks are visible
|
|
await expect(page.locator('.kanban')).toContainText(highPriorityTasks[0].title, {timeout: 10000})
|
|
await expect(page.locator('.kanban')).toContainText(highPriorityTasks[1].title)
|
|
|
|
// Verify low priority tasks are not visible
|
|
await expect(page.locator('.kanban')).not.toContainText(lowPriorityTasks[0].title)
|
|
await expect(page.locator('.kanban')).not.toContainText(lowPriorityTasks[1].title)
|
|
})
|
|
|
|
test('Should respect search query parameter from URL', async ({authenticatedPage: page}) => {
|
|
// Create buckets first
|
|
const projects = await ProjectFactory.create(1)
|
|
const views = await ProjectViewFactory.create(1, {
|
|
id: 4,
|
|
project_id: 1,
|
|
view_kind: 3,
|
|
})
|
|
const buckets = await BucketFactory.create(2, {
|
|
project_view_id: 4,
|
|
})
|
|
|
|
const {searchableTask} = await createTasksWithSearch(buckets)
|
|
|
|
await page.goto('/projects/1/4?s=meeting')
|
|
|
|
await expect(page).toHaveURL(/s=meeting/)
|
|
|
|
// Wait for search results to load and verify searchable task is visible
|
|
await expect(page.locator('.kanban')).toContainText(searchableTask.title, {timeout: 10000})
|
|
|
|
// Verify only one task is shown (the search result) - count task headings
|
|
await expect(page.locator('main h2')).toHaveCount(1)
|
|
})
|
|
})
|