mirror of
https://github.com/nextcloud/server.git
synced 2026-04-23 15:21:00 -04:00
Merge pull request #55747 from nextcloud/chore/migrate-cypress-vitest
refactor(test): migrate Cypress component test to vitest
This commit is contained in:
commit
0c1be89f1b
29 changed files with 1033 additions and 1181 deletions
4
.github/workflows/cypress.yml
vendored
4
.github/workflows/cypress.yml
vendored
|
|
@ -102,8 +102,8 @@ jobs:
|
|||
matrix:
|
||||
# Run multiple copies of the current job in parallel
|
||||
# Please increase the number or runners as your tests suite grows (0 based index for e2e tests)
|
||||
containers: ['component', 'setup', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
|
||||
# Hack as strategy.job-total includes the component and GitHub does not allow math expressions
|
||||
containers: ['setup', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
|
||||
# Hack as strategy.job-total includes the "setup" and GitHub does not allow math expressions
|
||||
# Always align this number with the total of e2e runners (max. index + 1)
|
||||
total-containers: [10]
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: CC0-1.0
|
||||
*/
|
||||
|
||||
export function setup() {
|
||||
process.env.TZ = 'UTC'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,5 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: CC0-1.0
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
import 'core-js/stable/index.js'
|
||||
|
|
|
|||
|
|
@ -1,123 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import RemoteShareDialog from './RemoteShareDialog.vue'
|
||||
|
||||
describe('RemoteShareDialog', () => {
|
||||
it('can be mounted', () => {
|
||||
cy.mount(RemoteShareDialog, {
|
||||
propsData: {
|
||||
owner: 'user123',
|
||||
name: 'my-photos',
|
||||
remote: 'nextcloud.local',
|
||||
passwordRequired: false,
|
||||
},
|
||||
})
|
||||
|
||||
cy.findByRole('dialog')
|
||||
.should('be.visible')
|
||||
.and('contain.text', 'user123@nextcloud.local')
|
||||
.and('contain.text', 'my-photos')
|
||||
cy.findByRole('button', { name: 'Cancel' })
|
||||
.should('be.visible')
|
||||
cy.findByRole('button', { name: /add remote share/i })
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('does not show password input if not enabled', () => {
|
||||
cy.mount(RemoteShareDialog, {
|
||||
propsData: {
|
||||
owner: 'user123',
|
||||
name: 'my-photos',
|
||||
remote: 'nextcloud.local',
|
||||
passwordRequired: false,
|
||||
},
|
||||
})
|
||||
|
||||
cy.findByRole('dialog')
|
||||
.should('be.visible')
|
||||
.find('input[type="password"]')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('emits true when accepted', () => {
|
||||
const onClose = cy.spy().as('onClose')
|
||||
|
||||
cy.mount(RemoteShareDialog, {
|
||||
listeners: {
|
||||
close: onClose,
|
||||
},
|
||||
propsData: {
|
||||
owner: 'user123',
|
||||
name: 'my-photos',
|
||||
remote: 'nextcloud.local',
|
||||
passwordRequired: false,
|
||||
},
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Cancel' }).click()
|
||||
cy.get('@onClose')
|
||||
.should('have.been.calledWith', false)
|
||||
})
|
||||
|
||||
it('show password input if needed', () => {
|
||||
cy.mount(RemoteShareDialog, {
|
||||
propsData: {
|
||||
owner: 'admin',
|
||||
name: 'secret-data',
|
||||
remote: 'nextcloud.local',
|
||||
passwordRequired: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.findByRole('dialog')
|
||||
.should('be.visible')
|
||||
.find('input[type="password"]')
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('emits the submitted password', () => {
|
||||
const onClose = cy.spy().as('onClose')
|
||||
|
||||
cy.mount(RemoteShareDialog, {
|
||||
listeners: {
|
||||
close: onClose,
|
||||
},
|
||||
propsData: {
|
||||
owner: 'admin',
|
||||
name: 'secret-data',
|
||||
remote: 'nextcloud.local',
|
||||
passwordRequired: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('input[type="password"]')
|
||||
.type('my password{enter}')
|
||||
cy.get('@onClose')
|
||||
.should('have.been.calledWith', true, 'my password')
|
||||
})
|
||||
|
||||
it('emits no password if cancelled', () => {
|
||||
const onClose = cy.spy().as('onClose')
|
||||
|
||||
cy.mount(RemoteShareDialog, {
|
||||
listeners: {
|
||||
close: onClose,
|
||||
},
|
||||
propsData: {
|
||||
owner: 'admin',
|
||||
name: 'secret-data',
|
||||
remote: 'nextcloud.local',
|
||||
passwordRequired: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('input[type="password"]')
|
||||
.type('my password')
|
||||
cy.findByRole('button', { name: 'Cancel' }).click()
|
||||
cy.get('@onClose')
|
||||
.should('have.been.calledWith', false)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { cleanup, fireEvent, render } from '@testing-library/vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import RemoteShareDialog from './RemoteShareDialog.vue'
|
||||
|
||||
describe('RemoteShareDialog', () => {
|
||||
beforeEach(cleanup)
|
||||
|
||||
it('can be mounted', async () => {
|
||||
const component = render(RemoteShareDialog, {
|
||||
props: {
|
||||
owner: 'user123',
|
||||
name: 'my-photos',
|
||||
remote: 'nextcloud.local',
|
||||
passwordRequired: false,
|
||||
},
|
||||
})
|
||||
|
||||
await expect(component.findByRole('dialog', { name: 'Remote share' })).resolves.not.toThrow()
|
||||
expect(component.getByRole('dialog').innerText).toContain(/my-photos from user123@nextcloud.local/)
|
||||
await expect(component.findByRole('button', { name: 'Cancel' })).resolves.not.toThrow()
|
||||
await expect(component.findByRole('button', { name: /Add remote share/ })).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it('does not show password input if not enabled', async () => {
|
||||
const component = render(RemoteShareDialog, {
|
||||
props: {
|
||||
owner: 'user123',
|
||||
name: 'my-photos',
|
||||
remote: 'nextcloud.local',
|
||||
passwordRequired: false,
|
||||
},
|
||||
})
|
||||
|
||||
await expect(component.findByLabelText('Remote share password')).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('emits true when accepted', () => {
|
||||
const onClose = vi.fn()
|
||||
|
||||
const component = render(RemoteShareDialog, {
|
||||
listeners: {
|
||||
close: onClose,
|
||||
},
|
||||
props: {
|
||||
owner: 'user123',
|
||||
name: 'my-photos',
|
||||
remote: 'nextcloud.local',
|
||||
passwordRequired: false,
|
||||
},
|
||||
})
|
||||
|
||||
component.getByRole('button', { name: 'Cancel' }).click()
|
||||
expect(onClose).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('show password input if needed', async () => {
|
||||
const component = render(RemoteShareDialog, {
|
||||
props: {
|
||||
owner: 'admin',
|
||||
name: 'secret-data',
|
||||
remote: 'nextcloud.local',
|
||||
passwordRequired: true,
|
||||
},
|
||||
})
|
||||
|
||||
await expect(component.findByLabelText('Remote share password')).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it('emits the submitted password', async () => {
|
||||
const onClose = vi.fn()
|
||||
|
||||
const component = render(RemoteShareDialog, {
|
||||
listeners: {
|
||||
close: onClose,
|
||||
},
|
||||
props: {
|
||||
owner: 'admin',
|
||||
name: 'secret-data',
|
||||
remote: 'nextcloud.local',
|
||||
passwordRequired: true,
|
||||
},
|
||||
})
|
||||
|
||||
const input = component.getByLabelText('Remote share password')
|
||||
await fireEvent.update(input, 'my password')
|
||||
component.getByRole('button', { name: 'Add remote share' }).click()
|
||||
expect(onClose).toHaveBeenCalledWith(true, 'my password')
|
||||
})
|
||||
|
||||
it('emits no password if cancelled', async () => {
|
||||
const onClose = vi.fn()
|
||||
|
||||
const component = render(RemoteShareDialog, {
|
||||
listeners: {
|
||||
close: onClose,
|
||||
},
|
||||
props: {
|
||||
owner: 'admin',
|
||||
name: 'secret-data',
|
||||
remote: 'nextcloud.local',
|
||||
passwordRequired: true,
|
||||
},
|
||||
})
|
||||
|
||||
const input = component.getByLabelText('Remote share password')
|
||||
await fireEvent.update(input, 'my password')
|
||||
component.getByRole('button', { name: 'Cancel' }).click()
|
||||
expect(onClose).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -35,8 +35,8 @@ const buttons = computed(() => [
|
|||
},
|
||||
{
|
||||
label: t('federatedfilesharing', 'Add remote share'),
|
||||
nativeType: props.passwordRequired ? 'submit' : undefined,
|
||||
type: 'primary',
|
||||
type: props.passwordRequired ? 'submit' : undefined,
|
||||
variant: 'primary',
|
||||
callback: () => emit('close', true, password.value),
|
||||
},
|
||||
])
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { defineComponent } from 'vue'
|
||||
import { useFileListWidth } from './useFileListWidth.ts'
|
||||
|
||||
const ComponentMock = defineComponent({
|
||||
template: '<div id="test-component" style="width: 100%;background: white;">{{ fileListWidth }}</div>',
|
||||
setup() {
|
||||
return {
|
||||
fileListWidth: useFileListWidth(),
|
||||
}
|
||||
},
|
||||
})
|
||||
const FileListMock = defineComponent({
|
||||
template: '<main id="app-content-vue" style="width: 100%;"><component-mock /></main>',
|
||||
components: {
|
||||
ComponentMock,
|
||||
},
|
||||
})
|
||||
|
||||
describe('composable: fileListWidth', () => {
|
||||
it('Has initial value', () => {
|
||||
cy.viewport(600, 400)
|
||||
|
||||
cy.mount(FileListMock, {})
|
||||
cy.get('#app-content-vue')
|
||||
.should('be.visible')
|
||||
.and('contain.text', '600')
|
||||
})
|
||||
|
||||
it('Is reactive to size change', () => {
|
||||
cy.viewport(600, 400)
|
||||
cy.mount(FileListMock)
|
||||
cy.get('#app-content-vue').should('contain.text', '600')
|
||||
|
||||
cy.viewport(800, 400)
|
||||
cy.screenshot()
|
||||
cy.get('#app-content-vue').should('contain.text', '800')
|
||||
})
|
||||
|
||||
it('Is reactive to style changes', () => {
|
||||
cy.viewport(600, 400)
|
||||
cy.mount(FileListMock)
|
||||
cy.get('#app-content-vue')
|
||||
.should('be.visible')
|
||||
.and('contain.text', '600')
|
||||
.invoke('attr', 'style', 'width: 100px')
|
||||
|
||||
cy.get('#app-content-vue')
|
||||
.should('contain.text', '100')
|
||||
})
|
||||
})
|
||||
80
apps/files/src/composables/useFileListWidth.spec.ts
Normal file
80
apps/files/src/composables/useFileListWidth.spec.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { cleanup, render } from '@testing-library/vue'
|
||||
import { configMocks, mockResizeObserver } from 'jsdom-testing-mocks'
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { defineComponent } from 'vue'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
let resizeObserver: ReturnType<typeof mockResizeObserver>
|
||||
|
||||
describe('composable: fileListWidth', () => {
|
||||
configMocks({ beforeAll, afterAll, beforeEach, afterEach })
|
||||
|
||||
beforeAll(() => {
|
||||
resizeObserver = mockResizeObserver()
|
||||
})
|
||||
|
||||
beforeEach(cleanup)
|
||||
|
||||
it('Has initial value', async () => {
|
||||
const { component } = await getFileList()
|
||||
expect(component.textContent).toBe('600')
|
||||
})
|
||||
|
||||
it('observes the file list element', async () => {
|
||||
const { fileList } = await getFileList()
|
||||
expect(resizeObserver.getObservedElements()).toContain(fileList)
|
||||
})
|
||||
|
||||
it('Is reactive to size change', async () => {
|
||||
const { component, fileList } = await getFileList()
|
||||
expect(component.textContent).toBe('600')
|
||||
expect(resizeObserver.getObservedElements()).toHaveLength(1)
|
||||
|
||||
resizeObserver.mockElementSize(fileList, { contentBoxSize: { inlineSize: 800, blockSize: 300 } })
|
||||
resizeObserver.resize(fileList)
|
||||
|
||||
// await rending
|
||||
await nextTick()
|
||||
expect(component.textContent).toBe('800')
|
||||
})
|
||||
})
|
||||
|
||||
async function getFileList() {
|
||||
const { useFileListWidth } = await import('./useFileListWidth.ts')
|
||||
|
||||
const ComponentMock = defineComponent({
|
||||
template: '<div data-testid="component" style="width: 100%;background: white;">{{ fileListWidth }}</div>',
|
||||
setup() {
|
||||
return {
|
||||
fileListWidth: useFileListWidth(),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const FileListMock = defineComponent({
|
||||
template: '<main id="app-content-vue" style="width: 100%;"><component-mock /></main>',
|
||||
components: {
|
||||
ComponentMock,
|
||||
},
|
||||
})
|
||||
|
||||
const root = render(FileListMock)
|
||||
const fileList = root.baseElement.querySelector('#app-content-vue') as HTMLElement
|
||||
|
||||
// mock initial size
|
||||
resizeObserver.mockElementSize(fileList, { contentBoxSize: { inlineSize: 600, blockSize: 200 } })
|
||||
resizeObserver.resize()
|
||||
// await rending
|
||||
await nextTick()
|
||||
|
||||
return {
|
||||
root,
|
||||
component: root.getByTestId('component'),
|
||||
fileList,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import DialogConfirmFileExtension from './DialogConfirmFileExtension.vue'
|
||||
import { useUserConfigStore } from '../store/userconfig.ts'
|
||||
|
||||
describe('DialogConfirmFileExtension', () => {
|
||||
it('renders with both extensions', () => {
|
||||
cy.mount(DialogConfirmFileExtension, {
|
||||
propsData: {
|
||||
oldExtension: '.old',
|
||||
newExtension: '.new',
|
||||
},
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
cy.findByRole('dialog')
|
||||
.as('dialog')
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('heading')
|
||||
.should('contain.text', 'Change file extension')
|
||||
cy.get('@dialog')
|
||||
.findByRole('checkbox', { name: /Do not show this dialog again/i })
|
||||
.should('exist')
|
||||
.and('not.be.checked')
|
||||
cy.get('@dialog')
|
||||
.findByRole('button', { name: 'Keep .old' })
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('button', { name: 'Use .new' })
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('renders without old extension', () => {
|
||||
cy.mount(DialogConfirmFileExtension, {
|
||||
propsData: {
|
||||
newExtension: '.new',
|
||||
},
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
cy.findByRole('dialog')
|
||||
.as('dialog')
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('button', { name: 'Keep without extension' })
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('button', { name: 'Use .new' })
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('renders without new extension', () => {
|
||||
cy.mount(DialogConfirmFileExtension, {
|
||||
propsData: {
|
||||
oldExtension: '.old',
|
||||
},
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
cy.findByRole('dialog')
|
||||
.as('dialog')
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('button', { name: 'Keep .old' })
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('button', { name: 'Remove extension' })
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('emits correct value on keep old', () => {
|
||||
cy.mount(DialogConfirmFileExtension, {
|
||||
propsData: {
|
||||
oldExtension: '.old',
|
||||
newExtension: '.new',
|
||||
},
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
}).as('component')
|
||||
|
||||
cy.findByRole('dialog')
|
||||
.as('dialog')
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('button', { name: 'Keep .old' })
|
||||
.click()
|
||||
cy.get('@component')
|
||||
.its('wrapper')
|
||||
.should((wrapper) => expect(wrapper.emitted('close')).to.eql([[false]]))
|
||||
})
|
||||
|
||||
it('emits correct value on use new', () => {
|
||||
cy.mount(DialogConfirmFileExtension, {
|
||||
propsData: {
|
||||
oldExtension: '.old',
|
||||
newExtension: '.new',
|
||||
},
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
}).as('component')
|
||||
|
||||
cy.findByRole('dialog')
|
||||
.as('dialog')
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('button', { name: 'Use .new' })
|
||||
.click()
|
||||
cy.get('@component')
|
||||
.its('wrapper')
|
||||
.should((wrapper) => expect(wrapper.emitted('close')).to.eql([[true]]))
|
||||
})
|
||||
|
||||
it('updates user config when checking the checkbox', () => {
|
||||
const pinia = createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})
|
||||
|
||||
cy.mount(DialogConfirmFileExtension, {
|
||||
propsData: {
|
||||
oldExtension: '.old',
|
||||
newExtension: '.new',
|
||||
},
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
},
|
||||
}).as('component')
|
||||
|
||||
cy.findByRole('dialog')
|
||||
.as('dialog')
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('checkbox', { name: /Do not show this dialog again/i })
|
||||
.check({ force: true })
|
||||
|
||||
cy.wrap(useUserConfigStore())
|
||||
.its('update')
|
||||
.should('have.been.calledWith', 'show_dialog_file_extension', false)
|
||||
})
|
||||
})
|
||||
132
apps/files/src/views/DialogConfirmFileExtension.spec.ts
Normal file
132
apps/files/src/views/DialogConfirmFileExtension.spec.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { cleanup, fireEvent, render } from '@testing-library/vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import DialogConfirmFileExtension from './DialogConfirmFileExtension.vue'
|
||||
import { useUserConfigStore } from '../store/userconfig.ts'
|
||||
|
||||
describe('DialogConfirmFileExtension', () => {
|
||||
beforeEach(cleanup)
|
||||
|
||||
it('renders with both extensions', async () => {
|
||||
const component = render(DialogConfirmFileExtension, {
|
||||
props: {
|
||||
oldExtension: '.old',
|
||||
newExtension: '.new',
|
||||
},
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
await expect(component.findByRole('dialog', { name: 'Change file extension' })).resolves.not.toThrow()
|
||||
expect((component.getByRole('checkbox', { name: /Do not show this dialog again/i }) as HTMLInputElement).checked).toBe(false)
|
||||
await expect(component.findByRole('button', { name: 'Keep .old' })).resolves.not.toThrow()
|
||||
await expect(component.findByRole('button', { name: 'Use .new' })).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it('renders without old extension', async () => {
|
||||
const component = render(DialogConfirmFileExtension, {
|
||||
props: {
|
||||
newExtension: '.new',
|
||||
},
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
await expect(component.findByRole('dialog', { name: 'Change file extension' })).resolves.not.toThrow()
|
||||
await expect(component.findByRole('button', { name: 'Keep without extension' })).resolves.not.toThrow()
|
||||
await expect(component.findByRole('button', { name: 'Use .new' })).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it('renders without new extension', async () => {
|
||||
const component = render(DialogConfirmFileExtension, {
|
||||
props: {
|
||||
oldExtension: '.old',
|
||||
},
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
await expect(component.findByRole('dialog', { name: 'Change file extension' })).resolves.not.toThrow()
|
||||
await expect(component.findByRole('button', { name: 'Keep .old' })).resolves.not.toThrow()
|
||||
await expect(component.findByRole('button', { name: 'Remove extension' })).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it('emits correct value on keep old', async () => {
|
||||
const onclose = vi.fn()
|
||||
const component = render(DialogConfirmFileExtension, {
|
||||
props: {
|
||||
oldExtension: '.old',
|
||||
newExtension: '.new',
|
||||
},
|
||||
listeners: {
|
||||
close: onclose,
|
||||
},
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
await fireEvent.click(component.getByRole('button', { name: 'Keep .old' }))
|
||||
expect(onclose).toHaveBeenCalledOnce()
|
||||
expect(onclose).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('emits correct value on use new', async () => {
|
||||
const onclose = vi.fn()
|
||||
const component = render(DialogConfirmFileExtension, {
|
||||
props: {
|
||||
oldExtension: '.old',
|
||||
newExtension: '.new',
|
||||
},
|
||||
listeners: {
|
||||
close: onclose,
|
||||
},
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
await fireEvent.click(component.getByRole('button', { name: 'Use .new' }))
|
||||
expect(onclose).toHaveBeenCalledOnce()
|
||||
expect(onclose).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('updates user config when checking the checkbox', async () => {
|
||||
const pinia = createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
})
|
||||
|
||||
const component = render(DialogConfirmFileExtension, {
|
||||
props: {
|
||||
oldExtension: '.old',
|
||||
newExtension: '.new',
|
||||
},
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
},
|
||||
})
|
||||
|
||||
await fireEvent.click(component.getByRole('checkbox', { name: /Do not show this dialog again/i }))
|
||||
const store = useUserConfigStore()
|
||||
expect(store.update).toHaveBeenCalledOnce()
|
||||
expect(store.update).toHaveBeenCalledWith('show_dialog_file_extension', false)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,262 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Folder, Navigation } from '@nextcloud/files'
|
||||
|
||||
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
|
||||
import { getNavigation, View } from '@nextcloud/files'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import NavigationView from './FilesNavigation.vue'
|
||||
import router from '../router/router.ts'
|
||||
import RouterService from '../services/RouterService.ts'
|
||||
import { useViewConfigStore } from '../store/viewConfig.ts'
|
||||
|
||||
function resetNavigation() {
|
||||
const nav = getNavigation()
|
||||
;[...nav.views].forEach(({ id }) => nav.remove(id))
|
||||
nav.setActive(null)
|
||||
}
|
||||
|
||||
function createView(id: string, name: string, parent?: string) {
|
||||
return new View({
|
||||
id,
|
||||
name,
|
||||
getContents: async () => ({ folder: {} as Folder, contents: [] }),
|
||||
icon: FolderSvg,
|
||||
order: 1,
|
||||
parent,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function mockWindow() {
|
||||
window.OCP ??= {}
|
||||
window.OCP.Files ??= {}
|
||||
window.OCP.Files.Router = new RouterService(router)
|
||||
}
|
||||
|
||||
describe('Navigation renders', () => {
|
||||
before(async () => {
|
||||
delete window._nc_navigation
|
||||
mockWindow()
|
||||
getNavigation().register(createView('files', 'Files'))
|
||||
await router.replace({ name: 'filelist', params: { view: 'files' } })
|
||||
|
||||
cy.mockInitialState('files', 'storageStats', {
|
||||
used: 1000 * 1000 * 1000,
|
||||
quota: -1,
|
||||
})
|
||||
})
|
||||
|
||||
after(() => cy.unmockInitialState())
|
||||
|
||||
it('renders', () => {
|
||||
cy.mount(NavigationView, {
|
||||
router,
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('[data-cy-files-navigation]').should('be.visible')
|
||||
cy.get('[data-cy-files-navigation-settings-quota]').should('be.visible')
|
||||
cy.get('[data-cy-files-navigation-settings-button]').should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Navigation API', () => {
|
||||
let Navigation: Navigation
|
||||
|
||||
before(async () => {
|
||||
delete window._nc_navigation
|
||||
Navigation = getNavigation()
|
||||
mockWindow()
|
||||
|
||||
await router.replace({ name: 'filelist', params: { view: 'files' } })
|
||||
})
|
||||
|
||||
beforeEach(() => resetNavigation())
|
||||
|
||||
it('Check API entries rendering', () => {
|
||||
Navigation.register(createView('files', 'Files'))
|
||||
console.warn(Navigation.views)
|
||||
|
||||
cy.mount(NavigationView, {
|
||||
router,
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('[data-cy-files-navigation]').should('be.visible')
|
||||
cy.get('[data-cy-files-navigation-item]').should('have.length', 1)
|
||||
cy.get('[data-cy-files-navigation-item="files"]').should('be.visible')
|
||||
cy.get('[data-cy-files-navigation-item="files"]').should('contain.text', 'Files')
|
||||
})
|
||||
|
||||
it('Adds a new entry and render', () => {
|
||||
Navigation.register(createView('files', 'Files'))
|
||||
Navigation.register(createView('sharing', 'Sharing'))
|
||||
|
||||
cy.mount(NavigationView, {
|
||||
router,
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('[data-cy-files-navigation]').should('be.visible')
|
||||
cy.get('[data-cy-files-navigation-item]').should('have.length', 2)
|
||||
cy.get('[data-cy-files-navigation-item="sharing"]').should('be.visible')
|
||||
cy.get('[data-cy-files-navigation-item="sharing"]').should('contain.text', 'Sharing')
|
||||
})
|
||||
|
||||
it('Adds a new children, render and open menu', () => {
|
||||
Navigation.register(createView('files', 'Files'))
|
||||
Navigation.register(createView('sharing', 'Sharing'))
|
||||
Navigation.register(createView('sharingin', 'Shared with me', 'sharing'))
|
||||
|
||||
cy.mount(NavigationView, {
|
||||
router,
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
cy.wrap(useViewConfigStore()).as('viewConfigStore')
|
||||
|
||||
cy.get('[data-cy-files-navigation]').should('be.visible')
|
||||
cy.get('[data-cy-files-navigation-item]').should('have.length', 3)
|
||||
|
||||
// Toggle the sharing entry children
|
||||
cy.get('[data-cy-files-navigation-item="sharing"] button.icon-collapse').should('exist')
|
||||
cy.get('[data-cy-files-navigation-item="sharing"] button.icon-collapse').click({ force: true })
|
||||
|
||||
// Expect store update to be called
|
||||
cy.get('@viewConfigStore').its('update').should('have.been.calledWith', 'sharing', 'expanded', true)
|
||||
|
||||
// Validate children
|
||||
cy.get('[data-cy-files-navigation-item="sharingin"]').should('be.visible')
|
||||
cy.get('[data-cy-files-navigation-item="sharingin"]').should('contain.text', 'Shared with me')
|
||||
|
||||
// Toggle the sharing entry children 🇦again
|
||||
cy.get('[data-cy-files-navigation-item="sharing"] button.icon-collapse').click({ force: true })
|
||||
cy.get('[data-cy-files-navigation-item="sharingin"]').should('not.be.visible')
|
||||
|
||||
// Expect store update to be called
|
||||
cy.get('@viewConfigStore').its('update').should('have.been.calledWith', 'sharing', 'expanded', false)
|
||||
})
|
||||
|
||||
it('Throws when adding a duplicate entry', () => {
|
||||
Navigation.register(createView('files', 'Files'))
|
||||
expect(() => Navigation.register(createView('files', 'Files')))
|
||||
.to.throw('View id files is already registered')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Quota rendering', () => {
|
||||
before(async () => {
|
||||
delete window._nc_navigation
|
||||
mockWindow()
|
||||
getNavigation().register(createView('files', 'Files'))
|
||||
await router.replace({ name: 'filelist', params: { view: 'files' } })
|
||||
})
|
||||
|
||||
afterEach(() => cy.unmockInitialState())
|
||||
|
||||
it('Unknown quota', () => {
|
||||
cy.mount(NavigationView, {
|
||||
router,
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('[data-cy-files-navigation-settings-quota]').should('not.exist')
|
||||
})
|
||||
|
||||
it('Unlimited quota', () => {
|
||||
cy.mockInitialState('files', 'storageStats', {
|
||||
used: 1024 * 1024 * 1024,
|
||||
quota: -1,
|
||||
total: 50 * 1024 * 1024 * 1024,
|
||||
})
|
||||
|
||||
cy.mount(NavigationView, {
|
||||
router,
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('[data-cy-files-navigation-settings-quota]').should('be.visible')
|
||||
cy.get('[data-cy-files-navigation-settings-quota]').should('contain.text', '1 GB used')
|
||||
cy.get('[data-cy-files-navigation-settings-quota] progress').should('not.exist')
|
||||
})
|
||||
|
||||
it('Non-reached quota', () => {
|
||||
cy.mockInitialState('files', 'storageStats', {
|
||||
used: 1024 * 1024 * 1024,
|
||||
quota: 5 * 1024 * 1024 * 1024,
|
||||
total: 5 * 1024 * 1024 * 1024,
|
||||
relative: 20, // percent
|
||||
})
|
||||
|
||||
cy.mount(NavigationView, {
|
||||
router,
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('[data-cy-files-navigation-settings-quota]').should('be.visible')
|
||||
cy.get('[data-cy-files-navigation-settings-quota]').should('contain.text', '1 GB of 5 GB used')
|
||||
cy.get('[data-cy-files-navigation-settings-quota] progress')
|
||||
.should('exist')
|
||||
.and('have.attr', 'value', '20')
|
||||
})
|
||||
|
||||
it('Reached quota', () => {
|
||||
cy.mockInitialState('files', 'storageStats', {
|
||||
used: 5 * 1024 * 1024 * 1024,
|
||||
quota: 1024 * 1024 * 1024,
|
||||
total: 1024 * 1024 * 1024,
|
||||
relative: 500, // percent
|
||||
})
|
||||
|
||||
cy.mount(NavigationView, {
|
||||
router,
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('[data-cy-files-navigation-settings-quota]').should('be.visible')
|
||||
cy.get('[data-cy-files-navigation-settings-quota]').should('contain.text', '5 GB of 1 GB used')
|
||||
cy.get('[data-cy-files-navigation-settings-quota] progress')
|
||||
.should('exist')
|
||||
.and('have.attr', 'value', '100') // progress max is 100
|
||||
})
|
||||
})
|
||||
286
apps/files/src/views/FilesNavigation.spec.ts
Normal file
286
apps/files/src/views/FilesNavigation.spec.ts
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Folder, Navigation } from '@nextcloud/files'
|
||||
|
||||
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
|
||||
import { getNavigation, View } from '@nextcloud/files'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { cleanup, fireEvent, getAllByRole, render } from '@testing-library/vue'
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import NavigationView from './FilesNavigation.vue'
|
||||
import router from '../router/router.ts'
|
||||
import RouterService from '../services/RouterService.ts'
|
||||
import { useViewConfigStore } from '../store/viewConfig.ts'
|
||||
|
||||
afterEach(() => removeInitialState())
|
||||
beforeAll(async () => {
|
||||
Object.defineProperty(document.documentElement, 'clientWidth', { value: 1920 })
|
||||
await fireEvent.resize(window)
|
||||
})
|
||||
|
||||
describe('Navigation', () => {
|
||||
beforeEach(cleanup)
|
||||
|
||||
beforeEach(async () => {
|
||||
delete window._nc_navigation
|
||||
mockWindow()
|
||||
getNavigation().register(createView('files', 'Files'))
|
||||
await router.replace({ name: 'filelist', params: { view: 'files' } })
|
||||
})
|
||||
|
||||
it('renders navigation with settings button and search', async () => {
|
||||
const component = render(NavigationView, {
|
||||
router,
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
// see the navigation
|
||||
await expect(component.findByRole('navigation', { name: 'Files' })).resolves.not.toThrow()
|
||||
// see the search box
|
||||
await expect(component.findByRole('searchbox', { name: /Search here/ })).resolves.not.toThrow()
|
||||
// see the settings entry
|
||||
await expect(component.findByRole('link', { name: /Files settings/ })).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it('renders no quota without storage stats', () => {
|
||||
const component = render(NavigationView, {
|
||||
router,
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
expect(component.baseElement.querySelector('[data-cy-files-navigation-settings-quota]')).toBeNull()
|
||||
})
|
||||
|
||||
it('Unlimited quota shows used storage but no progressbar', async () => {
|
||||
mockInitialState('files', 'storageStats', {
|
||||
used: 1024 * 1024 * 1024,
|
||||
quota: -1,
|
||||
total: 50 * 1024 * 1024 * 1024,
|
||||
})
|
||||
|
||||
const component = render(NavigationView, {
|
||||
router,
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
expect(component.baseElement.querySelector('[data-cy-files-navigation-settings-quota]')).not.toBeNull()
|
||||
|
||||
await expect(component.findByText('1 GB used')).resolves.not.toThrow()
|
||||
await expect(component.findByRole('progressbar')).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('Non-reached quota shows stats and progress', async () => {
|
||||
mockInitialState('files', 'storageStats', {
|
||||
used: 1024 * 1024 * 1024,
|
||||
quota: 5 * 1024 * 1024 * 1024,
|
||||
total: 5 * 1024 * 1024 * 1024,
|
||||
relative: 20, // percent
|
||||
})
|
||||
|
||||
const component = render(NavigationView, {
|
||||
router,
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
await expect(component.findByText('1 GB of 5 GB used')).resolves.not.toThrow()
|
||||
await expect(component.findByRole('progressbar')).resolves.not.toThrow()
|
||||
expect((component.getByRole('progressbar') as HTMLProgressElement).value).toBe(20)
|
||||
})
|
||||
|
||||
it('Reached quota', async () => {
|
||||
mockInitialState('files', 'storageStats', {
|
||||
used: 5 * 1024 * 1024 * 1024,
|
||||
quota: 1024 * 1024 * 1024,
|
||||
total: 1024 * 1024 * 1024,
|
||||
relative: 500, // percent
|
||||
})
|
||||
|
||||
const component = render(NavigationView, {
|
||||
router,
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
await expect(component.findByText('5 GB of 1 GB used')).resolves.not.toThrow()
|
||||
await expect(component.findByRole('progressbar')).resolves.not.toThrow()
|
||||
expect((component.getByRole('progressbar') as HTMLProgressElement).value).toBe(100)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Navigation API', () => {
|
||||
let Navigation: Navigation
|
||||
|
||||
beforeEach(async () => {
|
||||
delete window._nc_navigation
|
||||
Navigation = getNavigation()
|
||||
mockWindow()
|
||||
|
||||
await router.replace({ name: 'filelist', params: { view: 'files' } })
|
||||
})
|
||||
|
||||
beforeEach(resetNavigation)
|
||||
beforeEach(cleanup)
|
||||
|
||||
it('Check API entries rendering', async () => {
|
||||
Navigation.register(createView('files', 'Files'))
|
||||
|
||||
const component = render(NavigationView, {
|
||||
router,
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
// see the navigation
|
||||
await expect(component.findByRole('navigation', { name: 'Files' })).resolves.not.toThrow()
|
||||
// see the views
|
||||
await expect(component.findByRole('list', { name: 'Views' })).resolves.not.toThrow()
|
||||
// see the entry
|
||||
await expect(component.findByRole('link', { name: 'Files' })).resolves.not.toThrow()
|
||||
// see that the entry has all props
|
||||
const entry = component.getByRole('link', { name: 'Files' })
|
||||
expect(entry.getAttribute('href')).toMatch(/\/apps\/files\/files$/)
|
||||
expect(entry.getAttribute('aria-current')).toBe('page')
|
||||
expect(entry.getAttribute('title')).toBe('Files')
|
||||
})
|
||||
|
||||
it('Adds a new entry and render', async () => {
|
||||
Navigation.register(createView('files', 'Files'))
|
||||
Navigation.register(createView('sharing', 'Sharing'))
|
||||
|
||||
const component = render(NavigationView, {
|
||||
router,
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const list = component.getByRole('list', { name: 'Views' })
|
||||
expect(getAllByRole(list, 'listitem')).toHaveLength(2)
|
||||
|
||||
await expect(component.findByRole('link', { name: 'Files' })).resolves.not.toThrow()
|
||||
await expect(component.findByRole('link', { name: 'Sharing' })).resolves.not.toThrow()
|
||||
// see that the entry has all props
|
||||
const entry = component.getByRole('link', { name: 'Sharing' })
|
||||
expect(entry.getAttribute('href')).toMatch(/\/apps\/files\/sharing$/)
|
||||
expect(entry.getAttribute('aria-current')).toBeNull()
|
||||
expect(entry.getAttribute('title')).toBe('Sharing')
|
||||
})
|
||||
|
||||
it('Adds a new children, render and open menu', async () => {
|
||||
Navigation.register(createView('files', 'Files'))
|
||||
Navigation.register(createView('sharing', 'Sharing'))
|
||||
Navigation.register(createView('sharingin', 'Shared with me', 'sharing'))
|
||||
|
||||
const component = render(NavigationView, {
|
||||
router,
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
const viewConfigStore = useViewConfigStore()
|
||||
|
||||
const list = component.getByRole('list', { name: 'Views' })
|
||||
expect(getAllByRole(list, 'listitem')).toHaveLength(3)
|
||||
|
||||
// Toggle the sharing entry children
|
||||
const entry = component.getByRole('link', { name: 'Sharing' })
|
||||
expect(entry.getAttribute('aria-expanded')).toBe('false')
|
||||
await fireEvent.click(component.getByRole('button', { name: 'Open menu' }))
|
||||
expect(entry.getAttribute('aria-expanded')).toBe('true')
|
||||
|
||||
// Expect store update to be called
|
||||
expect(viewConfigStore.update).toHaveBeenCalled()
|
||||
expect(viewConfigStore.update).toHaveBeenCalledWith('sharing', 'expanded', true)
|
||||
|
||||
// Validate children
|
||||
await expect(component.findByRole('link', { name: 'Shared with me' })).resolves.not.toThrow()
|
||||
|
||||
await fireEvent.click(component.getByRole('button', { name: 'Collapse menu' }))
|
||||
// Expect store update to be called
|
||||
expect(viewConfigStore.update).toHaveBeenCalledWith('sharing', 'expanded', false)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Remove the mocked initial state
|
||||
*/
|
||||
function removeInitialState(): void {
|
||||
document.querySelectorAll('input[type="hidden"]').forEach((el) => {
|
||||
el.remove()
|
||||
})
|
||||
// clear the cache
|
||||
delete globalThis._nc_initial_state
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to mock an initial state value
|
||||
* @param app - The app
|
||||
* @param key - The key
|
||||
* @param value - The value
|
||||
*/
|
||||
function mockInitialState(app: string, key: string, value: unknown): void {
|
||||
const el = document.createElement('input')
|
||||
el.value = btoa(JSON.stringify(value))
|
||||
el.id = `initial-state-${app}-${key}`
|
||||
el.type = 'hidden'
|
||||
|
||||
document.head.appendChild(el)
|
||||
}
|
||||
|
||||
function resetNavigation() {
|
||||
const nav = getNavigation()
|
||||
;[...nav.views].forEach(({ id }) => nav.remove(id))
|
||||
nav.setActive(null)
|
||||
}
|
||||
|
||||
function createView(id: string, name: string, parent?: string) {
|
||||
return new View({
|
||||
id,
|
||||
name,
|
||||
getContents: async () => ({ folder: {} as Folder, contents: [] }),
|
||||
icon: FolderSvg,
|
||||
order: 1,
|
||||
parent,
|
||||
})
|
||||
}
|
||||
|
||||
function mockWindow() {
|
||||
window.OCP ??= {}
|
||||
window.OCP.Files ??= {}
|
||||
window.OCP.Files.Router = new RouterService(router)
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import Markdown from './Markdown.vue'
|
||||
|
||||
describe('Markdown component', () => {
|
||||
it('renders links', () => {
|
||||
cy.mount(Markdown, {
|
||||
propsData: {
|
||||
text: 'This is [a link](http://example.com)!',
|
||||
},
|
||||
})
|
||||
|
||||
cy.contains('This is')
|
||||
.find('a')
|
||||
.should('exist')
|
||||
.and('have.attr', 'href', 'http://example.com')
|
||||
.and('contain.text', 'a link')
|
||||
})
|
||||
|
||||
it('renders headings', () => {
|
||||
cy.mount(Markdown, {
|
||||
propsData: {
|
||||
text: '# level 1\nText\n## level 2\nText\n### level 3\nText\n#### level 4\nText\n##### level 5\nText\n###### level 6\nText\n',
|
||||
},
|
||||
})
|
||||
|
||||
for (let level = 1; level <= 6; level++) {
|
||||
cy.contains(`h${level}`, `level ${level}`)
|
||||
.should('be.visible')
|
||||
}
|
||||
})
|
||||
|
||||
it('can limit headings', () => {
|
||||
cy.mount(Markdown, {
|
||||
propsData: {
|
||||
text: '# level 1\nText\n## level 2\nText\n### level 3\nText\n#### level 4\nText\n##### level 5\nText\n###### level 6\nText\n',
|
||||
minHeading: 4,
|
||||
},
|
||||
})
|
||||
|
||||
cy.get('h1').should('not.exist')
|
||||
cy.get('h2').should('not.exist')
|
||||
cy.get('h3').should('not.exist')
|
||||
cy.get('h4')
|
||||
.should('exist')
|
||||
.and('contain.text', 'level 1')
|
||||
cy.get('h5')
|
||||
.should('exist')
|
||||
.and('contain.text', 'level 2')
|
||||
cy.contains('h6', 'level 3').should('exist')
|
||||
cy.contains('h6', 'level 4').should('exist')
|
||||
cy.contains('h6', 'level 5').should('exist')
|
||||
cy.contains('h6', 'level 6').should('exist')
|
||||
})
|
||||
})
|
||||
58
apps/settings/src/components/Markdown.spec.ts
Normal file
58
apps/settings/src/components/Markdown.spec.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { cleanup, render } from '@testing-library/vue'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import Markdown from './Markdown.vue'
|
||||
|
||||
describe('Markdown component', () => {
|
||||
beforeEach(cleanup)
|
||||
|
||||
it('renders links', () => {
|
||||
const component = render(Markdown, {
|
||||
props: {
|
||||
text: 'This is [a link](http://example.com)!',
|
||||
},
|
||||
})
|
||||
|
||||
const link = component.getByRole('link')
|
||||
expect(link).toBeInstanceOf(HTMLAnchorElement)
|
||||
expect(link.getAttribute('href')).toBe('http://example.com')
|
||||
expect(link.textContent).toBe('a link')
|
||||
})
|
||||
|
||||
it('renders headings', () => {
|
||||
const component = render(Markdown, {
|
||||
props: {
|
||||
text: '# level 1\nText\n## level 2\nText\n### level 3\nText\n#### level 4\nText\n##### level 5\nText\n###### level 6\nText\n',
|
||||
},
|
||||
})
|
||||
|
||||
for (let level = 1; level <= 6; level++) {
|
||||
const heading = component.getByRole('heading', { level })
|
||||
expect(heading.textContent).toBe(`level ${level}`)
|
||||
}
|
||||
})
|
||||
|
||||
it('can limit headings', async () => {
|
||||
const component = render(Markdown, {
|
||||
props: {
|
||||
text: '# level 1\nText\n## level 2\nText\n### level 3\nText\n#### level 4\nText\n##### level 5\nText\n###### level 6\nText\n',
|
||||
minHeading: 4,
|
||||
},
|
||||
})
|
||||
|
||||
await expect(component.findByRole('heading', { level: 1 })).rejects.toThrow()
|
||||
await expect(component.findByRole('heading', { level: 2 })).rejects.toThrow()
|
||||
await expect(component.findByRole('heading', { level: 3 })).rejects.toThrow()
|
||||
|
||||
expect(component.getByRole('heading', { level: 4 }).textContent).toBe('level 1')
|
||||
expect(component.getByRole('heading', { level: 5 }).textContent).toBe('level 2')
|
||||
await expect(component.findByRole('heading', { level: 6, name: 'level 3' })).resolves.not.toThrow()
|
||||
await expect(component.findByRole('heading', { level: 6, name: 'level 4' })).resolves.not.toThrow()
|
||||
await expect(component.findByRole('heading', { level: 6, name: 'level 5' })).resolves.not.toThrow()
|
||||
await expect(component.findByRole('heading', { level: 6, name: 'level 6' })).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
|
|
@ -1,377 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { SetupConfig, SetupLinks } from '../install.ts'
|
||||
|
||||
import SetupView from './Setup.vue'
|
||||
|
||||
import '../../css/guest.css'
|
||||
|
||||
const defaultConfig = Object.freeze({
|
||||
adminlogin: '',
|
||||
adminpass: '',
|
||||
dbuser: '',
|
||||
dbpass: '',
|
||||
dbname: '',
|
||||
dbtablespace: '',
|
||||
dbhost: '',
|
||||
dbtype: '',
|
||||
databases: {
|
||||
sqlite: 'SQLite',
|
||||
mysql: 'MySQL/MariaDB',
|
||||
pgsql: 'PostgreSQL',
|
||||
},
|
||||
directory: '',
|
||||
hasAutoconfig: false,
|
||||
htaccessWorking: true,
|
||||
serverRoot: '/var/www/html',
|
||||
errors: [],
|
||||
}) as SetupConfig
|
||||
|
||||
const links = {
|
||||
adminInstall: 'https://docs.nextcloud.com/server/32/go.php?to=admin-install',
|
||||
adminSourceInstall: 'https://docs.nextcloud.com/server/32/go.php?to=admin-source_install',
|
||||
adminDBConfiguration: 'https://docs.nextcloud.com/server/32/go.php?to=admin-db-configuration',
|
||||
} as SetupLinks
|
||||
|
||||
describe('Default setup page', () => {
|
||||
beforeEach(() => {
|
||||
cy.mockInitialState('core', 'links', links)
|
||||
})
|
||||
|
||||
afterEach(() => cy.unmockInitialState())
|
||||
|
||||
it('Renders default config', () => {
|
||||
cy.mockInitialState('core', 'config', defaultConfig)
|
||||
cy.mount(SetupView)
|
||||
|
||||
cy.get('[data-cy-setup-form]').scrollIntoView()
|
||||
cy.get('[data-cy-setup-form]').should('be.visible')
|
||||
|
||||
// Single note is the footer help
|
||||
cy.get('[data-cy-setup-form-note]')
|
||||
.should('have.length', 1)
|
||||
.should('be.visible')
|
||||
cy.get('[data-cy-setup-form-note]').should('contain', 'See the documentation')
|
||||
|
||||
// DB radio selectors
|
||||
cy.get('[data-cy-setup-form-field^="dbtype"]')
|
||||
.should('exist')
|
||||
.find('input')
|
||||
.should('be.checked')
|
||||
|
||||
cy.get('[data-cy-setup-form-field="dbtype-mysql"]').should('exist')
|
||||
cy.get('[data-cy-setup-form-field="dbtype-pgsql"]').should('exist')
|
||||
cy.get('[data-cy-setup-form-field="dbtype-oci"]').should('not.exist')
|
||||
|
||||
// Sqlite warning
|
||||
cy.get('[data-cy-setup-form-db-note="sqlite"]')
|
||||
.should('be.visible')
|
||||
|
||||
// admin login, password, data directory and 3 DB radio selectors
|
||||
cy.get('[data-cy-setup-form-field]')
|
||||
.should('be.visible')
|
||||
.should('have.length', 6)
|
||||
})
|
||||
|
||||
it('Renders single DB sqlite', () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
databases: {
|
||||
sqlite: 'SQLite',
|
||||
},
|
||||
}
|
||||
cy.mockInitialState('core', 'config', config)
|
||||
cy.mount(SetupView)
|
||||
|
||||
cy.get('[data-cy-setup-form-field^="dbtype"]')
|
||||
.should('exist')
|
||||
.should('not.be.visible')
|
||||
.find('input')
|
||||
.should('be.checked')
|
||||
|
||||
cy.get('[data-cy-setup-form-field="dbtype-sqlite"]').should('exist')
|
||||
|
||||
// Two warnings: sqlite and single db support
|
||||
cy.get('[data-cy-setup-form-db-note="sqlite"]')
|
||||
.should('be.visible')
|
||||
cy.get('[data-cy-setup-form-db-note="single-db"]')
|
||||
.should('be.visible')
|
||||
|
||||
// Admin login, password, data directory and db type
|
||||
cy.get('[data-cy-setup-form-field]')
|
||||
.should('be.visible')
|
||||
.should('have.length', 4)
|
||||
})
|
||||
|
||||
it('Renders single DB mysql', () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
databases: {
|
||||
mysql: 'MySQL/MariaDB',
|
||||
},
|
||||
}
|
||||
cy.mockInitialState('core', 'config', config)
|
||||
cy.mount(SetupView)
|
||||
|
||||
cy.get('[data-cy-setup-form-field^="dbtype"]')
|
||||
.should('exist')
|
||||
.should('not.be.visible')
|
||||
.find('input')
|
||||
.should('be.checked')
|
||||
|
||||
// Single db support warning
|
||||
cy.get('[data-cy-setup-form-db-note="single-db"]')
|
||||
.should('be.visible')
|
||||
.invoke('html')
|
||||
.should('contains', links.adminSourceInstall)
|
||||
|
||||
// No SQLite warning
|
||||
cy.get('[data-cy-setup-form-db-note="sqlite"]')
|
||||
.should('not.exist')
|
||||
|
||||
// Admin login, password, data directory, db type, db user,
|
||||
// db password, db name and db host
|
||||
cy.get('[data-cy-setup-form-field]')
|
||||
.should('be.visible')
|
||||
.should('have.length', 8)
|
||||
})
|
||||
|
||||
it('Changes fields from sqlite to mysql then oci', () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
databases: {
|
||||
sqlite: 'SQLite',
|
||||
mysql: 'MySQL/MariaDB',
|
||||
pgsql: 'PostgreSQL',
|
||||
oci: 'Oracle',
|
||||
},
|
||||
}
|
||||
cy.mockInitialState('core', 'config', config)
|
||||
cy.mount(SetupView)
|
||||
|
||||
// SQLite selected
|
||||
cy.get('[data-cy-setup-form-field="dbtype-sqlite"]')
|
||||
.should('be.visible')
|
||||
.find('input')
|
||||
.should('be.checked')
|
||||
|
||||
// Admin login, password, data directory and 4 DB radio selectors
|
||||
cy.get('[data-cy-setup-form-field]')
|
||||
.should('be.visible')
|
||||
.should('have.length', 7)
|
||||
|
||||
// Change to MySQL
|
||||
cy.get('[data-cy-setup-form-field="dbtype-mysql"]').click()
|
||||
cy.get('[data-cy-setup-form-field="dbtype-mysql"] input').should('be.checked')
|
||||
|
||||
// Admin login, password, data directory, db user, db password,
|
||||
// db name, db host and 4 DB radio selectors
|
||||
cy.get('[data-cy-setup-form-field]')
|
||||
.should('be.visible')
|
||||
.should('have.length', 11)
|
||||
|
||||
// Change to Oracle
|
||||
cy.get('[data-cy-setup-form-field="dbtype-oci"]').click()
|
||||
cy.get('[data-cy-setup-form-field="dbtype-oci"] input').should('be.checked')
|
||||
|
||||
// Admin login, password, data directory, db user, db password,
|
||||
// db name, db table space, db host and 4 DB radio selectors
|
||||
cy.get('[data-cy-setup-form-field]')
|
||||
.should('be.visible')
|
||||
.should('have.length', 12)
|
||||
cy.get('[data-cy-setup-form-field="dbtablespace"]')
|
||||
.should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Setup page with errors and warning', () => {
|
||||
beforeEach(() => {
|
||||
cy.mockInitialState('core', 'links', links)
|
||||
})
|
||||
|
||||
afterEach(() => cy.unmockInitialState())
|
||||
|
||||
it('Renders error from backend', () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
errors: [
|
||||
{
|
||||
error: 'Error message',
|
||||
hint: 'Error hint',
|
||||
},
|
||||
],
|
||||
}
|
||||
cy.mockInitialState('core', 'config', config)
|
||||
cy.mount(SetupView)
|
||||
|
||||
// Error message and hint
|
||||
cy.get('[data-cy-setup-form-note="error"]')
|
||||
.should('be.visible')
|
||||
.should('have.length', 1)
|
||||
.should('contain', 'Error message')
|
||||
.should('contain', 'Error hint')
|
||||
})
|
||||
|
||||
it('Renders errors from backend', () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
errors: [
|
||||
'Error message 1',
|
||||
{
|
||||
error: 'Error message',
|
||||
hint: 'Error hint',
|
||||
},
|
||||
],
|
||||
}
|
||||
cy.mockInitialState('core', 'config', config)
|
||||
cy.mount(SetupView)
|
||||
|
||||
// Error message and hint
|
||||
cy.get('[data-cy-setup-form-note="error"]')
|
||||
.should('be.visible')
|
||||
.should('have.length', 2)
|
||||
cy.get('[data-cy-setup-form-note="error"]').eq(0)
|
||||
.should('contain', 'Error message 1')
|
||||
cy.get('[data-cy-setup-form-note="error"]').eq(1)
|
||||
.should('contain', 'Error message')
|
||||
.should('contain', 'Error hint')
|
||||
})
|
||||
|
||||
it('Renders all the submitted fields on error', () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
adminlogin: 'admin',
|
||||
adminpass: 'password',
|
||||
dbname: 'nextcloud',
|
||||
dbtype: 'mysql',
|
||||
dbuser: 'nextcloud',
|
||||
dbpass: 'password',
|
||||
dbhost: 'localhost',
|
||||
directory: '/var/www/html/nextcloud',
|
||||
} as SetupConfig
|
||||
cy.mockInitialState('core', 'config', config)
|
||||
cy.mount(SetupView)
|
||||
|
||||
cy.get('input[data-cy-setup-form-field="adminlogin"]')
|
||||
.should('have.value', 'admin')
|
||||
cy.get('input[data-cy-setup-form-field="adminpass"]')
|
||||
.should('have.value', 'password')
|
||||
cy.get('[data-cy-setup-form-field="dbtype-mysql"] input')
|
||||
.should('be.checked')
|
||||
cy.get('input[data-cy-setup-form-field="dbname"]')
|
||||
.should('have.value', 'nextcloud')
|
||||
cy.get('input[data-cy-setup-form-field="dbuser"]')
|
||||
.should('have.value', 'nextcloud')
|
||||
cy.get('input[data-cy-setup-form-field="dbpass"]')
|
||||
.should('have.value', 'password')
|
||||
cy.get('input[data-cy-setup-form-field="dbhost"]')
|
||||
.should('have.value', 'localhost')
|
||||
cy.get('input[data-cy-setup-form-field="directory"]')
|
||||
.should('have.value', '/var/www/html/nextcloud')
|
||||
})
|
||||
|
||||
it('Renders the htaccess warning', () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
htaccessWorking: false,
|
||||
}
|
||||
cy.mockInitialState('core', 'config', config)
|
||||
cy.mount(SetupView)
|
||||
|
||||
cy.get('[data-cy-setup-form-note="htaccess"]')
|
||||
.should('be.visible')
|
||||
.should('contain', 'Security warning')
|
||||
.invoke('html')
|
||||
.should('contains', links.adminInstall)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Setup page with autoconfig', () => {
|
||||
beforeEach(() => {
|
||||
cy.mockInitialState('core', 'links', links)
|
||||
})
|
||||
|
||||
afterEach(() => cy.unmockInitialState())
|
||||
|
||||
it('Renders autoconfig', () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
hasAutoconfig: true,
|
||||
dbname: 'nextcloud',
|
||||
dbtype: 'mysql',
|
||||
dbuser: 'nextcloud',
|
||||
dbpass: 'password',
|
||||
dbhost: 'localhost',
|
||||
directory: '/var/www/html/nextcloud',
|
||||
} as SetupConfig
|
||||
cy.mockInitialState('core', 'config', config)
|
||||
cy.mount(SetupView)
|
||||
|
||||
// Autoconfig info note
|
||||
cy.get('[data-cy-setup-form-note="autoconfig"]')
|
||||
.should('be.visible')
|
||||
.should('contain', 'Autoconfig file detected')
|
||||
|
||||
// Database and storage section is hidden as already set in autoconfig
|
||||
cy.get('[data-cy-setup-form-advanced-config]').should('be.visible')
|
||||
.invoke('attr', 'open')
|
||||
.should('equal', undefined)
|
||||
|
||||
// Oracle tablespace is hidden
|
||||
cy.get('[data-cy-setup-form-field="dbtablespace"]')
|
||||
.should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Submit a full form sends the data', () => {
|
||||
beforeEach(() => {
|
||||
cy.mockInitialState('core', 'links', links)
|
||||
})
|
||||
|
||||
afterEach(() => cy.unmockInitialState())
|
||||
|
||||
it('Submits a full form', () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
adminlogin: 'admin',
|
||||
adminpass: 'password',
|
||||
dbname: 'nextcloud',
|
||||
dbtype: 'mysql',
|
||||
dbuser: 'nextcloud',
|
||||
dbpass: 'password',
|
||||
dbhost: 'localhost',
|
||||
dbtablespace: 'tablespace',
|
||||
directory: '/var/www/html/nextcloud',
|
||||
} as SetupConfig
|
||||
|
||||
cy.intercept('POST', '**', {
|
||||
delay: 2000,
|
||||
}).as('setup')
|
||||
|
||||
cy.mockInitialState('core', 'config', config)
|
||||
cy.mount(SetupView)
|
||||
|
||||
// Not chaining breaks the test as the POST prevents the element from being retrieved twice
|
||||
// eslint-disable-next-line cypress/unsafe-to-chain-command
|
||||
cy.get('[data-cy-setup-form-submit]')
|
||||
.click()
|
||||
.invoke('attr', 'disabled')
|
||||
.should('equal', 'disabled', { timeout: 500 })
|
||||
|
||||
cy.wait('@setup')
|
||||
.its('request.body')
|
||||
.should('deep.equal', new URLSearchParams({
|
||||
adminlogin: 'admin',
|
||||
adminpass: 'password',
|
||||
directory: '/var/www/html/nextcloud',
|
||||
dbtype: 'mysql',
|
||||
dbuser: 'nextcloud',
|
||||
dbpass: 'password',
|
||||
dbname: 'nextcloud',
|
||||
dbhost: 'localhost',
|
||||
}).toString())
|
||||
})
|
||||
})
|
||||
306
core/src/views/Setup.spec.ts
Normal file
306
core/src/views/Setup.spec.ts
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { SetupConfig, SetupLinks } from '../install.ts'
|
||||
|
||||
import { cleanup, findByRole, fireEvent, getAllByRole, getByRole, render } from '@testing-library/vue'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import SetupView from './Setup.vue'
|
||||
|
||||
import '../../css/guest.css'
|
||||
|
||||
const defaultConfig = Object.freeze({
|
||||
adminlogin: '',
|
||||
adminpass: '',
|
||||
dbuser: '',
|
||||
dbpass: '',
|
||||
dbname: '',
|
||||
dbtablespace: '',
|
||||
dbhost: '',
|
||||
dbtype: '',
|
||||
databases: {
|
||||
sqlite: 'SQLite',
|
||||
mysql: 'MySQL/MariaDB',
|
||||
pgsql: 'PostgreSQL',
|
||||
},
|
||||
directory: '',
|
||||
hasAutoconfig: false,
|
||||
htaccessWorking: true,
|
||||
serverRoot: '/var/www/html',
|
||||
errors: [],
|
||||
}) as SetupConfig
|
||||
|
||||
const links = {
|
||||
adminInstall: 'https://docs.nextcloud.com/server/32/go.php?to=admin-install',
|
||||
adminSourceInstall: 'https://docs.nextcloud.com/server/32/go.php?to=admin-source_install',
|
||||
adminDBConfiguration: 'https://docs.nextcloud.com/server/32/go.php?to=admin-db-configuration',
|
||||
} as SetupLinks
|
||||
|
||||
describe('Default setup page', () => {
|
||||
beforeEach(cleanup)
|
||||
beforeEach(() => {
|
||||
removeInitialState()
|
||||
mockInitialState('core', 'links', links)
|
||||
})
|
||||
|
||||
it('Renders default config', async () => {
|
||||
mockInitialState('core', 'config', defaultConfig)
|
||||
const component = render(SetupView)
|
||||
|
||||
// Single note is the footer help
|
||||
expect(component.getAllByRole('note')).toHaveLength(1)
|
||||
expect(component.getByRole('note').textContent).toContain('See the documentation')
|
||||
|
||||
// DB radio selectors
|
||||
const dbTypes = component.getByRole('group', { name: 'Database type' })
|
||||
expect(getAllByRole(dbTypes, 'radio')).toHaveLength(3)
|
||||
await expect(findByRole(dbTypes, 'radio', { checked: true })).resolves.not.toThrow()
|
||||
await expect(findByRole(dbTypes, 'radio', { name: /MySQL/ })).resolves.not.toThrow()
|
||||
await expect(findByRole(dbTypes, 'radio', { name: /PostgreSQL/ })).resolves.not.toThrow()
|
||||
await expect(findByRole(dbTypes, 'radio', { name: /SQLite/ })).resolves.not.toThrow()
|
||||
|
||||
// Sqlite warning
|
||||
await expect(component.findByText(/SQLite should only be used for minimal and development instances/)).resolves.not.toThrow()
|
||||
|
||||
// admin login, password, data directory
|
||||
await expect(component.findByRole('textbox', { name: 'Administration account name' })).resolves.not.toThrow()
|
||||
await expect(component.findByLabelText('Administration account password')).resolves.not.toThrow()
|
||||
await expect(component.findByRole('textbox', { name: 'Data folder' })).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it('Renders single DB sqlite', async () => {
|
||||
mockInitialState('core', 'config', {
|
||||
...defaultConfig,
|
||||
databases: {
|
||||
sqlite: 'SQLite',
|
||||
},
|
||||
})
|
||||
const component = render(SetupView)
|
||||
|
||||
const dbTypes = component.getByRole('group', { name: 'Database type' })
|
||||
expect(getAllByRole(dbTypes, 'radio', { hidden: true })).toHaveLength(1)
|
||||
await expect(findByRole(dbTypes, 'radio', { name: /SQLite/, hidden: true })).resolves.not.toThrow()
|
||||
|
||||
// Two warnings: sqlite and single db support
|
||||
await expect(component.findByText(/Only SQLite is available./)).resolves.not.toThrow()
|
||||
await expect(component.findByText(/SQLite should only be used for minimal and development instances/)).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it('Renders single DB mysql', async () => {
|
||||
mockInitialState('core', 'config', {
|
||||
...defaultConfig,
|
||||
databases: {
|
||||
mysql: 'MySQL/MariaDB',
|
||||
},
|
||||
})
|
||||
const component = render(SetupView)
|
||||
|
||||
const dbTypes = component.getByRole('group', { name: 'Database type' })
|
||||
expect(getAllByRole(dbTypes, 'radio', { hidden: true })).toHaveLength(1)
|
||||
await expect(findByRole(dbTypes, 'radio', { name: /MySQL/, hidden: true })).resolves.not.toThrow()
|
||||
|
||||
// Single db support warning
|
||||
await expect(component.findByText(/Only MySQL.* is available./)).resolves.not.toThrow()
|
||||
|
||||
// No SQLite warning
|
||||
await expect(component.findByText(/SQLite should only be used for minimal and development instances/)).rejects.toThrow()
|
||||
|
||||
// database config
|
||||
await expect(component.findByRole('textbox', { name: /Database user/ })).resolves.not.toThrow()
|
||||
await expect(component.findByRole('textbox', { name: /Database name/ })).resolves.not.toThrow()
|
||||
await expect(component.findByRole('textbox', { name: /Database host/ })).resolves.not.toThrow()
|
||||
await expect(component.findByLabelText(/Database password/)).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it('Changes fields from sqlite to mysql then oci', async () => {
|
||||
mockInitialState('core', 'config', {
|
||||
...defaultConfig,
|
||||
databases: {
|
||||
sqlite: 'SQLite',
|
||||
mysql: 'MySQL/MariaDB',
|
||||
pgsql: 'PostgreSQL',
|
||||
oci: 'Oracle',
|
||||
},
|
||||
})
|
||||
const component = render(SetupView)
|
||||
|
||||
// SQLite selected
|
||||
await expect(component.findByRole('radio', { name: /SQLite/, checked: true })).resolves.not.toThrow()
|
||||
|
||||
// 4 db toggles
|
||||
const dbTypes = component.getByRole('group', { name: 'Database type' })
|
||||
expect(getAllByRole(dbTypes, 'radio')).toHaveLength(4)
|
||||
|
||||
// but no database config fields
|
||||
await expect(findByRole(dbTypes, 'group', { name: /Database connection/ })).rejects.toThrow()
|
||||
|
||||
// Change to MySQL
|
||||
await fireEvent.click(getByRole(dbTypes, 'radio', { name: /MySQL/, checked: false }))
|
||||
expect((getByRole(dbTypes, 'radio', { name: /SQLite/, checked: false }) as HTMLInputElement).checked).toBe(false)
|
||||
expect((getByRole(dbTypes, 'radio', { name: /MySQL/, checked: true }) as HTMLInputElement).checked).toBe(true)
|
||||
|
||||
// now the database config fields are visible
|
||||
await expect(component.findByRole('group', { name: /Database connection/ })).resolves.not.toThrow()
|
||||
// but not the Database tablespace
|
||||
await expect(component.findByRole('textbox', { name: /Database tablespace/ })).rejects.toThrow()
|
||||
|
||||
// Change to Oracle
|
||||
await fireEvent.click(getByRole(dbTypes, 'radio', { name: /Oracle/, checked: false }))
|
||||
|
||||
// see database config fields are visible and tablespace
|
||||
await expect(component.findByRole('textbox', { name: /Database tablespace/ })).resolves.not.toThrow()
|
||||
await expect(component.findByRole('group', { name: /Database connection/ })).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Setup page with errors and warning', () => {
|
||||
beforeEach(cleanup)
|
||||
beforeEach(() => {
|
||||
removeInitialState()
|
||||
mockInitialState('core', 'links', links)
|
||||
})
|
||||
|
||||
it('Renders error from backend', async () => {
|
||||
mockInitialState('core', 'config', {
|
||||
...defaultConfig,
|
||||
errors: [
|
||||
{
|
||||
error: 'Error message',
|
||||
hint: 'Error hint',
|
||||
},
|
||||
],
|
||||
})
|
||||
const component = render(SetupView)
|
||||
|
||||
// Error message and hint
|
||||
await expect(component.findByText('Error message')).resolves.not.toThrow()
|
||||
await expect(component.findByText('Error hint')).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it('Renders errors from backend', async () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
errors: [
|
||||
'Error message 1',
|
||||
{
|
||||
error: 'Error message 2',
|
||||
hint: 'Error hint',
|
||||
},
|
||||
],
|
||||
}
|
||||
mockInitialState('core', 'config', config)
|
||||
const component = render(SetupView)
|
||||
|
||||
// Error message and hint
|
||||
await expect(component.findByText('Error message 1')).resolves.not.toThrow()
|
||||
await expect(component.findByText('Error message 2')).resolves.not.toThrow()
|
||||
await expect(component.findByText('Error hint')).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it('Renders all the submitted fields on error', async () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
adminlogin: 'admin',
|
||||
adminpass: 'password',
|
||||
dbname: 'nextcloud',
|
||||
dbtype: 'mysql',
|
||||
dbuser: 'nextcloud',
|
||||
dbpass: 'password',
|
||||
dbhost: 'localhost',
|
||||
directory: '/var/www/html/nextcloud',
|
||||
} as SetupConfig
|
||||
mockInitialState('core', 'config', config)
|
||||
const component = render(SetupView)
|
||||
|
||||
await expect(component.findByRole('textbox', { name: 'Data folder' })).resolves.not.toThrow()
|
||||
expect((component.getByRole('textbox', { name: 'Data folder' }) as HTMLInputElement).value).toBe('/var/www/html/nextcloud')
|
||||
|
||||
await expect(component.findByRole('textbox', { name: 'Administration account name' })).resolves.not.toThrow()
|
||||
expect((component.getByRole('textbox', { name: 'Administration account name' }) as HTMLInputElement).value).toBe('admin')
|
||||
|
||||
await expect(component.findByLabelText('Administration account password')).resolves.not.toThrow()
|
||||
expect((component.getByLabelText('Administration account password') as HTMLInputElement).value).toBe('password')
|
||||
|
||||
await expect(component.findByRole('radio', { name: /MySQL/, checked: true, hidden: true })).resolves.not.toThrow()
|
||||
await expect(component.findByRole('textbox', { name: 'Database name' })).resolves.not.toThrow()
|
||||
expect((component.getByRole('textbox', { name: 'Database name' }) as HTMLInputElement).value).toBe('nextcloud')
|
||||
await expect(component.findByRole('textbox', { name: 'Database user' })).resolves.not.toThrow()
|
||||
expect((component.getByRole('textbox', { name: 'Database user' }) as HTMLInputElement).value).toBe('nextcloud')
|
||||
await expect(component.findByRole('textbox', { name: 'Database host' })).resolves.not.toThrow()
|
||||
expect((component.getByRole('textbox', { name: 'Database host' }) as HTMLInputElement).value).toBe('localhost')
|
||||
await expect(component.findByLabelText('Database password')).resolves.not.toThrow()
|
||||
expect((component.getByLabelText('Database password') as HTMLInputElement).value).toBe('password')
|
||||
})
|
||||
|
||||
it('Renders the htaccess warning', async () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
htaccessWorking: false,
|
||||
}
|
||||
mockInitialState('core', 'config', config)
|
||||
const component = render(SetupView)
|
||||
|
||||
await expect(component.findByText('Security warning')).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Setup page with autoconfig', () => {
|
||||
beforeEach(cleanup)
|
||||
beforeEach(() => {
|
||||
removeInitialState()
|
||||
mockInitialState('core', 'links', links)
|
||||
})
|
||||
|
||||
it('Renders autoconfig', async () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
hasAutoconfig: true,
|
||||
dbname: 'nextcloud',
|
||||
dbtype: 'mysql',
|
||||
dbuser: 'nextcloud',
|
||||
dbpass: 'password',
|
||||
dbhost: 'localhost',
|
||||
directory: '/var/www/html/nextcloud',
|
||||
} as SetupConfig
|
||||
mockInitialState('core', 'config', config)
|
||||
const component = render(SetupView)
|
||||
|
||||
// Autoconfig info note
|
||||
await expect(component.findByText('Autoconfig file detected')).resolves.not.toThrow()
|
||||
|
||||
// Oracle tablespace is hidden
|
||||
await expect(component.findByRole('textbox', { name: 'Database tablespace' })).rejects.toThrow()
|
||||
|
||||
// Database and storage section is hidden as already set in autoconfig
|
||||
await expect(component.findByText('Storage & database')).resolves.not.toThrow()
|
||||
expect(component.getByText('Storage & database').closest('details')!.getAttribute('hidden')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Remove the mocked initial state
|
||||
*/
|
||||
function removeInitialState(): void {
|
||||
document.querySelectorAll('input[type="hidden"]').forEach((el) => {
|
||||
el.remove()
|
||||
})
|
||||
// clear the cache
|
||||
delete globalThis._nc_initial_state
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to mock an initial state value
|
||||
* @param app - The app
|
||||
* @param key - The key
|
||||
* @param value - The value
|
||||
*/
|
||||
function mockInitialState(app: string, key: string, value: unknown): void {
|
||||
const el = document.createElement('input')
|
||||
el.value = btoa(JSON.stringify(value))
|
||||
el.id = `initial-state-${app}-${key}`
|
||||
el.type = 'hidden'
|
||||
|
||||
document.head.appendChild(el)
|
||||
}
|
||||
|
|
@ -89,6 +89,10 @@
|
|||
|
||||
<!-- Database type select -->
|
||||
<fieldset class="setup-form__database-type">
|
||||
<legend class="hidden-visually">
|
||||
{{ t('core', 'Database type') }}
|
||||
</legend>
|
||||
|
||||
<!-- Using v-show instead of v-if ensures that the input dbtype remains set even when only one database engine is available -->
|
||||
<p v-show="!firstAndOnlyDatabase" :class="`setup-form__database-type-select--${DBTypeGroupDirection}`" class="setup-form__database-type-select">
|
||||
<NcCheckboxRadioSwitch
|
||||
|
|
@ -126,6 +130,10 @@
|
|||
|
||||
<!-- Database configuration -->
|
||||
<fieldset v-if="config.dbtype !== 'sqlite'">
|
||||
<legend class="hidden-visually">
|
||||
{{ t('core', 'Database connection') }}
|
||||
</legend>
|
||||
|
||||
<NcTextField
|
||||
v-model="config.dbuser"
|
||||
:label="t('core', 'Database user')"
|
||||
|
|
|
|||
|
|
@ -134,40 +134,4 @@ export default defineConfig({
|
|||
return config
|
||||
},
|
||||
},
|
||||
|
||||
component: {
|
||||
specPattern: ['core/**/*.cy.ts', 'apps/**/*.cy.ts'],
|
||||
devServer: {
|
||||
framework: 'vue',
|
||||
bundler: 'webpack',
|
||||
webpackConfig: async () => {
|
||||
process.env.npm_package_name = 'NcCypress'
|
||||
process.env.npm_package_version = '1.0.0'
|
||||
process.env.NODE_ENV = 'development'
|
||||
|
||||
/**
|
||||
* Needed for cypress stubbing
|
||||
*
|
||||
* @see https://github.com/sinonjs/sinon/issues/1121
|
||||
* @see https://github.com/cypress-io/cypress/issues/18662
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const babel = require('./babel.config.js')
|
||||
babel.plugins.push([
|
||||
'@babel/plugin-transform-modules-commonjs',
|
||||
{
|
||||
loose: true,
|
||||
},
|
||||
])
|
||||
|
||||
const config = webpackConfig
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
type: 'asset/source',
|
||||
})
|
||||
|
||||
return config
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<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,43 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { mount } from '@cypress/vue2'
|
||||
|
||||
import '@testing-library/cypress/add-commands'
|
||||
import 'cypress-axe'
|
||||
// styles
|
||||
import '../../apps/theming/css/default.css'
|
||||
import '../../core/css/server.css'
|
||||
|
||||
Cypress.Commands.add('mount', (component, options = {}) => {
|
||||
// Setup options object
|
||||
options.extensions = options.extensions || {}
|
||||
options.extensions.plugins = options.extensions.plugins || []
|
||||
options.extensions.components = options.extensions.components || {}
|
||||
|
||||
return mount(component, options)
|
||||
})
|
||||
|
||||
Cypress.Commands.add('mockInitialState', (app: string, key: string, value: unknown) => {
|
||||
cy.document().then(($document) => {
|
||||
const input = $document.createElement('input')
|
||||
input.setAttribute('type', 'hidden')
|
||||
input.setAttribute('id', `initial-state-${app}-${key}`)
|
||||
input.setAttribute('value', btoa(JSON.stringify(value)))
|
||||
$document.body.appendChild(input)
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add('unmockInitialState', (app?: string, key?: string) => {
|
||||
cy.window().then(($window) => {
|
||||
// @ts-expect-error internal value
|
||||
delete $window._nc_initial_state
|
||||
})
|
||||
|
||||
cy.document().then(($document) => {
|
||||
$document.querySelectorAll('body > input[type="hidden"]' + (app ? `[id="initial-state-${app}-${key}"]` : ''))
|
||||
.forEach((node) => $document.body.removeChild(node))
|
||||
})
|
||||
})
|
||||
17
cypress/support/cypress-component.d.ts
vendored
17
cypress/support/cypress-component.d.ts
vendored
|
|
@ -1,17 +0,0 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { mount } from '@cypress/vue2'
|
||||
|
||||
declare global {
|
||||
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
mount: typeof mount
|
||||
mockInitialState: (app: string, key: string, value: unknown) => Cypress.Chainable<void>
|
||||
unmockInitialState: (app?: string, key?: string) => Cypress.Chainable<void>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["./**/*.ts", "../**/*.cy.ts", "./cypress-e2e.d.ts", "./cypress-component.d.ts"],
|
||||
"include": ["./**/*.ts", "./cypress-e2e.d.ts", "./cypress-component.d.ts"],
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
|
|
|
|||
4
dist/core-install.js
vendored
4
dist/core-install.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-install.js.map
vendored
2
dist/core-install.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/federatedfilesharing-external.js
vendored
4
dist/federatedfilesharing-external.js
vendored
File diff suppressed because one or more lines are too long
2
dist/federatedfilesharing-external.js.map
vendored
2
dist/federatedfilesharing-external.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -56,7 +56,10 @@ export default defineConfig([
|
|||
},
|
||||
},
|
||||
// Cypress setup
|
||||
CypressEslint.configs.recommended,
|
||||
{
|
||||
...CypressEslint.configs.recommended,
|
||||
files: ['cypress/**', '**/*.cy.*'],
|
||||
},
|
||||
{
|
||||
name: 'server/cypress',
|
||||
files: ['cypress/**', '**/*.cy.*'],
|
||||
|
|
|
|||
45
package-lock.json
generated
45
package-lock.json
generated
|
|
@ -92,7 +92,6 @@
|
|||
"@babel/plugin-transform-private-methods": "^7.27.1",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@codecov/webpack-plugin": "^1.9.1",
|
||||
"@cypress/vue2": "^2.1.1",
|
||||
"@cypress/webpack-preprocessor": "^7.0.0",
|
||||
"@nextcloud/babel-config": "^1.2.0",
|
||||
"@nextcloud/cypress": "^1.0.0-beta.15",
|
||||
|
|
@ -128,6 +127,7 @@
|
|||
"file-loader": "^6.2.0",
|
||||
"handlebars-loader": "^1.7.3",
|
||||
"jsdom": "^27.0.0",
|
||||
"jsdom-testing-mocks": "^1.16.0",
|
||||
"mime": "^4.1.0",
|
||||
"msw": "^2.11.3",
|
||||
"raw-loader": "^4.0.2",
|
||||
|
|
@ -2267,21 +2267,6 @@
|
|||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@cypress/vue2": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@cypress/vue2/-/vue2-2.1.1.tgz",
|
||||
"integrity": "sha512-8/1Z6XrSdJWU9ybniGKyUe5iztVIi/Y5PwWg6mtsa8IMdtK2ZA8Vrv/ZIZ8jT3XAEUSaMhPBEh6TgUbq03kr8w==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cypress": ">=4.5.0",
|
||||
"vue": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cypress/webpack-preprocessor": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@cypress/webpack-preprocessor/-/webpack-preprocessor-7.0.1.tgz",
|
||||
|
|
@ -8068,6 +8053,13 @@
|
|||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/bezier-easing": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
|
||||
"integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||
|
|
@ -9566,6 +9558,13 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/css-mediaquery": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
|
||||
"integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==",
|
||||
"dev": true,
|
||||
"license": "BSD"
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
|
||||
|
|
@ -14978,6 +14977,20 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom-testing-mocks": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom-testing-mocks/-/jsdom-testing-mocks-1.16.0.tgz",
|
||||
"integrity": "sha512-wLrulXiLpjmcUYOYGEvz4XARkrmdVpyxzdBl9IAMbQ+ib2/UhUTRCn49McdNfXLff2ysGBUms49ZKX0LR1Q0gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bezier-easing": "^2.1.0",
|
||||
"css-mediaquery": "^0.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/tldts": {
|
||||
"version": "7.0.14",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.14.tgz",
|
||||
|
|
|
|||
|
|
@ -19,9 +19,7 @@
|
|||
"scripts": {
|
||||
"build": "webpack --node-env production --progress",
|
||||
"postbuild": "build/npm-post-build.sh",
|
||||
"cypress": "npm run cypress:component && npm run cypress:e2e",
|
||||
"cypress:component": "cypress run --component",
|
||||
"cypress:e2e": "cypress run --e2e",
|
||||
"cypress": "cypress run --e2e",
|
||||
"cypress:gui": "cypress open",
|
||||
"cypress:version": "cypress version",
|
||||
"dev": "webpack --node-env development --progress",
|
||||
|
|
@ -128,7 +126,6 @@
|
|||
"@babel/plugin-transform-private-methods": "^7.27.1",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@codecov/webpack-plugin": "^1.9.1",
|
||||
"@cypress/vue2": "^2.1.1",
|
||||
"@cypress/webpack-preprocessor": "^7.0.0",
|
||||
"@nextcloud/babel-config": "^1.2.0",
|
||||
"@nextcloud/cypress": "^1.0.0-beta.15",
|
||||
|
|
@ -164,6 +161,7 @@
|
|||
"file-loader": "^6.2.0",
|
||||
"handlebars-loader": "^1.7.3",
|
||||
"jsdom": "^27.0.0",
|
||||
"jsdom-testing-mocks": "^1.16.0",
|
||||
"mime": "^4.1.0",
|
||||
"msw": "^2.11.3",
|
||||
"raw-loader": "^4.0.2",
|
||||
|
|
|
|||
Loading…
Reference in a new issue