Merge pull request #55747 from nextcloud/chore/migrate-cypress-vitest
refactor(test): migrate Cypress component test to vitestpull/55681/head
commit
0c1be89f1b
@ -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)
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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())
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
}
|
||||
@ -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))
|
||||
})
|
||||
})
|
||||
@ -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>
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue