mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
refactor(web): extract the VM delete flow into useVmDelete
This commit is contained in:
@@ -49,7 +49,7 @@ const poolStats = createPoolStats({
|
||||
const dashboard = createPoolDashboardTopFiveUsage({ cpu: [createHostCpuUsage()] }, { cpu: [createVmCpuUsage()] })
|
||||
```
|
||||
|
||||
Shared _helper_ factories live there too — `mount-composable.ts`, `mount-chart-card.ts`, `create-enhanced-data-helpers.ts`, `global-test-config.ts`, `create-test-router.ts`, `create-job-result.ts` (what a mocked `defineJob` composable returns), `create-action-item.ts` (a menu / button `LeafActionItem`), `create-plugin-actions.ts` (what a mocked `useXoPluginActions` returns), `find-rendered-values.ts`, `find-tags.ts` (the labels of the rendered tags), `find-titles.ts` (the card and section titles), `find-tooltip-content.ts` (the tooltip an element shows on hover), `get-button.ts` (by its label, or the `aria-label` of an icon button: `findButton` / `getButton` the `<button>` to click, `getUiButton` the `UiButton` whose `busy` / `disabled` props the app sets), `get-checkbox.ts` (the input of a `UiCheckbox` by its label), `drawer.ts` (submit or cancel a drawer, read its buttons), `select.ts` (open a `VtsSelect`, read or pick its options, read the selection it shows), `mount-json-schema-form.ts` (a JSON schema form, or a composable run in one of its fields), `json-schema-item-label.ts` (the labels of the items of a JSON schema list), `find-parts.ts`, `find-input-labels.ts`, `find-input-messages.ts`, `find-accented-labels.ts` (`findAccentedTags` / `findAccentedInfos`: each `UiTag` / `UiInfo` as `{ label, accent }`), `find-card-title.ts` (`findCardTitle`, the whole title; `findCardTitleLabel`, its label alone), `create-select-bindings.ts`, `find-select-stub-labels.ts`, `mount-with-select.ts`, `form-messages.ts`, `credentials-error-wiring.ts`, `v2v-target-cells.ts`, `stub-fetch-response.ts`, `stub-v2v-invalid-credentials-response.ts` (the 422 the V2V routes answer to rejected credentials), `silence-console-error.ts`, `create-v2v-wizard-router.ts` (and `V2V_VMS_FILTER_QUERY_KEY`, the route key of the VMs step filter), `connect-v2v-source.ts` (`connectV2vSource`: sets the V2V connection and inventory a wizard step reads, once Pinia is active), `mount-on-v2v-wizard.ts` (`mountOnV2vWizard`: stores a migration list, routes the wizard to a step, on the pool of the list unless given a query, mounts with that router, then connects a source), `mount-v2v-vms-step.ts` (`mountV2vVmsStep`: mounts a composable on the VMs step, with a stored list and a connected source), `create-v2v-vm-row.ts` (also `getV2vVmNames` / `getV2vVmIds`, the names / ids of V2V VMs or rows, in order), `v2v-vms-fixtures.ts` (two datastores and one inventory VM per case the VMs step tells apart: on one datastore, on both, unsupported, in the list, and the list holding that last one), `v2v-mapping-fixtures.ts` (an inventory VM of two disks, one with a snapshot chain, and two NICs, and the pool offers of a mapping: the targets of the list defaults plus one other SR, network and template; the inventory the VM is read from, the target options its selects list and the disk and NIC rows of its mapping), `v2v-wizard-fixtures.ts` (the connected source, a list stored for a pool without VMs then with some, that pool and another one), `find-table-cells.ts` (see [Testing components](./testing-components.md#tables)), `query-builder-filters.ts` (`UNPARSABLE_FILTER`), `local-storage.ts`, `v2v-remembered-connections-storage.ts`, `v2v-migration-list-storage.ts`, `fill-v2v-connection-form.ts`, `choose-v2v-destination.ts` (choose the pool of the V2V destination form, or a complete destination), `seed-v2v-destination-pools.ts` (the collections a V2V destination pool offers its prefill from), `v2v-connection-provide.ts` (the V2V connection the migration page provides), `v2v-connection-reader.ts` (a component that reads the V2V connection its parent provides), `now.ts` (the fixed date of the fake timers), `group-by.ts`, `i18n.ts` and `linear-chart-stub.ts`. The shared module mocks (`mock-xo-*.ts`, `mock-delete-modal.ts`) live there as well, see [mocking.md](./mocking.md).
|
||||
Shared _helper_ factories live there too — `mount-composable.ts`, `mount-chart-card.ts`, `create-enhanced-data-helpers.ts`, `global-test-config.ts`, `create-test-router.ts`, `create-job-result.ts` (what a mocked `defineJob` composable returns), `create-action-item.ts` (a menu / button `LeafActionItem`), `create-plugin-actions.ts` (what a mocked `useXoPluginActions` returns), `find-rendered-values.ts`, `find-tags.ts` (the labels of the rendered tags), `find-titles.ts` (the card and section titles), `find-tooltip-content.ts` (the tooltip an element shows on hover), `get-button.ts` (by its label, or the `aria-label` of an icon button: `findButton` / `getButton` the `<button>` to click, `getUiButton` the `UiButton` whose `busy` / `disabled` props the app sets), `get-checkbox.ts` (the input of a `UiCheckbox` by its label), `drawer.ts` (submit or cancel a drawer, read its buttons), `select.ts` (open a `VtsSelect`, read or pick its options, read the selection it shows), `mount-json-schema-form.ts` (a JSON schema form, or a composable run in one of its fields), `json-schema-item-label.ts` (the labels of the items of a JSON schema list), `find-parts.ts`, `find-input-labels.ts`, `find-input-messages.ts`, `find-accented-labels.ts` (`findAccentedTags` / `findAccentedInfos`: each `UiTag` / `UiInfo` as `{ label, accent }`), `find-card-title.ts` (`findCardTitle`, the whole title; `findCardTitleLabel`, its label alone), `create-select-bindings.ts`, `find-select-stub-labels.ts`, `mount-with-select.ts`, `form-messages.ts`, `credentials-error-wiring.ts`, `v2v-target-cells.ts`, `stub-fetch-response.ts`, `stub-v2v-invalid-credentials-response.ts` (the 422 the V2V routes answer to rejected credentials), `fetch-requests.ts` (`hasRequested(method, endpoint)`: whether the stubbed `fetch` sent that request to the REST API), `silence-console-error.ts`, `create-v2v-wizard-router.ts` (and `V2V_VMS_FILTER_QUERY_KEY`, the route key of the VMs step filter), `connect-v2v-source.ts` (`connectV2vSource`: sets the V2V connection and inventory a wizard step reads, once Pinia is active), `mount-on-v2v-wizard.ts` (`mountOnV2vWizard`: stores a migration list, routes the wizard to a step, on the pool of the list unless given a query, mounts with that router, then connects a source), `mount-v2v-vms-step.ts` (`mountV2vVmsStep`: mounts a composable on the VMs step, with a stored list and a connected source), `create-v2v-vm-row.ts` (also `getV2vVmNames` / `getV2vVmIds`, the names / ids of V2V VMs or rows, in order), `v2v-vms-fixtures.ts` (two datastores and one inventory VM per case the VMs step tells apart: on one datastore, on both, unsupported, in the list, and the list holding that last one), `v2v-mapping-fixtures.ts` (an inventory VM of two disks, one with a snapshot chain, and two NICs, and the pool offers of a mapping: the targets of the list defaults plus one other SR, network and template; the inventory the VM is read from, the target options its selects list and the disk and NIC rows of its mapping), `v2v-wizard-fixtures.ts` (the connected source, a list stored for a pool without VMs then with some, that pool and another one), `find-table-cells.ts` (see [Testing components](./testing-components.md#tables)), `query-builder-filters.ts` (`UNPARSABLE_FILTER`), `local-storage.ts`, `v2v-remembered-connections-storage.ts`, `v2v-migration-list-storage.ts`, `fill-v2v-connection-form.ts`, `choose-v2v-destination.ts` (choose the pool of the V2V destination form, or a complete destination), `seed-v2v-destination-pools.ts` (the collections a V2V destination pool offers its prefill from), `v2v-connection-provide.ts` (the V2V connection the migration page provides), `v2v-connection-reader.ts` (a component that reads the V2V connection its parent provides), `now.ts` (the fixed date of the fake timers), `group-by.ts`, `i18n.ts` and `linear-chart-stub.ts`. The shared module mocks (`mock-xo-*.ts`, `mock-delete-modal.ts`) live there as well, see [mocking.md](./mocking.md).
|
||||
|
||||
Each factory returns a **fully-populated** object of the real front-end type and accepts a `Partial<T>` of overrides, spread last, so a test only states the fields relevant to its case:
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ Collections always read together share one mock module exporting each of their c
|
||||
|
||||
The same goes for `mock-xo-v2v-vm-rows-collections.ts`, the VM, task and VM snapshot collections the V2V VM rows are built from: `v2vVmRowsCollectionsMock` holds `vms`, `tasks`, `snapshots` and their `are…Ready` flags (true after a reset; set one to false to hold that collection as still loading).
|
||||
|
||||
`mock-delete-modal.ts` stands for `useDeleteModal`, whose overlay a unit test cannot answer: `deleteModalMock.answer('onConfirm' | 'onCancel')` sets how the next confirmation is answered, `deleteModalMock.open` is the spy to read the props it was opened with, and `deleteModalMock.reset()` goes in `beforeEach`.
|
||||
`mock-delete-modal.ts` stands for `useDeleteModal`, whose overlay a unit test cannot answer: `deleteModalMock.answer('onConfirm' | 'onCancel')` sets how the next confirmation is answered, `deleteModalMock.confirm()` runs the `onConfirm` event of the last confirmation for a caller that passes its events instead of awaiting the answer, `deleteModalMock.open` is the spy to read the props it was opened with, and `deleteModalMock.reset()` goes in `beforeEach`.
|
||||
|
||||
`mock-xo-routes.ts` stands for `useXoRoutes`, the XO 5 routes the app fetches: an XO 5 link reads `https://xo5.example.com/#<path>`.
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import VmDeleteButton from '@/modules/vm/components/actions/delete/VmDeleteButton.vue'
|
||||
import type { useVmDelete } from '@/modules/vm/composables/use-vm-delete.composable.ts'
|
||||
import { createVm } from '@/test/create-vm.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import MenuItem from '@core/components/menu/MenuItem.vue'
|
||||
import MenuTrigger from '@core/components/menu/MenuTrigger.vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { ref, toValue } from 'vue'
|
||||
|
||||
const { useDelete, deleteVm } = vi.hoisted(() => ({
|
||||
useDelete: vi.fn(),
|
||||
deleteVm: vi.fn(),
|
||||
}))
|
||||
|
||||
// The composable calls the REST API and opens modals
|
||||
vi.mock(import('@/modules/vm/composables/use-vm-delete.composable.ts'), () => ({
|
||||
useVmDelete: useDelete as unknown as typeof useVmDelete,
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
deleteVm.mockReset()
|
||||
useDelete.mockReset()
|
||||
|
||||
mockDelete()
|
||||
})
|
||||
|
||||
function mockDelete(isDeletingVm = false) {
|
||||
useDelete.mockReturnValue({ deleteVm, isDeletingVm: ref(isDeletingVm) })
|
||||
}
|
||||
|
||||
const VM = createVm()
|
||||
|
||||
function mountButton() {
|
||||
return mount(VmDeleteButton, { props: { vm: VM }, global: createGlobalTestConfig() })
|
||||
}
|
||||
|
||||
it('offers to delete the VM', () => {
|
||||
const menuItem = mountButton().getComponent(MenuItem)
|
||||
|
||||
expect([menuItem.text(), menuItem.props('icon'), menuItem.props('accent')]).toEqual([
|
||||
t('action:delete'),
|
||||
'action:delete',
|
||||
'danger',
|
||||
])
|
||||
})
|
||||
|
||||
it('deletes the VM it is given', () => {
|
||||
mountButton()
|
||||
|
||||
expect(toValue(useDelete.mock.lastCall?.[0])).toEqual(VM)
|
||||
})
|
||||
|
||||
it('deletes the VM when clicking the menu item', async () => {
|
||||
await mountButton().getComponent(MenuTrigger).trigger('click')
|
||||
|
||||
expect(deleteVm).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('shows the menu item as busy while the VM is being deleted', () => {
|
||||
mockDelete(true)
|
||||
|
||||
expect(mountButton().getComponent(MenuItem).props('busy')).toBe(true)
|
||||
})
|
||||
@@ -1,20 +1,14 @@
|
||||
<template>
|
||||
<MenuItem icon="action:delete" :busy="isRunning" accent="danger" @click="openModal()">
|
||||
<MenuItem icon="action:delete" :busy="isDeletingVm" accent="danger" @click="deleteVm()">
|
||||
{{ t('action:delete') }}
|
||||
</MenuItem>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useXoVmUtils } from '@/modules/vm/composables/xo-vm-utils.composable.ts'
|
||||
import { useXoVmDeleteJob } from '@/modules/vm/jobs/xo-vm-delete.job.ts'
|
||||
import { useVmDelete } from '@/modules/vm/composables/use-vm-delete.composable.ts'
|
||||
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
|
||||
import { extractVmHostId } from '@/modules/vm/utils/xo-vm.util.ts'
|
||||
import MenuItem from '@core/components/menu/MenuItem.vue'
|
||||
import { useBlockedModal } from '@core/composables/modals/use-blocked-modal.ts'
|
||||
import { useDeleteModal } from '@core/composables/modals/use-delete-modal.ts'
|
||||
import { useRedirectAfterDelete } from '@core/composables/redirect-after-delete.composable.ts'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const { vm } = defineProps<{
|
||||
vm: FrontXoVm
|
||||
@@ -22,48 +16,5 @@ const { vm } = defineProps<{
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { run: deleteVm, canRun, isRunning } = useXoVmDeleteJob(() => [vm])
|
||||
|
||||
const { xo5VmAdvancedHref } = useXoVmUtils(() => vm)
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const { redirectIfOnObjectPage } = useRedirectAfterDelete({
|
||||
isOnObjectPage: () => route.name?.includes('/vm/') ?? false,
|
||||
redirectTo: () => {
|
||||
const hostId = extractVmHostId(vm)
|
||||
|
||||
if (hostId !== undefined) {
|
||||
return { name: '/host/[id]/vms', params: { id: hostId } }
|
||||
}
|
||||
|
||||
return { name: '/pool/[id]/vms', params: { id: vm.$pool } }
|
||||
},
|
||||
})
|
||||
|
||||
const { open: openDeleteModal } = useDeleteModal()
|
||||
|
||||
const { open: openBlockedModal } = useBlockedModal()
|
||||
|
||||
function openModal() {
|
||||
if (!canRun.value) {
|
||||
return openBlockedModal({
|
||||
props: {
|
||||
blockedOperation: 'destroy',
|
||||
href: xo5VmAdvancedHref.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return openDeleteModal({
|
||||
props: {
|
||||
subject: vm.name_label,
|
||||
description: t('vm-delete-warning'),
|
||||
confirmLabel: t('action:delete-n-vms', { n: 1 }),
|
||||
},
|
||||
events: {
|
||||
onConfirm: () => deleteVm({ detached: true, onSuccess: redirectIfOnObjectPage }),
|
||||
},
|
||||
})
|
||||
}
|
||||
const { deleteVm, isDeletingVm } = useVmDelete(() => vm)
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useXoVmUtils } from '@/modules/vm/composables/xo-vm-utils.composable.ts'
|
||||
import { useXoVmDeleteJob } from '@/modules/vm/jobs/xo-vm-delete.job.ts'
|
||||
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
|
||||
import { extractVmHostId } from '@/modules/vm/utils/xo-vm.util.ts'
|
||||
import { useBlockedModal } from '@core/composables/modals/use-blocked-modal.ts'
|
||||
import { useDeleteModal } from '@core/composables/modals/use-delete-modal.ts'
|
||||
import { useRedirectAfterDelete } from '@core/composables/redirect-after-delete.composable.ts'
|
||||
import { toComputed } from '@core/utils/to-computed.util.ts'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
export function useVmDelete(rawVm: MaybeRefOrGetter<FrontXoVm>) {
|
||||
const vm = toComputed(rawVm)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { run, canRun, isRunning: isDeletingVm } = useXoVmDeleteJob(() => [vm.value])
|
||||
|
||||
const { xo5VmAdvancedHref } = useXoVmUtils(vm)
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const { redirectIfOnObjectPage } = useRedirectAfterDelete({
|
||||
isOnObjectPage: () => route.name?.includes('/vm/') ?? false,
|
||||
redirectTo: () => {
|
||||
const hostId = extractVmHostId(vm.value)
|
||||
|
||||
if (hostId !== undefined) {
|
||||
return { name: '/host/[id]/vms', params: { id: hostId } }
|
||||
}
|
||||
|
||||
return { name: '/pool/[id]/vms', params: { id: vm.value.$pool } }
|
||||
},
|
||||
})
|
||||
|
||||
const { open: openDeleteModal } = useDeleteModal()
|
||||
|
||||
const { open: openBlockedModal } = useBlockedModal()
|
||||
|
||||
function deleteVm() {
|
||||
if (!canRun.value) {
|
||||
return openBlockedModal({
|
||||
props: {
|
||||
blockedOperation: 'destroy',
|
||||
href: xo5VmAdvancedHref.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return openDeleteModal({
|
||||
props: {
|
||||
subject: vm.value.name_label,
|
||||
description: t('vm-delete-warning'),
|
||||
confirmLabel: t('action:delete-n-vms', { n: 1 }),
|
||||
},
|
||||
events: {
|
||||
onConfirm: () => run({ detached: true, onSuccess: redirectIfOnObjectPage }),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return { deleteVm, isDeletingVm }
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useVmDelete } from '@/modules/vm/composables/use-vm-delete.composable.ts'
|
||||
import type { FrontXoVm } from '@/modules/vm/remote-resources/use-xo-vm-collection.ts'
|
||||
import { createTestRouter } from '@/test/create-test-router.ts'
|
||||
import { createVm } from '@/test/create-vm.ts'
|
||||
import { hasRequested } from '@/test/fetch-requests.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import { deleteModalMock } from '@/test/mock-delete-modal.ts'
|
||||
import { mountComposable } from '@/test/mount-composable.ts'
|
||||
import { stubFetchResponse } from '@/test/stub-fetch-response.ts'
|
||||
import type { useBlockedModal } from '@core/composables/modals/use-blocked-modal.ts'
|
||||
import { flushPromises } from '@vue/test-utils'
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
const { openBlockedModal } = vi.hoisted(() => ({ openBlockedModal: vi.fn() }))
|
||||
|
||||
vi.mock(import('@core/composables/modals/use-delete-modal.ts'), () => import('@/test/mock-delete-modal.ts'))
|
||||
|
||||
vi.mock(import('@core/composables/modals/use-blocked-modal.ts'), () => ({
|
||||
useBlockedModal: (() => ({ open: openBlockedModal })) as unknown as typeof useBlockedModal,
|
||||
}))
|
||||
|
||||
vi.mock(import('@/shared/remote-resources/use-xo-routes.ts'), () => import('@/test/mock-xo-routes.ts'))
|
||||
|
||||
beforeEach(() => {
|
||||
deleteModalMock.reset()
|
||||
openBlockedModal.mockReset()
|
||||
})
|
||||
|
||||
const VM = createVm()
|
||||
|
||||
const POOL_VM = createVm({ $container: VM.$pool })
|
||||
|
||||
function mountVmDelete(vm: FrontXoVm = VM, router: Router = createTestRouter()) {
|
||||
return mountComposable(() => useVmDelete(vm), { router }).wrapper.vm
|
||||
}
|
||||
|
||||
function hasDeleted(vm: FrontXoVm) {
|
||||
return hasRequested('DELETE', `vms/${vm.id}`)
|
||||
}
|
||||
|
||||
// Where the app is once the VM is deleted from that page
|
||||
async function deleteFrom(path: string, vm: FrontXoVm = VM) {
|
||||
stubFetchResponse(200, {})
|
||||
const router = createTestRouter()
|
||||
await router.push(path)
|
||||
|
||||
mountVmDelete(vm, router).deleteVm()
|
||||
deleteModalMock.confirm()
|
||||
await flushPromises()
|
||||
|
||||
return router.currentRoute.value.fullPath
|
||||
}
|
||||
|
||||
describe('deleteVm', () => {
|
||||
it('asks to confirm the deletion of the VM, by its name', () => {
|
||||
mountVmDelete().deleteVm()
|
||||
|
||||
expect(deleteModalMock.open.mock.lastCall?.[0].props).toEqual({
|
||||
subject: VM.name_label,
|
||||
description: t('vm-delete-warning'),
|
||||
confirmLabel: t('action:delete-n-vms', { n: 1 }),
|
||||
})
|
||||
})
|
||||
|
||||
it('deletes nothing until confirmed', () => {
|
||||
mountVmDelete().deleteVm()
|
||||
|
||||
expect(hasDeleted(VM)).toBe(false)
|
||||
})
|
||||
|
||||
it('deletes the VM once confirmed', () => {
|
||||
mountVmDelete().deleteVm()
|
||||
|
||||
deleteModalMock.confirm()
|
||||
|
||||
expect(hasDeleted(VM)).toBe(true)
|
||||
})
|
||||
|
||||
it('tells why a VM whose deletion is blocked cannot be deleted, without asking to confirm', () => {
|
||||
mountVmDelete(
|
||||
createVm({ blockedOperations: { destroy: 'protected' } as FrontXoVm['blockedOperations'] })
|
||||
).deleteVm()
|
||||
|
||||
expect(openBlockedModal.mock.lastCall?.[0]).toEqual({
|
||||
props: { blockedOperation: 'destroy', href: `https://xo5.example.com/#/vms/${VM.id}/advanced` },
|
||||
})
|
||||
expect(deleteModalMock.open).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['the VMs of its host', VM, `/host/${VM.$container}/vms`],
|
||||
['the VMs of its pool when it runs on no host', POOL_VM, `/pool/${VM.$pool}/vms`],
|
||||
])('leaves the page of the deleted VM for %s', async (_, vm, path) => {
|
||||
expect(await deleteFrom(`/vm/${vm.id}/dashboard`, vm)).toBe(path)
|
||||
})
|
||||
|
||||
it('stays on a page that is not the page of the VM', async () => {
|
||||
expect(await deleteFrom(`/pool/${VM.$pool}/vms`)).toBe(`/pool/${VM.$pool}/vms`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isDeletingVm', () => {
|
||||
it('is true while the VM is being deleted', () => {
|
||||
const vmDelete = mountVmDelete()
|
||||
|
||||
vmDelete.deleteVm()
|
||||
deleteModalMock.confirm()
|
||||
|
||||
expect(vmDelete.isDeletingVm).toBe(true)
|
||||
})
|
||||
})
|
||||
8
@xen-orchestra/web/src/test/fetch-requests.ts
Normal file
8
@xen-orchestra/web/src/test/fetch-requests.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { BASE_URL } from '@/shared/utils/fetch.util.ts'
|
||||
|
||||
// Whether the stubbed `fetch` (see `setup.ts`, `stub-fetch-response.ts`) sent this request to the REST API
|
||||
export function hasRequested(method: string, endpoint: string) {
|
||||
return vi
|
||||
.mocked(fetch)
|
||||
.mock.calls.some(([url, options]) => url === `${BASE_URL}/${endpoint}` && options?.method === method)
|
||||
}
|
||||
@@ -8,6 +8,10 @@ export const deleteModalMock = {
|
||||
answer(event: DeleteModalEvent) {
|
||||
this.open.mockResolvedValue({ event })
|
||||
},
|
||||
// For a confirmation that runs its `onConfirm` event instead of awaiting the answer
|
||||
confirm() {
|
||||
this.open.mock.lastCall?.[0].events.onConfirm()
|
||||
},
|
||||
reset() {
|
||||
this.open.mockReset()
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user