chore(tests): remove Cypress, use Playwright exclusively (#1976)
- Removes Cypress test framework entirely, using only Playwright for E2E tests - All Cypress tests were already covered by Playwright; added 2 missing tests for URL filter/search parameters - Removes ~2000 lines of Cypress code and configuration - Updated ESLint and Stylelint configurations to reflect testing changes 🐰 Farewell to Cypress, dear and bright, We hop to Playwright's testing light, Factories cleared, the config gone, Our tests now march in Playwright's song! ~✨ The Testing Rabbit
This commit is contained in:
parent
62f291c9a8
commit
ad1a5f9b5c
|
|
@ -422,66 +422,3 @@ jobs:
|
|||
name: playwright-test-results-${{ matrix.shard }}
|
||||
path: frontend/test-results/
|
||||
retention-days: 30
|
||||
|
||||
test-frontend-e2e-cypress:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- api-build
|
||||
- frontend-build
|
||||
services:
|
||||
dex:
|
||||
image: ghcr.io/go-vikunja/dex-testing:main@sha256:d401c06a9f8fd36ece446a07499b827232af7f21eb36872a76c9eac4d0c77bab
|
||||
ports:
|
||||
- 5556:5556
|
||||
container:
|
||||
image: cypress/browsers:latest@sha256:c03803eed8a1c80a1cfe38672e8c6f661439fcff5cfa2c5ed424ffa502b0b0a1
|
||||
options: --user 1001
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Download Vikunja Binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: vikunja_bin
|
||||
- uses: ./.github/actions/setup-frontend
|
||||
with:
|
||||
install-e2e-binaries: true
|
||||
- name: Download Frontend
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: frontend_dist
|
||||
path: ./frontend/dist
|
||||
- name: Inject testing flag into index.html
|
||||
run: |
|
||||
sed -i 's/<head>/<head><script>window.TESTING=true;<\/script>/' ./frontend/dist/index.html
|
||||
- run: chmod +x ./vikunja
|
||||
- uses: cypress-io/github-action@v6
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CYPRESS_API_URL: http://127.0.0.1:3456/api/v1
|
||||
CYPRESS_TEST_SECRET: averyLongSecretToSe33dtheDB
|
||||
CYPRESS_DEFAULT_COMMAND_TIMEOUT: 60000
|
||||
CYPRESS_CI_BUILD_ID: "${{ github.workflow }}-${{ github.run_id }}-${{ github.run_attempt }}" # see https://github.com/cypress-io/github-action/issues/431
|
||||
VIKUNJA_SERVICE_TESTINGTOKEN: averyLongSecretToSe33dtheDB
|
||||
VIKUNJA_LOG_LEVEL: DEBUG
|
||||
VIKUNJA_CORS_ENABLE: 1
|
||||
VIKUNJA_SERVICE_PUBLICURL: http://127.0.0.1:3456
|
||||
VIKUNJA_DATABASE_PATH: memory
|
||||
VIKUNJA_DATABASE_TYPE: sqlite
|
||||
VIKUNJA_RATELIMIT_NOAUTHLIMIT: 1000
|
||||
VIKUNJA_AUTH_OPENID_ENABLED: 1
|
||||
VIKUNJA_AUTH_OPENID_PROVIDERS_DEX_NAME: Dex
|
||||
VIKUNJA_AUTH_OPENID_PROVIDERS_DEX_AUTHURL: http://dex:5556
|
||||
VIKUNJA_AUTH_OPENID_PROVIDERS_DEX_CLIENTID: vikunja
|
||||
VIKUNJA_AUTH_OPENID_PROVIDERS_DEX_CLIENTSECRET: secret
|
||||
with:
|
||||
install: false
|
||||
working-directory: frontend
|
||||
browser: chrome
|
||||
record: false
|
||||
parallel: false
|
||||
start: |
|
||||
pnpm run preview:vikunja
|
||||
pnpm run preview
|
||||
wait-on: http://127.0.0.1:4173,http://127.0.0.1:3456/api/v1/info
|
||||
wait-on-timeout: 10
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ coverage
|
|||
.vite/
|
||||
|
||||
# Test files
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,2 @@
|
|||
# https://github.com/pnpm/pnpm/issues/8378#issuecomment-2636152421
|
||||
public-hoist-pattern[]=*eslint*
|
||||
|
||||
# Make sure to install Cypress binary
|
||||
# https://github.com/cypress-io/github-action/blob/108b8684ae52e735ff7891524cbffbcd4be5b19f/README.md#pnpm
|
||||
side-effects-cache=false
|
||||
|
|
|
|||
|
|
@ -84,7 +84,6 @@
|
|||
"ignoreFiles": [
|
||||
"node_modules/**/*",
|
||||
"dist/**/*",
|
||||
"cypress/**/*",
|
||||
"**/*.js",
|
||||
"**/*.ts"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
import {defineConfig} from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
env: {
|
||||
API_URL: 'http://localhost:3456/api/v1',
|
||||
TEST_SECRET: 'averyLongSecretToSe33dtheDB',
|
||||
},
|
||||
video: false,
|
||||
retries: {
|
||||
runMode: 2,
|
||||
},
|
||||
projectId: '181c7x',
|
||||
e2e: {
|
||||
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
|
||||
baseUrl: 'http://127.0.0.1:4173',
|
||||
experimentalRunAllSpecs: true,
|
||||
// testIsolation: false,
|
||||
},
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'vue',
|
||||
bundler: 'vite',
|
||||
},
|
||||
},
|
||||
viewportWidth: 1600,
|
||||
viewportHeight: 900,
|
||||
experimentalMemoryManagement: true,
|
||||
})
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
# Frontend Testing With Cypress
|
||||
|
||||
## Setup
|
||||
|
||||
* Enable the [seeder api endpoint](https://vikunja.io/docs/config-options/#testingtoken). You'll then need to add the testingtoken in `cypress.json` or set the `CYPRESS_TEST_SECRET` environment variable.
|
||||
* Basic configuration happens in the `cypress.json` file
|
||||
* Overridable with [env](https://docs.cypress.io/guides/guides/environment-variables.html#Option-3-CYPRESS)
|
||||
* Override base url with `CYPRESS_BASE_URL`
|
||||
|
||||
## Fixtures
|
||||
|
||||
We're using the [test endpoint](https://vikunja.io/docs/config-options/#testingtoken) of the vikunja api to
|
||||
seed the database with test data before running the tests.
|
||||
This ensures better reproducibility of tests.
|
||||
|
||||
## Running The Tests Locally
|
||||
|
||||
### Using Docker
|
||||
|
||||
The easiest way to run all frontend tests locally is by using the `docker-compose` file in this repository.
|
||||
It uses the same configuration as the CI.
|
||||
|
||||
To use it, run
|
||||
|
||||
```shell
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Then, once all containers are started, run
|
||||
|
||||
```shell
|
||||
docker-compose run cypress bash
|
||||
```
|
||||
|
||||
to get a shell inside the cypress container.
|
||||
In that shell you can then execute the tests with
|
||||
|
||||
```shell
|
||||
pnpm run test:e2e
|
||||
```
|
||||
|
||||
### Using The Cypress Dashboard
|
||||
|
||||
To open the Cypress Dashboard and run tests from there, run
|
||||
|
||||
```shell
|
||||
pnpm run test:e2e:dev
|
||||
```
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
services:
|
||||
dex:
|
||||
image: ghcr.io/go-vikunja/dex-testing:main@sha256:d401c06a9f8fd36ece446a07499b827232af7f21eb36872a76c9eac4d0c77bab
|
||||
ports:
|
||||
- 5556:5556
|
||||
cypress:
|
||||
image: cypress/browsers:latest@sha256:c03803eed8a1c80a1cfe38672e8c6f661439fcff5cfa2c5ed424ffa502b0b0a1
|
||||
volumes:
|
||||
- ..:/project
|
||||
- $HOME/.cache:/home/node/.cache/
|
||||
user: node
|
||||
working_dir: /project
|
||||
environment:
|
||||
CYPRESS_API_URL: http://api:3456/api/v1
|
||||
CYPRESS_TEST_SECRET: averyLongSecretToSe33dtheDB
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import {ProjectFactory} from '../../factories/project'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {ProjectViewFactory} from "../../factories/project_view";
|
||||
|
||||
export function createDefaultViews(projectId: number, startViewId = 1, truncate: boolean = true) {
|
||||
if (truncate) {
|
||||
ProjectViewFactory.truncate()
|
||||
}
|
||||
const list = ProjectViewFactory.create(1, {
|
||||
id: startViewId,
|
||||
project_id: projectId,
|
||||
view_kind: 0,
|
||||
}, false)
|
||||
const gantt = ProjectViewFactory.create(1, {
|
||||
id: startViewId + 1,
|
||||
project_id: projectId,
|
||||
view_kind: 1,
|
||||
}, false)
|
||||
const table = ProjectViewFactory.create(1, {
|
||||
id: startViewId + 2,
|
||||
project_id: projectId,
|
||||
view_kind: 2,
|
||||
}, false)
|
||||
const kanban = ProjectViewFactory.create(1, {
|
||||
id: startViewId + 3,
|
||||
project_id: projectId,
|
||||
view_kind: 3,
|
||||
bucket_configuration_mode: 1,
|
||||
}, false)
|
||||
|
||||
return [
|
||||
list[0],
|
||||
gantt[0],
|
||||
table[0],
|
||||
kanban[0],
|
||||
]
|
||||
}
|
||||
|
||||
export function createProjects(count: number = 1) {
|
||||
const projects = ProjectFactory.create(count, {
|
||||
title: i => count === 1 ? 'First Project' : `Project ${i + 1}`,
|
||||
})
|
||||
|
||||
TaskFactory.truncate()
|
||||
ProjectViewFactory.truncate()
|
||||
|
||||
for (let i = 0; i < projects.length; i++) {
|
||||
const views = createDefaultViews(projects[i].id, i * 4 + 1, false)
|
||||
projects[i].views = views
|
||||
}
|
||||
|
||||
return projects
|
||||
}
|
||||
|
||||
export function prepareProjects(setProjects = (...args: any[]) => {
|
||||
}) {
|
||||
beforeEach(() => {
|
||||
const projects = createProjects()
|
||||
setProjects(projects)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {UserProjectFactory} from '../../factories/users_project'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {TaskRelationFactory} from '../../factories/task_relation'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
import {ProjectFactory} from '../../factories/project'
|
||||
import {prepareProjects, createProjects} from './prepareProjects'
|
||||
import {BucketFactory} from '../../factories/bucket'
|
||||
import {
|
||||
createTasksWithPriorities,
|
||||
createTasksWithSearch,
|
||||
} from '../../support/filterTestHelpers'
|
||||
|
||||
describe('Project View List', () => {
|
||||
createFakeUserAndLogin()
|
||||
prepareProjects()
|
||||
|
||||
it('Should respect filter query parameter from URL', () => {
|
||||
const {highPriorityTasks, lowPriorityTasks} = createTasksWithPriorities()
|
||||
|
||||
cy.visit('/projects/1/1?filter=priority%20>=%204')
|
||||
|
||||
cy.url()
|
||||
.should('include', 'filter=priority')
|
||||
|
||||
cy.contains('.tasks', highPriorityTasks[0].title, {timeout: 10000})
|
||||
.should('exist')
|
||||
|
||||
cy.get('.tasks')
|
||||
.should('contain', highPriorityTasks[0].title)
|
||||
cy.get('.tasks')
|
||||
.should('contain', highPriorityTasks[1].title)
|
||||
|
||||
cy.get('.tasks')
|
||||
.should('not.contain', lowPriorityTasks[0].title)
|
||||
cy.get('.tasks')
|
||||
.should('not.contain', lowPriorityTasks[1].title)
|
||||
})
|
||||
|
||||
it('Should respect search query parameter from URL', () => {
|
||||
const {searchableTask} = createTasksWithSearch()
|
||||
|
||||
cy.visit('/projects/1/1?s=meeting')
|
||||
|
||||
cy.url()
|
||||
.should('include', 's=meeting')
|
||||
|
||||
cy.contains('.tasks', searchableTask.title, {timeout: 10000})
|
||||
.should('exist')
|
||||
|
||||
cy.get('.tasks')
|
||||
.should('contain', searchableTask.title)
|
||||
|
||||
cy.get('.tasks .task')
|
||||
.should('have.length', 1)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {ProjectFactory} from '../../factories/project'
|
||||
import {seed} from '../../support/seed'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {BucketFactory} from '../../factories/bucket'
|
||||
import {updateUserSettings} from '../../support/updateUserSettings'
|
||||
import {createDefaultViews} from "../project/prepareProjects";
|
||||
|
||||
function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
|
||||
const project = ProjectFactory.create()[0]
|
||||
const views = createDefaultViews(project.id)
|
||||
BucketFactory.create(1, {
|
||||
project_view_id: views[3].id,
|
||||
})
|
||||
const tasks = []
|
||||
let dueDate = startDueDate
|
||||
for (let i = 0; i < numberOfTasks; i++) {
|
||||
const now = new Date()
|
||||
dueDate = new Date(new Date(dueDate).setDate(dueDate.getDate() + 2))
|
||||
tasks.push({
|
||||
id: i + 1,
|
||||
project_id: project.id,
|
||||
done: false,
|
||||
created_by_id: 1,
|
||||
title: 'Test Task ' + i,
|
||||
index: i + 1,
|
||||
due_date: dueDate.toISOString(),
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
})
|
||||
}
|
||||
seed(TaskFactory.table, tasks)
|
||||
return {tasks, project}
|
||||
}
|
||||
|
||||
describe('Home Page Task Overview', () => {
|
||||
createFakeUserAndLogin()
|
||||
|
||||
it('Should show a new task with a very soon due date at the top', () => {
|
||||
const {tasks} = seedTasks(49)
|
||||
const newTaskTitle = 'New Task'
|
||||
|
||||
cy.visit('/')
|
||||
|
||||
TaskFactory.create(1, {
|
||||
id: 999,
|
||||
title: newTaskTitle,
|
||||
due_date: new Date().toISOString(),
|
||||
}, false)
|
||||
|
||||
cy.visit(`/projects/${tasks[0].project_id}/1`)
|
||||
cy.get('.tasks .task')
|
||||
.should('contain.text', newTaskTitle)
|
||||
cy.visit('/')
|
||||
cy.get('[data-cy="showTasks"] .card .task')
|
||||
.first()
|
||||
.should('contain.text', newTaskTitle)
|
||||
})
|
||||
|
||||
it('Should not show a new task without a date at the bottom when there are > 50 tasks', () => {
|
||||
// We're not using the api here to create the task in order to verify the flow
|
||||
const {tasks} = seedTasks(100)
|
||||
const newTaskTitle = 'New Task'
|
||||
|
||||
cy.visit('/')
|
||||
|
||||
cy.visit(`/projects/${tasks[0].project_id}/1`)
|
||||
cy.get('.task-add textarea')
|
||||
.type(newTaskTitle+'{enter}')
|
||||
cy.visit('/')
|
||||
cy.get('[data-cy="showTasks"] .card .task')
|
||||
.last()
|
||||
.should('not.contain.text', newTaskTitle)
|
||||
})
|
||||
|
||||
it('Should show a new task without a date at the bottom when there are < 50 tasks', () => {
|
||||
seedTasks(40)
|
||||
const newTaskTitle = 'New Task'
|
||||
TaskFactory.create(1, {
|
||||
id: 999,
|
||||
title: newTaskTitle,
|
||||
}, false)
|
||||
|
||||
cy.visit('/')
|
||||
cy.get('[data-cy="showTasks"] .card .task')
|
||||
.last()
|
||||
.should('contain.text', newTaskTitle)
|
||||
})
|
||||
|
||||
it('Should show a task without a due date added via default project at the bottom', () => {
|
||||
const {project} = seedTasks(40)
|
||||
updateUserSettings({
|
||||
default_project_id: project.id,
|
||||
overdue_tasks_reminders_time: '9:00',
|
||||
})
|
||||
|
||||
const newTaskTitle = 'New Task'
|
||||
cy.visit('/')
|
||||
|
||||
cy.get('.add-task-textarea')
|
||||
.type(`${newTaskTitle}{enter}`)
|
||||
|
||||
cy.get('[data-cy="showTasks"] .card .task')
|
||||
.last()
|
||||
.should('contain.text', newTaskTitle)
|
||||
})
|
||||
|
||||
})
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["./**/*", "../support/**/*", "../factories/**/*"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"isolatedModules": false,
|
||||
"target": "ES2015",
|
||||
"lib": ["ESNext", "dom"],
|
||||
"types": ["cypress"],
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import {UserFactory} from '../../factories/user'
|
||||
import {ProjectFactory} from '../../factories/project'
|
||||
|
||||
const testAndAssertFailed = fixture => {
|
||||
cy.intercept(Cypress.env('API_URL') + '/login*').as('login')
|
||||
|
||||
cy.visit('/login')
|
||||
cy.get('input[id=username]').type(fixture.username)
|
||||
cy.get('input[id=password]').type(fixture.password)
|
||||
cy.get('.button').contains('Login').click()
|
||||
|
||||
cy.wait('@login')
|
||||
cy.url().should('include', '/')
|
||||
cy.get('div.message.danger').contains('Wrong username or password.')
|
||||
}
|
||||
|
||||
const credentials = {
|
||||
username: 'test',
|
||||
password: '1234',
|
||||
}
|
||||
|
||||
function login() {
|
||||
cy.get('input[id=username]').type(credentials.username)
|
||||
cy.get('input[id=password]').type(credentials.password)
|
||||
cy.get('.button').contains('Login').click()
|
||||
cy.url().should('include', '/')
|
||||
}
|
||||
|
||||
context('Login', () => {
|
||||
beforeEach(() => {
|
||||
UserFactory.create(1, {username: credentials.username})
|
||||
})
|
||||
|
||||
it('Should fail with a bad password', () => {
|
||||
const fixture = {
|
||||
username: 'test',
|
||||
password: '123456',
|
||||
}
|
||||
|
||||
testAndAssertFailed(fixture)
|
||||
})
|
||||
|
||||
it('Should redirect to the previous route after logging in', () => {
|
||||
const projects = ProjectFactory.create(1)
|
||||
cy.visit(`/projects/${projects[0].id}/1`)
|
||||
|
||||
cy.url().should('include', '/login')
|
||||
|
||||
login()
|
||||
|
||||
cy.url().should('include', `/projects/${projects[0].id}/1`)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
describe('User Settings', () => {
|
||||
createFakeUserAndLogin()
|
||||
|
||||
it('Changes the user avatar', () => {
|
||||
cy.intercept(`${Cypress.env('API_URL')}/user/settings/avatar/upload`).as('uploadAvatar')
|
||||
|
||||
cy.visit('/user/settings/avatar')
|
||||
|
||||
cy.get('input[name=avatarProvider][value=upload]')
|
||||
.click()
|
||||
cy.get('input[type=file]', {timeout: 1000})
|
||||
.selectFile('cypress/fixtures/image.jpg', {force: true}) // The input is not visible, but on purpose
|
||||
cy.get('.vue-handler-wrapper.vue-handler-wrapper--south .vue-simple-handler.vue-simple-handler--south')
|
||||
.trigger('mousedown', {which: 1})
|
||||
.trigger('mousemove', {clientY: 100})
|
||||
.trigger('mouseup')
|
||||
cy.get('[data-cy="uploadAvatar"]')
|
||||
.contains('Upload Avatar')
|
||||
.click()
|
||||
|
||||
cy.wait('@uploadAvatar')
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
})
|
||||
|
||||
it('Updates the name', () => {
|
||||
cy.visit('/user/settings/general')
|
||||
|
||||
cy.get('.general-settings input.input')
|
||||
.first()
|
||||
.type('Lorem Ipsum')
|
||||
cy.get('[data-cy="saveGeneralSettings"]')
|
||||
.contains('Save')
|
||||
.click()
|
||||
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.navbar .username-dropdown-trigger .username')
|
||||
.should('contain', 'Lorem Ipsum')
|
||||
})
|
||||
})
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import {faker} from '@faker-js/faker'
|
||||
import {Factory} from '../support/factory'
|
||||
|
||||
export class BucketFactory extends Factory {
|
||||
static table = 'buckets'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
title: faker.lorem.words(3),
|
||||
project_view_id: '{increment}',
|
||||
created_by_id: 1,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import {Factory} from '../support/factory'
|
||||
|
||||
export class LabelTaskFactory extends Factory {
|
||||
static table = 'label_tasks'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
task_id: 1,
|
||||
label_id: 1,
|
||||
created: now.toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import {faker} from '@faker-js/faker'
|
||||
|
||||
import {Factory} from '../support/factory'
|
||||
|
||||
export class LabelFactory extends Factory {
|
||||
static table = 'labels'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
title: faker.lorem.words(2),
|
||||
description: faker.lorem.text(10),
|
||||
hex_color: (Math.random()*0xFFFFFF<<0).toString(16), // random 6-digit hex number
|
||||
created_by_id: 1,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import {Factory} from '../support/factory'
|
||||
import {faker} from '@faker-js/faker'
|
||||
|
||||
export class LinkShareFactory extends Factory {
|
||||
static table = 'link_shares'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
hash: faker.lorem.word(32),
|
||||
project_id: 1,
|
||||
permission: 0,
|
||||
sharing_type: 0,
|
||||
shared_by_id: 1,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import {Factory} from '../support/factory'
|
||||
import {faker} from '@faker-js/faker'
|
||||
|
||||
export interface ProjectAttributes {
|
||||
id: number | '{increment}';
|
||||
title: string;
|
||||
owner_id: number;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export class ProjectFactory extends Factory {
|
||||
static table = 'projects'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
title: faker.lorem.words(3),
|
||||
owner_id: 1,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import {Factory} from '../support/factory'
|
||||
import {faker} from '@faker-js/faker'
|
||||
|
||||
export class ProjectViewFactory extends Factory {
|
||||
static table = 'project_views'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
title: faker.lorem.words(3),
|
||||
project_id: '{increment}',
|
||||
view_kind: 0,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import {faker} from '@faker-js/faker'
|
||||
import {Factory} from '../support/factory'
|
||||
|
||||
export class TaskFactory extends Factory {
|
||||
static table = 'tasks'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
title: faker.lorem.words(3),
|
||||
done: false,
|
||||
project_id: 1,
|
||||
created_by_id: 1,
|
||||
index: '{increment}',
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import {Factory} from '../support/factory'
|
||||
|
||||
export class TaskAssigneeFactory extends Factory {
|
||||
static table = 'task_assignees'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
task_id: 1,
|
||||
user_id: 1,
|
||||
created: now.toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import {Factory} from '../support/factory'
|
||||
|
||||
export class TaskAttachmentFactory extends Factory {
|
||||
static table = 'task_attachments'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
task_id: 1,
|
||||
file_id: 1,
|
||||
created: now.toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import {Factory} from '../support/factory'
|
||||
|
||||
export class TaskBucketFactory extends Factory {
|
||||
static table = 'task_buckets'
|
||||
|
||||
static factory() {
|
||||
return {
|
||||
task_id: '{increment}',
|
||||
bucket_id: '{increment}',
|
||||
project_view_id: '{increment}',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import {faker} from '@faker-js/faker'
|
||||
|
||||
import {Factory} from '../support/factory'
|
||||
|
||||
export class TaskCommentFactory extends Factory {
|
||||
static table = 'task_comments'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
comment: faker.lorem.text(3),
|
||||
author_id: 1,
|
||||
task_id: 1,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import {Factory} from '../support/factory'
|
||||
|
||||
export class TaskRelationFactory extends Factory {
|
||||
static table = 'task_relations'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
task_id: '{increment}',
|
||||
other_task_id: '{increment}',
|
||||
relation_kind: 'related',
|
||||
created_by_id: 1,
|
||||
created: now.toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import {Factory} from '../support/factory'
|
||||
|
||||
export class TaskReminderFactory extends Factory {
|
||||
static table = 'task_reminders'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
task_id: 1,
|
||||
reminder: now.toISOString(),
|
||||
created: now.toISOString(),
|
||||
relative_to: '',
|
||||
relative_period: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import {faker} from '@faker-js/faker'
|
||||
import {Factory} from '../support/factory'
|
||||
|
||||
export class TeamFactory extends Factory {
|
||||
static table = 'teams'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
name: faker.lorem.words(3),
|
||||
created_by_id: 1,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import {Factory} from '../support/factory'
|
||||
|
||||
export class TeamMemberFactory extends Factory {
|
||||
static table = 'team_members'
|
||||
|
||||
static factory() {
|
||||
return {
|
||||
team_id: 1,
|
||||
user_id: 1,
|
||||
admin: false,
|
||||
created: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import {faker} from '@faker-js/faker'
|
||||
import {Factory} from '../support/factory'
|
||||
|
||||
export interface TokenAttributes {
|
||||
id: number | '{increment}';
|
||||
user_id: number;
|
||||
token: string;
|
||||
kind: number;
|
||||
created: string;
|
||||
}
|
||||
|
||||
export class TokenFactory extends Factory {
|
||||
static table = 'user_tokens'
|
||||
|
||||
// The factory method itself produces an object where id is '{increment}' (a string)
|
||||
// before it gets processed by the main create() method in the base Factory class.
|
||||
static factory(attrs?: Partial<Omit<TokenAttributes, 'id'>>): Omit<TokenAttributes, 'id'> & { id: string } {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}', // This is a string
|
||||
user_id: 1, // Default user_id
|
||||
token: faker.string.alphanumeric(64),
|
||||
kind: 1, // TokenPasswordReset
|
||||
created: now.toISOString(),
|
||||
...(attrs ?? {}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import {faker} from '@faker-js/faker'
|
||||
|
||||
import {Factory} from '../support/factory'
|
||||
|
||||
export interface UserAttributes {
|
||||
id: number | '{increment}';
|
||||
username: string;
|
||||
password?: string;
|
||||
status: number;
|
||||
issuer: string;
|
||||
language: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export class UserFactory extends Factory {
|
||||
static table = 'users'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
username: faker.lorem.word(10) + faker.string.uuid(),
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.', // 1234
|
||||
status: 0,
|
||||
issuer: 'local',
|
||||
language: 'en',
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import {Factory} from '../support/factory'
|
||||
|
||||
export class UserProjectFactory extends Factory {
|
||||
static table = 'users_projects'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
project_id: 1,
|
||||
user_id: 1,
|
||||
permission: 0,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 872 KiB |
|
|
@ -1,39 +0,0 @@
|
|||
|
||||
// This authenticates a user and puts the token in local storage which allows us to perform authenticated requests.
|
||||
// Built after https://github.com/cypress-io/cypress-example-recipes/tree/bd2d6ffb33214884cab343d38e7f9e6ebffb323f/examples/logging-in__jwt
|
||||
|
||||
import {UserFactory} from '../factories/user'
|
||||
|
||||
export function login(user, cacheAcrossSpecs = false) {
|
||||
if (!user) {
|
||||
throw new Error('Needs user')
|
||||
}
|
||||
// Caching session when logging in via page visit
|
||||
cy.session(`user__${user.username}`, () => {
|
||||
cy.request('POST', `${Cypress.env('API_URL')}/login`, {
|
||||
username: user.username,
|
||||
password: '1234',
|
||||
}).then(({ body }) => {
|
||||
window.localStorage.setItem('token', body.token)
|
||||
})
|
||||
}, {
|
||||
cacheAcrossSpecs,
|
||||
})
|
||||
}
|
||||
|
||||
export function createFakeUser() {
|
||||
return UserFactory.create(1)[0]
|
||||
}
|
||||
|
||||
export function createFakeUserAndLogin() {
|
||||
let user
|
||||
before(() => {
|
||||
user = createFakeUser()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
login(user, true)
|
||||
})
|
||||
|
||||
return user
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
Cypress.Commands.add('pasteFile', {prevSubject: true}, (subject, fileName, fileType = 'image/png') => {
|
||||
// Load the file fixture as base64
|
||||
cy.fixture(fileName, 'base64').then((fileContent) => {
|
||||
// Convert base64 to a Blob
|
||||
const blob = Cypress.Blob.base64StringToBlob(fileContent, fileType)
|
||||
// Create a File object
|
||||
const testFile = new File([blob], fileName, {type: fileType})
|
||||
// Create a DataTransfer and add the file
|
||||
const dataTransfer = new DataTransfer()
|
||||
dataTransfer.items.add(testFile)
|
||||
|
||||
// Create the paste event with clipboardData containing the file
|
||||
const pasteEvent = new ClipboardEvent('paste', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clipboardData: dataTransfer,
|
||||
})
|
||||
|
||||
// Dispatch the paste event on the target element
|
||||
subject[0].dispatchEvent(pasteEvent)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Components App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div data-cy-root></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
// ***********************************************************
|
||||
// This example support/component.ts is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
import { mount } from 'cypress/vue'
|
||||
// Ensure global styles are loaded
|
||||
import '../../src/styles/global.scss';
|
||||
|
||||
Cypress.Commands.add('mount', mount)
|
||||
|
||||
// Example use:
|
||||
// cy.mount(MyComponent)
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
|
||||
import './commands'
|
||||
import '@4tw/cypress-drag-drop'
|
||||
|
||||
// see https://github.com/cypress-io/cypress/issues/702#issuecomment-587127275
|
||||
Cypress.on('window:before:load', (win) => {
|
||||
// disable service workers
|
||||
// @ts-ignore
|
||||
delete win.navigator.__proto__.ServiceWorker
|
||||
})
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
import {seed} from './seed'
|
||||
|
||||
/**
|
||||
* A factory makes it easy to seed the database with data.
|
||||
*/
|
||||
export class Factory {
|
||||
static table: string | null = null
|
||||
|
||||
static factory() {
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds a bunch of fake data into the database.
|
||||
*
|
||||
* Takes an override object as its single argument which will override the data from the factory.
|
||||
* If the value of one of the override fields is `{increment}` that value will be replaced with an incrementing
|
||||
* number through all created entities.
|
||||
*
|
||||
* @param override
|
||||
* @returns {[]}
|
||||
*/
|
||||
static create(count = 1, override = {}, truncate = true) {
|
||||
const data = []
|
||||
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const entry = {
|
||||
...this.factory(),
|
||||
...override,
|
||||
}
|
||||
for (const e in entry) {
|
||||
if(typeof entry[e] === 'function') {
|
||||
entry[e] = entry[e](i)
|
||||
continue
|
||||
}
|
||||
if (entry[e] === '{increment}') {
|
||||
entry[e] = i
|
||||
}
|
||||
}
|
||||
data.push(entry)
|
||||
}
|
||||
|
||||
// Create a flattened copy of the data for seeding
|
||||
// This removes nested objects/arrays that the backend can't handle
|
||||
const flatData = data.map(item => {
|
||||
const flatItem = {}
|
||||
for (const key in item) {
|
||||
const value = item[key]
|
||||
// Only include primitive values (string, number, boolean, null, Date)
|
||||
if (value === null || value === undefined ||
|
||||
typeof value === 'string' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean' ||
|
||||
value instanceof Date) {
|
||||
flatItem[key] = value
|
||||
}
|
||||
// Skip arrays, objects, and other complex types
|
||||
}
|
||||
return flatItem
|
||||
})
|
||||
|
||||
seed(this.table, flatData, truncate)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
static truncate() {
|
||||
seed(this.table, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
import {TaskFactory} from '../factories/task'
|
||||
import {TaskBucketFactory} from '../factories/task_buckets'
|
||||
|
||||
export function createTasksWithPriorities(buckets?: any[]) {
|
||||
TaskFactory.truncate()
|
||||
|
||||
const highPriorityTask1 = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
project_id: 1,
|
||||
priority: 4,
|
||||
title: 'High Priority Task 1',
|
||||
}, false)[0]
|
||||
|
||||
const highPriorityTask2 = TaskFactory.create(1, {
|
||||
id: 2,
|
||||
project_id: 1,
|
||||
priority: 4,
|
||||
title: 'High Priority Task 2',
|
||||
}, false)[0]
|
||||
|
||||
const lowPriorityTask1 = TaskFactory.create(1, {
|
||||
id: 3,
|
||||
project_id: 1,
|
||||
priority: 1,
|
||||
title: 'Low Priority Task 1',
|
||||
}, false)[0]
|
||||
|
||||
const lowPriorityTask2 = TaskFactory.create(1, {
|
||||
id: 4,
|
||||
project_id: 1,
|
||||
priority: 1,
|
||||
title: 'Low Priority Task 2',
|
||||
}, false)[0]
|
||||
|
||||
// If buckets are provided (for Kanban), add tasks to buckets
|
||||
if (buckets && buckets.length > 0) {
|
||||
TaskBucketFactory.truncate()
|
||||
TaskBucketFactory.create(1, {
|
||||
task_id: highPriorityTask1.id,
|
||||
bucket_id: buckets[0].id,
|
||||
project_view_id: buckets[0].project_view_id,
|
||||
}, false)
|
||||
TaskBucketFactory.create(1, {
|
||||
task_id: highPriorityTask2.id,
|
||||
bucket_id: buckets[0].id,
|
||||
project_view_id: buckets[0].project_view_id,
|
||||
}, false)
|
||||
TaskBucketFactory.create(1, {
|
||||
task_id: lowPriorityTask1.id,
|
||||
bucket_id: buckets[0].id,
|
||||
project_view_id: buckets[0].project_view_id,
|
||||
}, false)
|
||||
TaskBucketFactory.create(1, {
|
||||
task_id: lowPriorityTask2.id,
|
||||
bucket_id: buckets[0].id,
|
||||
project_view_id: buckets[0].project_view_id,
|
||||
}, false)
|
||||
}
|
||||
|
||||
return {
|
||||
highPriorityTasks: [highPriorityTask1, highPriorityTask2],
|
||||
lowPriorityTasks: [lowPriorityTask1, lowPriorityTask2],
|
||||
}
|
||||
}
|
||||
|
||||
export function createTasksWithSearch(buckets?: any[]) {
|
||||
TaskFactory.truncate()
|
||||
|
||||
const task1 = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
project_id: 1,
|
||||
title: 'Regular task 1',
|
||||
}, false)[0]
|
||||
|
||||
const task2 = TaskFactory.create(1, {
|
||||
id: 2,
|
||||
project_id: 1,
|
||||
title: 'Regular task 2',
|
||||
}, false)[0]
|
||||
|
||||
const task3 = TaskFactory.create(1, {
|
||||
id: 3,
|
||||
project_id: 1,
|
||||
title: 'Regular task 3',
|
||||
}, false)[0]
|
||||
|
||||
const searchableTask = TaskFactory.create(1, {
|
||||
id: 4,
|
||||
project_id: 1,
|
||||
title: 'Meeting notes for project',
|
||||
}, false)[0]
|
||||
|
||||
// If buckets are provided (for Kanban), add tasks to buckets
|
||||
if (buckets && buckets.length > 0) {
|
||||
TaskBucketFactory.truncate()
|
||||
TaskBucketFactory.create(1, {
|
||||
task_id: task1.id,
|
||||
bucket_id: buckets[0].id,
|
||||
project_view_id: buckets[0].project_view_id,
|
||||
}, false)
|
||||
TaskBucketFactory.create(1, {
|
||||
task_id: task2.id,
|
||||
bucket_id: buckets[0].id,
|
||||
project_view_id: buckets[0].project_view_id,
|
||||
}, false)
|
||||
TaskBucketFactory.create(1, {
|
||||
task_id: task3.id,
|
||||
bucket_id: buckets[0].id,
|
||||
project_view_id: buckets[0].project_view_id,
|
||||
}, false)
|
||||
TaskBucketFactory.create(1, {
|
||||
task_id: searchableTask.id,
|
||||
bucket_id: buckets[0].id,
|
||||
project_view_id: buckets[0].project_view_id,
|
||||
}, false)
|
||||
}
|
||||
|
||||
return { searchableTask }
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/// <reference types="cypress" />
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable<Subject = any> {
|
||||
/**
|
||||
* Pastes a file onto the subject element.
|
||||
* @param fileName The name of the file to paste
|
||||
* @param fileType The MIME type of the file (defaults to 'image/png')
|
||||
*/
|
||||
pasteFile(fileName: string, fileType?: string): Chainable<Subject>;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
/**
|
||||
* Seeds a db table with data. If a data object is provided as the second argument, it will load the fixtures
|
||||
* file for the table and merge the data from it with the passed data. This allows you to override specific
|
||||
* fields of the fixtures without having to redeclare the whole fixture.
|
||||
*
|
||||
* Passing null as the second argument empties the table.
|
||||
*
|
||||
* @param table
|
||||
* @param data
|
||||
*/
|
||||
export function seed(table, data = {}, truncate = true) {
|
||||
if (data === null) {
|
||||
data = []
|
||||
}
|
||||
|
||||
cy.request({
|
||||
method: 'PATCH',
|
||||
url: `${Cypress.env('API_URL')}/test/${table}?truncate=${truncate ? 'true' : 'false'}`,
|
||||
headers: {
|
||||
'Authorization': Cypress.env('TEST_SECRET'),
|
||||
},
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
|
||||
export function updateUserSettings(settings) {
|
||||
const token = `Bearer ${window.localStorage.getItem('token')}`
|
||||
|
||||
return cy.request({
|
||||
method: 'GET',
|
||||
url: `${Cypress.env('API_URL')}/user`,
|
||||
headers: {
|
||||
'Authorization': token,
|
||||
},
|
||||
})
|
||||
.its('body')
|
||||
.then(oldSettings => {
|
||||
return cy.request({
|
||||
method: 'POST',
|
||||
url: `${Cypress.env('API_URL')}/user/settings/general`,
|
||||
headers: {
|
||||
'Authorization': token,
|
||||
},
|
||||
body: {
|
||||
...oldSettings,
|
||||
...settings,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-svg-loader" />
|
||||
/// <reference types="cypress" />
|
||||
/// <reference types="@histoire/plugin-vue/components" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ export default [
|
|||
{
|
||||
ignores: [
|
||||
'**/*.test.ts',
|
||||
'./cypress',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -78,15 +77,6 @@ export default [
|
|||
},
|
||||
|
||||
|
||||
// 'parser': 'vue-eslint-parser',
|
||||
// 'parserOptions': {
|
||||
// 'parser': '@typescript-eslint/parser',
|
||||
// 'ecmaVersion': 'latest',
|
||||
// 'tsconfigRootDir': __dirname,
|
||||
// },
|
||||
// 'ignorePatterns': [
|
||||
// 'cypress/*',
|
||||
// ],
|
||||
},
|
||||
|
||||
]
|
||||
|
|
|
|||
|
|
@ -38,12 +38,6 @@
|
|||
"test:e2e": "playwright test",
|
||||
"test:e2e:headed": "playwright test --headed",
|
||||
"test:e2e:ui": "playwright test --ui-host=0.0.0.0",
|
||||
"test:cypress:headed": "start-server-and-test preview http://127.0.0.1:4173 'cypress open --e2e'",
|
||||
"test:cypress:e2e": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome'",
|
||||
"test:cypress:e2e-nix": "CYPRESS_RUN_BINARY=`which Cypress` CYPRESS_API_URL=http://127.0.0.1:3456/api/v1 start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chromium'",
|
||||
"test:cypress:e2e-record-test": "start-server-and-test preview:test http://127.0.0.1:4173 'cypress run --e2e --browser chrome --record'",
|
||||
"test:cypress:e2e-dev-dev": "start-server-and-test preview:dev http://127.0.0.1:4173 'cypress open --e2e'",
|
||||
"test:cypress:e2e-dev": "start-server-and-test preview http://127.0.0.1:4173 'cypress open --e2e'",
|
||||
"test:unit": "vitest --dir ./src",
|
||||
"typecheck": "vue-tsc --build --force",
|
||||
"fonts:update": "pnpm fonts:download && pnpm fonts:subset",
|
||||
|
|
@ -109,9 +103,6 @@
|
|||
"zhyswan-vuedraggable": "4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@4tw/cypress-drag-drop": "2.3.1",
|
||||
"@cypress/vite-dev-server": "7.0.1",
|
||||
"@cypress/vue": "6.0.2",
|
||||
"@faker-js/faker": "9.9.0",
|
||||
"@histoire/plugin-screenshot": "1.0.0-alpha.5",
|
||||
"@histoire/plugin-vue": "1.0.0-alpha.5",
|
||||
|
|
@ -132,7 +123,6 @@
|
|||
"browserslist": "4.28.1",
|
||||
"caniuse-lite": "1.0.30001759",
|
||||
"csstype": "3.2.3",
|
||||
"cypress": "14.5.4",
|
||||
"esbuild": "0.27.1",
|
||||
"eslint": "9.39.1",
|
||||
"eslint-plugin-vue": "10.6.2",
|
||||
|
|
@ -144,7 +134,6 @@
|
|||
"rollup": "4.53.3",
|
||||
"rollup-plugin-visualizer": "6.0.5",
|
||||
"sass-embedded": "1.93.3",
|
||||
"start-server-and-test": "2.1.3",
|
||||
"stylelint": "16.26.1",
|
||||
"stylelint-config-property-sort-order-smacss": "10.0.0",
|
||||
"stylelint-config-recommended-vue": "1.6.1",
|
||||
|
|
@ -170,7 +159,6 @@
|
|||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"@sentry/cli",
|
||||
"cypress",
|
||||
"esbuild",
|
||||
"puppeteer",
|
||||
"vue-demi"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,7 +2,6 @@ import type {Directive} from 'vue'
|
|||
|
||||
declare global {
|
||||
interface Window {
|
||||
Cypress: object;
|
||||
TESTING?: boolean;
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +13,7 @@ function isTestingEnabled(): boolean {
|
|||
return import.meta.env.DEV || window.TESTING === true
|
||||
}
|
||||
|
||||
const cypressDirective = <Directive<HTMLElement,string>>{
|
||||
const testIdDirective = <Directive<HTMLElement,string>>{
|
||||
mounted(el, {arg, value}) {
|
||||
if (!isTestingEnabled()) {
|
||||
return
|
||||
|
|
@ -31,4 +30,4 @@ const cypressDirective = <Directive<HTMLElement,string>>{
|
|||
},
|
||||
}
|
||||
|
||||
export default cypressDirective
|
||||
export default testIdDirective
|
||||
|
|
@ -6,7 +6,7 @@ import './styles/global.scss'
|
|||
|
||||
import {createPinia} from 'pinia'
|
||||
|
||||
import cypress from '@/directives/cypress'
|
||||
import testid from '@/directives/testid'
|
||||
|
||||
import FontAwesomeIcon from '@/components/misc/Icon'
|
||||
import XButton from '@/components/input/button.vue'
|
||||
|
|
@ -19,7 +19,7 @@ export const setupVue3 = defineSetupVue3(({ app }) => {
|
|||
app.use(pinia)
|
||||
app.use(i18n)
|
||||
|
||||
app.directive('cy', cypress)
|
||||
app.directive('cy', testid)
|
||||
|
||||
app.component('Icon', FontAwesomeIcon)
|
||||
app.component('XButton', XButton)
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ import focus from '@/directives/focus'
|
|||
import {vTooltip} from 'floating-vue'
|
||||
import 'floating-vue/dist/style.css'
|
||||
import shortcut from '@/directives/shortcut'
|
||||
import cypress from '@/directives/cypress'
|
||||
import testid from '@/directives/testid'
|
||||
|
||||
// global components
|
||||
import FontAwesomeIcon from '@/components/misc/Icon'
|
||||
|
|
@ -60,7 +60,7 @@ setLanguage(browserLanguage).then(() => {
|
|||
app.directive('focus', focus)
|
||||
app.directive('tooltip', vTooltip)
|
||||
app.directive('shortcut', shortcut)
|
||||
app.directive('cy', cypress)
|
||||
app.directive('cy', testid)
|
||||
|
||||
app.component('Icon', FontAwesomeIcon)
|
||||
app.component('XButton', Button)
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
import { mount } from 'cypress/vue'
|
||||
|
||||
type MountParams = Parameters<typeof mount>;
|
||||
type OptionsParam = MountParams[1];
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
mount: typeof mount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import {UserFactory} from '../../factories/user'
|
|||
import {ProjectFactory} from '../../factories/project'
|
||||
import {createProjects} from './prepareProjects'
|
||||
import {BucketFactory} from '../../factories/bucket'
|
||||
import {createTasksWithPriorities, createTasksWithSearch} from '../../support/filterTestHelpers'
|
||||
|
||||
test.describe('Project View List', () => {
|
||||
test('Should be an empty project', async ({authenticatedPage: page}) => {
|
||||
|
|
@ -164,4 +165,36 @@ test.describe('Project View List', () => {
|
|||
await expect(page.locator('ul.tasks > div > .single-task')).toBeVisible()
|
||||
await expect(page.locator('ul.tasks > div > .subtask-nested')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Should respect filter query parameter from URL', async ({authenticatedPage: page}) => {
|
||||
await createProjects(1)
|
||||
const {highPriorityTasks, lowPriorityTasks} = await createTasksWithPriorities()
|
||||
|
||||
await page.goto('/projects/1/1?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('.tasks')).toContainText(highPriorityTasks[0].title, {timeout: 10000})
|
||||
await expect(page.locator('.tasks')).toContainText(highPriorityTasks[1].title)
|
||||
|
||||
// Verify low priority tasks are NOT visible
|
||||
await expect(page.locator('.tasks')).not.toContainText(lowPriorityTasks[0].title)
|
||||
await expect(page.locator('.tasks')).not.toContainText(lowPriorityTasks[1].title)
|
||||
})
|
||||
|
||||
test('Should respect search query parameter from URL', async ({authenticatedPage: page}) => {
|
||||
await createProjects(1)
|
||||
const {searchableTask} = await createTasksWithSearch()
|
||||
|
||||
await page.goto('/projects/1/1?s=meeting')
|
||||
|
||||
await expect(page).toHaveURL(/s=meeting/)
|
||||
|
||||
// Wait for tasks to load and verify searchable task is visible
|
||||
await expect(page.locator('.tasks')).toContainText(searchableTask.title, {timeout: 10000})
|
||||
|
||||
// Only one task should be visible (the searchable one)
|
||||
await expect(page.locator('.tasks .task')).toHaveCount(1)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
"env.config.d.ts",
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*"
|
||||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
|
|
|||
Loading…
Reference in New Issue