refactor(test): migrate component tests in core to vitest

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
pull/55747/head
Ferdinand Thiessen 2025-10-14 13:26:55 +07:00
parent 81cfb9580a
commit 3f6f277dba
No known key found for this signature in database
GPG Key ID: 45FAE7268762B400
3 changed files with 314 additions and 377 deletions

@ -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)
}

@ -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')"