diff --git a/__tests__/setup-global.js b/__tests__/setup-global.js
index 93230b0deab..df4486751c7 100644
--- a/__tests__/setup-global.js
+++ b/__tests__/setup-global.js
@@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: CC0-1.0
*/
+
export function setup() {
process.env.TZ = 'UTC'
}
diff --git a/__tests__/setup-testing-library.js b/__tests__/setup-testing-library.js
index 190e6f93e7c..2020513d2f5 100644
--- a/__tests__/setup-testing-library.js
+++ b/__tests__/setup-testing-library.js
@@ -2,5 +2,6 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: CC0-1.0
*/
+
import '@testing-library/jest-dom/vitest'
import 'core-js/stable/index.js'
diff --git a/apps/federatedfilesharing/src/components/RemoteShareDialog.cy.ts b/apps/federatedfilesharing/src/components/RemoteShareDialog.cy.ts
deleted file mode 100644
index 79b5138327a..00000000000
--- a/apps/federatedfilesharing/src/components/RemoteShareDialog.cy.ts
+++ /dev/null
@@ -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)
- })
-})
diff --git a/apps/federatedfilesharing/src/components/RemoteShareDialog.spec.ts b/apps/federatedfilesharing/src/components/RemoteShareDialog.spec.ts
new file mode 100644
index 00000000000..d4603b06666
--- /dev/null
+++ b/apps/federatedfilesharing/src/components/RemoteShareDialog.spec.ts
@@ -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)
+ })
+})
diff --git a/apps/federatedfilesharing/src/components/RemoteShareDialog.vue b/apps/federatedfilesharing/src/components/RemoteShareDialog.vue
index 9ee44f586bf..06847ca0f6f 100644
--- a/apps/federatedfilesharing/src/components/RemoteShareDialog.vue
+++ b/apps/federatedfilesharing/src/components/RemoteShareDialog.vue
@@ -35,8 +35,8 @@ const buttons = computed(() => [
},
{
label: t('federatedfilesharing', 'Add remote share'),
- nativeType: props.passwordRequired ? 'submit' : undefined,
- type: 'primary',
+ type: props.passwordRequired ? 'submit' : undefined,
+ variant: 'primary',
callback: () => emit('close', true, password.value),
},
])
diff --git a/apps/files/src/composables/useFileListWidth.cy.ts b/apps/files/src/composables/useFileListWidth.cy.ts
deleted file mode 100644
index b0d42c4a2d6..00000000000
--- a/apps/files/src/composables/useFileListWidth.cy.ts
+++ /dev/null
@@ -1,56 +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: '
{{ fileListWidth }}
',
- setup() {
- return {
- fileListWidth: useFileListWidth(),
- }
- },
-})
-const FileListMock = defineComponent({
- template: '',
- 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')
- })
-})
diff --git a/apps/files/src/composables/useFileListWidth.spec.ts b/apps/files/src/composables/useFileListWidth.spec.ts
new file mode 100644
index 00000000000..c36f7cb4456
--- /dev/null
+++ b/apps/files/src/composables/useFileListWidth.spec.ts
@@ -0,0 +1,79 @@
+/*!
+ * 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, nextTick } from 'vue'
+
+let resizeObserver: ReturnType
+
+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: '{{ fileListWidth }}
',
+ setup() {
+ return {
+ fileListWidth: useFileListWidth(),
+ }
+ },
+ })
+
+ const FileListMock = defineComponent({
+ template: '',
+ 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,
+ }
+}
diff --git a/apps/files/src/views/DialogConfirmFileExtension.cy.ts b/apps/files/src/views/DialogConfirmFileExtension.cy.ts
deleted file mode 100644
index 460497dd91f..00000000000
--- a/apps/files/src/views/DialogConfirmFileExtension.cy.ts
+++ /dev/null
@@ -1,161 +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'
-
-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)
- })
-})
diff --git a/apps/files/src/views/DialogConfirmFileExtension.spec.ts b/apps/files/src/views/DialogConfirmFileExtension.spec.ts
new file mode 100644
index 00000000000..4fba503767b
--- /dev/null
+++ b/apps/files/src/views/DialogConfirmFileExtension.spec.ts
@@ -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)
+ })
+})
diff --git a/apps/files/src/views/Navigation.cy.ts b/apps/files/src/views/Navigation.cy.ts
deleted file mode 100644
index 8dd0573201d..00000000000
--- a/apps/files/src/views/Navigation.cy.ts
+++ /dev/null
@@ -1,260 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-import type { Navigation } from '@nextcloud/files'
-import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
-import { createTestingPinia } from '@pinia/testing'
-
-import NavigationView from './Navigation.vue'
-import { useViewConfigStore } from '../store/viewConfig'
-import { Folder, View, getNavigation } from '@nextcloud/files'
-
-import router from '../router/router.ts'
-import RouterService from '../services/RouterService'
-
-const resetNavigation = () => {
- const nav = getNavigation()
- ;[...nav.views].forEach(({ id }) => nav.remove(id))
- nav.setActive(null)
-}
-
-const createView = (id: string, name: string, parent?: string) => 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
- })
-})
diff --git a/apps/files/src/views/Navigation.spec.ts b/apps/files/src/views/Navigation.spec.ts
new file mode 100644
index 00000000000..dac79d4eda5
--- /dev/null
+++ b/apps/files/src/views/Navigation.spec.ts
@@ -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 './Navigation.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)
+}
diff --git a/apps/settings/src/components/Markdown.cy.ts b/apps/settings/src/components/Markdown.cy.ts
deleted file mode 100644
index ccdf43c26df..00000000000
--- a/apps/settings/src/components/Markdown.cy.ts
+++ /dev/null
@@ -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')
- })
-})
diff --git a/apps/settings/src/components/Markdown.spec.ts b/apps/settings/src/components/Markdown.spec.ts
new file mode 100644
index 00000000000..64f5d902279
--- /dev/null
+++ b/apps/settings/src/components/Markdown.spec.ts
@@ -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()
+ })
+})
diff --git a/package-lock.json b/package-lock.json
index 3f2d666c39a..2f0421ac471 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -128,7 +128,8 @@
"handlebars-loader": "^1.7.3",
"jasmine-core": "~2.99.1",
"jasmine-sinon": "^0.4.0",
- "jsdom": "^26.1.0",
+ "jsdom": "^27.0.0",
+ "jsdom-testing-mocks": "^1.16.0",
"karma": "^6.4.4",
"karma-chrome-launcher": "^3.2.0",
"karma-coverage": "2.2.1",
@@ -163,6 +164,13 @@
"npm": "^10.5.0"
}
},
+ "node_modules/@acemir/cssom": {
+ "version": "0.9.29",
+ "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.29.tgz",
+ "integrity": "sha512-G90x0VW+9nW4dFajtjCoT+NM0scAfH9Mb08IcjgFHYbfiL/lU04dTF9JuVOi3/OH+DJCQdcIseSXkdCB9Ky6JA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@actions/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
@@ -224,25 +232,59 @@
}
},
"node_modules/@asamuzakjp/css-color": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
- "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz",
+ "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@csstools/css-calc": "^2.1.3",
- "@csstools/css-color-parser": "^3.0.9",
- "@csstools/css-parser-algorithms": "^3.0.4",
- "@csstools/css-tokenizer": "^3.0.3",
- "lru-cache": "^10.4.3"
+ "@csstools/css-calc": "^2.1.4",
+ "@csstools/css-color-parser": "^3.1.0",
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4",
+ "lru-cache": "^11.2.4"
}
},
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "version": "11.2.4",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
+ "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
"dev": true,
- "license": "ISC"
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "6.7.6",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz",
+ "integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/nwsapi": "^2.3.9",
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.1.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.2.4"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": {
+ "version": "11.2.4",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
+ "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@asamuzakjp/nwsapi": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
@@ -2032,9 +2074,9 @@
}
},
"node_modules/@csstools/color-helpers": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
- "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
"dev": true,
"funding": [
{
@@ -2076,9 +2118,9 @@
}
},
"node_modules/@csstools/css-color-parser": {
- "version": "3.0.10",
- "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz",
- "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
"dev": true,
"funding": [
{
@@ -2092,7 +2134,7 @@
],
"license": "MIT",
"dependencies": {
- "@csstools/color-helpers": "^5.0.2",
+ "@csstools/color-helpers": "^5.1.0",
"@csstools/css-calc": "^2.1.4"
},
"engines": {
@@ -2127,9 +2169,9 @@
}
},
"node_modules/@csstools/css-syntax-patches-for-csstree": {
- "version": "1.0.19",
- "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.19.tgz",
- "integrity": "sha512-QW5/SM2ARltEhoKcmRI1LoLf3/C7dHGswwCnfLcoMgqurBT4f8GvwXMgAbK/FwcxthmJRK5MGTtddj0yQn0J9g==",
+ "version": "1.0.21",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.21.tgz",
+ "integrity": "sha512-plP8N8zKfEZ26figX4Nvajx8DuzfuRpLTqglQ5d0chfnt35Qt3X+m6ASZ+rG0D0kxe/upDVNwSIVJP5n4FuNfw==",
"dev": true,
"funding": [
{
@@ -2141,6 +2183,7 @@
"url": "https://opencollective.com/csstools"
}
],
+ "license": "MIT-0",
"engines": {
"node": ">=18"
}
@@ -8930,6 +8973,23 @@
"tweetnacl": "^0.14.3"
}
},
+ "node_modules/bezier-easing": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
+ "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
"node_modules/big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -10557,6 +10617,13 @@
"node": ">=10"
}
},
+ "node_modules/css-mediaquery": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
+ "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==",
+ "dev": true,
+ "license": "BSD"
+ },
"node_modules/css-tree": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
@@ -10592,17 +10659,18 @@
}
},
"node_modules/cssstyle": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
- "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "version": "5.3.5",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.5.tgz",
+ "integrity": "sha512-GlsEptulso7Jg0VaOZ8BXQi3AkYM5BOJKEO/rjMidSCq70FkIC5y0eawrCXeYzxgt3OCf4Ls+eoxN+/05vN0Ag==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@asamuzakjp/css-color": "^3.2.0",
- "rrweb-cssom": "^0.8.0"
+ "@asamuzakjp/css-color": "^4.1.1",
+ "@csstools/css-syntax-patches-for-csstree": "^1.0.21",
+ "css-tree": "^3.1.0"
},
"engines": {
- "node": ">=18"
+ "node": ">=20"
}
},
"node_modules/csstype": {
@@ -10800,17 +10868,17 @@
}
},
"node_modules/data-urls": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
- "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz",
+ "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"whatwg-mimetype": "^4.0.0",
- "whatwg-url": "^14.0.0"
+ "whatwg-url": "^15.0.0"
},
"engines": {
- "node": ">=18"
+ "node": ">=20"
}
},
"node_modules/data-view-buffer": {
@@ -16833,35 +16901,35 @@
}
},
"node_modules/jsdom": {
- "version": "26.1.0",
- "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
- "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "version": "27.3.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.3.0.tgz",
+ "integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "cssstyle": "^4.2.1",
- "data-urls": "^5.0.0",
- "decimal.js": "^10.5.0",
+ "@acemir/cssom": "^0.9.28",
+ "@asamuzakjp/dom-selector": "^6.7.6",
+ "cssstyle": "^5.3.4",
+ "data-urls": "^6.0.0",
+ "decimal.js": "^10.6.0",
"html-encoding-sniffer": "^4.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"is-potential-custom-element-name": "^1.0.1",
- "nwsapi": "^2.2.16",
- "parse5": "^7.2.1",
- "rrweb-cssom": "^0.8.0",
+ "parse5": "^8.0.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
- "tough-cookie": "^5.1.1",
+ "tough-cookie": "^6.0.0",
"w3c-xmlserializer": "^5.0.0",
- "webidl-conversions": "^7.0.0",
+ "webidl-conversions": "^8.0.0",
"whatwg-encoding": "^3.1.1",
"whatwg-mimetype": "^4.0.0",
- "whatwg-url": "^14.1.1",
- "ws": "^8.18.0",
+ "whatwg-url": "^15.1.0",
+ "ws": "^8.18.3",
"xml-name-validator": "^5.0.0"
},
"engines": {
- "node": ">=18"
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"canvas": "^3.0.0"
@@ -16872,6 +16940,53 @@
}
}
},
+ "node_modules/jsdom-testing-mocks": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/jsdom-testing-mocks/-/jsdom-testing-mocks-1.16.0.tgz",
+ "integrity": "sha512-wLrulXiLpjmcUYOYGEvz4XARkrmdVpyxzdBl9IAMbQ+ib2/UhUTRCn49McdNfXLff2ysGBUms49ZKX0LR1Q0gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bezier-easing": "^2.1.0",
+ "css-mediaquery": "^0.1.2"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/jsdom/node_modules/tldts": {
+ "version": "7.0.19",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz",
+ "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.0.19"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/jsdom/node_modules/tldts-core": {
+ "version": "7.0.19",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz",
+ "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsdom/node_modules/tough-cookie": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
+ "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/jsdom/node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
@@ -19444,13 +19559,6 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
- "node_modules/nwsapi": {
- "version": "2.2.21",
- "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz",
- "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -20002,9 +20110,9 @@
}
},
"node_modules/parse5": {
- "version": "7.3.0",
- "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
- "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
+ "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -22229,13 +22337,6 @@
"fsevents": "~2.3.2"
}
},
- "node_modules/rrweb-cssom": {
- "version": "0.8.0",
- "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
- "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/run-applescript": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
@@ -25206,16 +25307,16 @@
}
},
"node_modules/tr46": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
- "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
- "node": ">=18"
+ "node": ">=20"
}
},
"node_modules/tr46/node_modules/punycode": {
@@ -26989,13 +27090,13 @@
"dev": true
},
"node_modules/webidl-conversions": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
- "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz",
+ "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
- "node": ">=12"
+ "node": ">=20"
}
},
"node_modules/webpack": {
@@ -27453,17 +27554,17 @@
}
},
"node_modules/whatwg-url": {
- "version": "14.2.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
- "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz",
+ "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "tr46": "^5.1.0",
- "webidl-conversions": "^7.0.0"
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.0"
},
"engines": {
- "node": ">=18"
+ "node": ">=20"
}
},
"node_modules/which": {
diff --git a/package.json b/package.json
index e8a04e9984a..5ae92397500 100644
--- a/package.json
+++ b/package.json
@@ -159,7 +159,8 @@
"handlebars-loader": "^1.7.3",
"jasmine-core": "~2.99.1",
"jasmine-sinon": "^0.4.0",
- "jsdom": "^26.1.0",
+ "jsdom": "^27.0.0",
+ "jsdom-testing-mocks": "^1.16.0",
"karma": "^6.4.4",
"karma-chrome-launcher": "^3.2.0",
"karma-coverage": "2.2.1",