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 }}
|
name: playwright-test-results-${{ matrix.shard }}
|
||||||
path: frontend/test-results/
|
path: frontend/test-results/
|
||||||
retention-days: 30
|
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/
|
.vite/
|
||||||
|
|
||||||
# Test files
|
# Test files
|
||||||
cypress/screenshots
|
|
||||||
cypress/videos
|
|
||||||
playwright-report/
|
playwright-report/
|
||||||
test-results/
|
test-results/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,2 @@
|
||||||
# https://github.com/pnpm/pnpm/issues/8378#issuecomment-2636152421
|
# https://github.com/pnpm/pnpm/issues/8378#issuecomment-2636152421
|
||||||
public-hoist-pattern[]=*eslint*
|
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": [
|
"ignoreFiles": [
|
||||||
"node_modules/**/*",
|
"node_modules/**/*",
|
||||||
"dist/**/*",
|
"dist/**/*",
|
||||||
"cypress/**/*",
|
|
||||||
"**/*.js",
|
"**/*.js",
|
||||||
"**/*.ts"
|
"**/*.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/client" />
|
||||||
/// <reference types="vite-svg-loader" />
|
/// <reference types="vite-svg-loader" />
|
||||||
/// <reference types="cypress" />
|
|
||||||
/// <reference types="@histoire/plugin-vue/components" />
|
/// <reference types="@histoire/plugin-vue/components" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ export default [
|
||||||
{
|
{
|
||||||
ignores: [
|
ignores: [
|
||||||
'**/*.test.ts',
|
'**/*.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": "playwright test",
|
||||||
"test:e2e:headed": "playwright test --headed",
|
"test:e2e:headed": "playwright test --headed",
|
||||||
"test:e2e:ui": "playwright test --ui-host=0.0.0.0",
|
"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",
|
"test:unit": "vitest --dir ./src",
|
||||||
"typecheck": "vue-tsc --build --force",
|
"typecheck": "vue-tsc --build --force",
|
||||||
"fonts:update": "pnpm fonts:download && pnpm fonts:subset",
|
"fonts:update": "pnpm fonts:download && pnpm fonts:subset",
|
||||||
|
|
@ -109,9 +103,6 @@
|
||||||
"zhyswan-vuedraggable": "4.1.3"
|
"zhyswan-vuedraggable": "4.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"@faker-js/faker": "9.9.0",
|
||||||
"@histoire/plugin-screenshot": "1.0.0-alpha.5",
|
"@histoire/plugin-screenshot": "1.0.0-alpha.5",
|
||||||
"@histoire/plugin-vue": "1.0.0-alpha.5",
|
"@histoire/plugin-vue": "1.0.0-alpha.5",
|
||||||
|
|
@ -132,7 +123,6 @@
|
||||||
"browserslist": "4.28.1",
|
"browserslist": "4.28.1",
|
||||||
"caniuse-lite": "1.0.30001759",
|
"caniuse-lite": "1.0.30001759",
|
||||||
"csstype": "3.2.3",
|
"csstype": "3.2.3",
|
||||||
"cypress": "14.5.4",
|
|
||||||
"esbuild": "0.27.1",
|
"esbuild": "0.27.1",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.1",
|
||||||
"eslint-plugin-vue": "10.6.2",
|
"eslint-plugin-vue": "10.6.2",
|
||||||
|
|
@ -144,7 +134,6 @@
|
||||||
"rollup": "4.53.3",
|
"rollup": "4.53.3",
|
||||||
"rollup-plugin-visualizer": "6.0.5",
|
"rollup-plugin-visualizer": "6.0.5",
|
||||||
"sass-embedded": "1.93.3",
|
"sass-embedded": "1.93.3",
|
||||||
"start-server-and-test": "2.1.3",
|
|
||||||
"stylelint": "16.26.1",
|
"stylelint": "16.26.1",
|
||||||
"stylelint-config-property-sort-order-smacss": "10.0.0",
|
"stylelint-config-property-sort-order-smacss": "10.0.0",
|
||||||
"stylelint-config-recommended-vue": "1.6.1",
|
"stylelint-config-recommended-vue": "1.6.1",
|
||||||
|
|
@ -170,7 +159,6 @@
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
"@sentry/cli",
|
"@sentry/cli",
|
||||||
"cypress",
|
|
||||||
"esbuild",
|
"esbuild",
|
||||||
"puppeteer",
|
"puppeteer",
|
||||||
"vue-demi"
|
"vue-demi"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,7 +2,6 @@ import type {Directive} from 'vue'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
Cypress: object;
|
|
||||||
TESTING?: boolean;
|
TESTING?: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +13,7 @@ function isTestingEnabled(): boolean {
|
||||||
return import.meta.env.DEV || window.TESTING === true
|
return import.meta.env.DEV || window.TESTING === true
|
||||||
}
|
}
|
||||||
|
|
||||||
const cypressDirective = <Directive<HTMLElement,string>>{
|
const testIdDirective = <Directive<HTMLElement,string>>{
|
||||||
mounted(el, {arg, value}) {
|
mounted(el, {arg, value}) {
|
||||||
if (!isTestingEnabled()) {
|
if (!isTestingEnabled()) {
|
||||||
return
|
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 {createPinia} from 'pinia'
|
||||||
|
|
||||||
import cypress from '@/directives/cypress'
|
import testid from '@/directives/testid'
|
||||||
|
|
||||||
import FontAwesomeIcon from '@/components/misc/Icon'
|
import FontAwesomeIcon from '@/components/misc/Icon'
|
||||||
import XButton from '@/components/input/button.vue'
|
import XButton from '@/components/input/button.vue'
|
||||||
|
|
@ -19,7 +19,7 @@ export const setupVue3 = defineSetupVue3(({ app }) => {
|
||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
app.use(i18n)
|
app.use(i18n)
|
||||||
|
|
||||||
app.directive('cy', cypress)
|
app.directive('cy', testid)
|
||||||
|
|
||||||
app.component('Icon', FontAwesomeIcon)
|
app.component('Icon', FontAwesomeIcon)
|
||||||
app.component('XButton', XButton)
|
app.component('XButton', XButton)
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ import focus from '@/directives/focus'
|
||||||
import {vTooltip} from 'floating-vue'
|
import {vTooltip} from 'floating-vue'
|
||||||
import 'floating-vue/dist/style.css'
|
import 'floating-vue/dist/style.css'
|
||||||
import shortcut from '@/directives/shortcut'
|
import shortcut from '@/directives/shortcut'
|
||||||
import cypress from '@/directives/cypress'
|
import testid from '@/directives/testid'
|
||||||
|
|
||||||
// global components
|
// global components
|
||||||
import FontAwesomeIcon from '@/components/misc/Icon'
|
import FontAwesomeIcon from '@/components/misc/Icon'
|
||||||
|
|
@ -60,7 +60,7 @@ setLanguage(browserLanguage).then(() => {
|
||||||
app.directive('focus', focus)
|
app.directive('focus', focus)
|
||||||
app.directive('tooltip', vTooltip)
|
app.directive('tooltip', vTooltip)
|
||||||
app.directive('shortcut', shortcut)
|
app.directive('shortcut', shortcut)
|
||||||
app.directive('cy', cypress)
|
app.directive('cy', testid)
|
||||||
|
|
||||||
app.component('Icon', FontAwesomeIcon)
|
app.component('Icon', FontAwesomeIcon)
|
||||||
app.component('XButton', Button)
|
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 {ProjectFactory} from '../../factories/project'
|
||||||
import {createProjects} from './prepareProjects'
|
import {createProjects} from './prepareProjects'
|
||||||
import {BucketFactory} from '../../factories/bucket'
|
import {BucketFactory} from '../../factories/bucket'
|
||||||
|
import {createTasksWithPriorities, createTasksWithSearch} from '../../support/filterTestHelpers'
|
||||||
|
|
||||||
test.describe('Project View List', () => {
|
test.describe('Project View List', () => {
|
||||||
test('Should be an empty project', async ({authenticatedPage: page}) => {
|
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 > .single-task')).toBeVisible()
|
||||||
await expect(page.locator('ul.tasks > div > .subtask-nested')).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",
|
"env.config.d.ts",
|
||||||
"vite.config.*",
|
"vite.config.*",
|
||||||
"vitest.config.*",
|
"vitest.config.*",
|
||||||
"cypress.config.*"
|
"playwright.config.*"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue