Merge remote-tracking branch 'origin/develop' into staging

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-12-05 14:14:38 +07:00
commit 3f007f25b0
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
28 changed files with 467 additions and 173 deletions

View File

@ -115,6 +115,7 @@ function defineApplication (builder: Builder): void {
navigationComponentIcon: testManagement.icon.TestRuns,
mainComponentLabel: testManagement.string.TestResults,
mainComponentIcon: testManagement.icon.TestResult,
mainHeaderComponent: testManagement.component.TestRunHeader,
navigationComponentProps: {
_class: testManagement.class.TestRun,
icon: testManagement.icon.TestRuns,

View File

@ -41,6 +41,7 @@ export default mergeIds(testManagementId, testManganement, {
RunButton: '' as AnyComponent,
TestResultPresenter: '' as AnyComponent,
EditTestResult: '' as AnyComponent,
TestResultFooter: '' as AnyComponent
TestResultFooter: '' as AnyComponent,
TestRunHeader: '' as AnyComponent
}
})

View File

@ -64,4 +64,17 @@
<symbol id="run" viewBox="-5 -3 24 24">
<path d="M2.067,0.043C2.21-0.028,2.372-0.008,2.493,0.085l13.312,8.503c0.094,0.078,0.154,0.191,0.154,0.313c0,0.12-0.061,0.237-0.154,0.314L2.492,17.717c-0.07,0.057-0.162,0.087-0.25,0.087l-0.176-0.04c-0.136-0.065-0.222-0.207-0.222-0.361V0.402C1.844,0.25,1.93,0.107,2.067,0.043z" />
</symbol>
<symbol id="status-untested" viewBox="0 0 14 14" fill="#D7D8DB">
<path d="M7,0C3.1,0,0,3.1,0,7c0,3.9,3.1,7,7,7c3.9,0,7-3.1,7-7C14,3.1,10.9,0,7,0z M7,12c-2.8,0-5-2.2-5-5s2.2-5,5-5s5,2.2,5,5S9.8,12,7,12z" />
</symbol>
<symbol id="status-passed" viewBox="0 0 14 14" fill="var(--theme-won-color)">
<path d="M7,0C3.1,0,0,3.1,0,7c0,3.9,3.1,7,7,7c3.9,0,7-3.1,7-7C14,3.1,10.9,0,7,0z M9.9,3.9c0.3-0.3,0.8-0.3,1.2,0l0,0c0.3,0.3,0.3,0.9,0,1.2l-5,5c-0.3,0.3-0.9,0.3-1.2,0l-2-2c-0.3-0.3-0.3-0.9,0-1.2c0.2-0.2,0.4-0.2,0.6-0.2s0.4,0.1,0.6,0.2l1.4,1.4l4-4v0L9.9,3.9z" />
</symbol>
<symbol id="status-failed" viewBox="0 0 14 14" fill="var(--theme-error-color)">
<path style="fill-rule:evenodd;clip-rule:evenodd;" d="M7,14c3.9,0,7-3.1,7-7c0-3.9-3.1-7-7-7C3.1,0,0,3.1,0,7C0,10.9,3.1,14,7,14z M5,4C4.7,3.7,4.3,3.7,4,4S3.7,4.7,4,5l2,2L4,9C3.7,9.3,3.7,9.7,4,10c0.3,0.3,0.8,0.3,1.1,0l2-2l2,2c0.3,0.3,0.8,0.3,1.1,0c0.3-0.3,0.3-0.8,0-1.1l-2-2l2-2c0.3-0.3,0.3-0.8,0-1.1C9.7,3.7,9.3,3.7,9,4l-2,2L5,4z" />
</symbol>
<symbol id="status-blocked" viewBox="0 0 14 14" fill="var(--theme-warning-color)">
<path d="M7,0C3.1,0,0,3.1,0,7c0,3.9,3.1,7,7,7c3.9,0,7-3.1,7-7C14,3.1,10.9,0,7,0z M7,12c-2.8,0-5-2.2-5-5s2.2-5,5-5s5,2.2,5,5S9.8,12,7,12z" />
<path d="M7,7V3.5c1.9,0,3.5,1.6,3.5,3.5S8.9,10.5,7,10.5V7z" />
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -79,6 +79,9 @@
"TestCaseDescription": "Popis testovacího případu",
"TestResultAttributes": "Výsledek",
"GoToNextTest": "Další",
"GoToNextTestTooltip": "Přejít na další test"
"GoToNextTestTooltip": "Přejít na další test",
"AllTests": "Všechny testy",
"MyTests": "Moje testy",
"Comments": "Komentáře"
}
}

View File

@ -79,6 +79,9 @@
"TestCaseDescription": "Test case description",
"TestResultAttributes": "Result",
"GoToNextTest": "Next",
"GoToNextTestTooltip": "Go to next test"
"GoToNextTestTooltip": "Go to next test",
"AllTests": "All tests",
"MyTests": "My tests",
"Comments": "Comments"
}
}

View File

@ -79,6 +79,9 @@
"TestCaseDescription": "Descripción del caso de prueba",
"TestResultAttributes": "Resultado",
"GoToNextTest": "Siguiente",
"GoToNextTestTooltip": "Ir al siguiente test"
"GoToNextTestTooltip": "Ir al siguiente test",
"AllTests": "Todas las pruebas",
"MyTests": "Mis pruebas",
"Comments": "Comentarios"
}
}

View File

@ -79,6 +79,9 @@
"TestCaseDescription": "Description du cas de test",
"TestResultAttributes": "Résultat",
"GoToNextTest": "Suivant",
"GoToNextTestTooltip": "Aller au test suivant"
"GoToNextTestTooltip": "Aller au test suivant",
"AllTests": "Tous les tests",
"MyTests": "Mes tests",
"Comments": "Commentaires"
}
}

View File

@ -79,6 +79,9 @@
"TestCaseDescription": "Descrizione del caso di test",
"TestResultAttributes": "Risultato",
"GoToNextTest": "Successivo",
"GoToNextTestTooltip": "Vai al test successivo"
"GoToNextTestTooltip": "Vai al test successivo",
"AllTests": "Tutte le test",
"MyTests": "Le mie test",
"Comments": "Commenti"
}
}

View File

@ -79,6 +79,9 @@
"TestCaseDescription": "Descrição do caso de teste",
"TestResultAttributes": "Resultado",
"GoToNextTest": "Próximo",
"GoToNextTestTooltip": "Ir para o próximo teste"
"GoToNextTestTooltip": "Ir para o próximo teste",
"AllTests": "Todos os testes",
"MyTests": "Meus testes",
"Comments": "Comentários"
}
}

View File

@ -79,6 +79,9 @@
"TestCaseDescription": "Описание тест-кейса",
"TestResultAttributes": "Результат",
"GoToNextTest": "Следующий",
"GoToNextTestTooltip": "Перейти к следующему тесту"
"GoToNextTestTooltip": "Перейти к следующему тесту",
"AllTests": "Все тесты",
"MyTests": "Мои тесты",
"Comments": "Комментарии"
}
}

View File

@ -79,6 +79,9 @@
"TestCaseDescription": "測試用例描述",
"TestResultAttributes": "結果",
"GoToNextTest": "下一個",
"GoToNextTestTooltip": "轉到下一個測試"
"GoToNextTestTooltip": "轉到下一個測試",
"AllTests": "所有測試",
"MyTests": "我的測試",
"Comments": "評論"
}
}

View File

@ -38,9 +38,9 @@ loadMetadata(testManagement.icon, {
StatusRejected: `${icons}#status-canceled`,
TestLibrary: `${icons}#test-library`,
TestResult: `${icons}#testResult`,
StatusNonTested: `${icons}#status-draft`,
StatusBlocked: `${icons}#status-review-comments`,
StatusPassed: `${icons}#status-approved`,
StatusFailed: `${icons}#status-canceled`,
StatusNonTested: `${icons}#status-untested`,
StatusBlocked: `${icons}#status-blocked`,
StatusPassed: `${icons}#status-passed`,
StatusFailed: `${icons}#status-failed`,
Run: `${icons}#run`
})

View File

@ -15,16 +15,12 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte'
import { AttachmentStyleBoxCollabEditor } from '@hcengineering/attachment-resources'
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import { type Class, type Ref, Doc, Mixin, WithLookup } from '@hcengineering/core'
import { ActionContext, createQuery } from '@hcengineering/presentation'
import { type Class, type Ref, WithLookup } from '@hcengineering/core'
import { TestCase, TestResult } from '@hcengineering/test-management'
import { Panel } from '@hcengineering/panel'
import { Label, Scroller } from '@hcengineering/ui'
import { DocAttributeBar, getDocMixins } from '@hcengineering/view-resources'
import RightHeader from './RightHeader.svelte'
import NextButton from './NextButton.svelte'
import TestResultAside from './TestResultAside.svelte'
import TestCaseDetails from '../test-case/TestCaseDetails.svelte'
import testManagement from '../../plugin'
@ -36,13 +32,6 @@
const testCase = object?.$lookup?.testCase as TestCase | undefined
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
let mixins: Mixin<Doc>[] = []
$: mixins = object ? getDocMixins(object, false) : []
let descriptionBox: AttachmentStyleBoxCollabEditor
const query = createQuery()
@ -61,10 +50,6 @@
}
)
let content: HTMLElement
$: descriptionKey = hierarchy.getAttribute(testManagement.class.TestResult, 'description')
onMount(() => dispatch('open', { ignoreKeys: [] }))
</script>
@ -77,34 +62,15 @@
isAside={true}
isSub={false}
adaptive={'default'}
withoutActivity={true}
on:open
on:close={() => dispatch('close')}
>
<div class="space-divider" />
<div class="w-full mt-6">
<AttachmentStyleBoxCollabEditor
focusIndex={30}
{object}
key={{ key: 'description', attr: descriptionKey }}
bind:this={descriptionBox}
identifier={object?._id}
placeholder={testManagement.string.DescriptionPlaceholder}
boundary={content}
/>
</div>
<svelte:fragment slot="extra">
<NextButton {object} />
</svelte:fragment>
<TestCaseDetails _id={object.testCase} object={testCase} _class={testManagement.class.TestCase} />
<svelte:fragment slot="aside">
<DocAttributeBar {object} {mixins} ignoreKeys={['name']} />
<RightHeader>
<Label label={testManagement.string.TestCaseDescription} />
</RightHeader>
<Scroller padding={'0.5rem 2rem'}>
<TestCaseDetails _id={object.testCase} object={testCase} _class={testManagement.class.TestCase} />
</Scroller>
<TestResultAside {object} />
</svelte:fragment>
</Panel>
{/if}

View File

@ -1,81 +0,0 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import { Button, Loading, Location, navigate } from '@hcengineering/ui'
import { initializeIterator, testResultIteratorProvider, testIteratorStore } from './store/testIteratorStore'
import testManagement, { TestResult } from '@hcengineering/test-management'
import { Doc, type DocumentQuery, WithLookup } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { getObjectLinkFragment } from '@hcengineering/view-resources'
import view from '@hcengineering/view'
export let object: WithLookup<TestResult> | undefined
let isLoading = true
let hasNext = false
const client = getClient()
const hierarchy = client.getHierarchy()
const unsubscribe = testIteratorStore.subscribe(() => {
hasNext = testResultIteratorProvider.getIterator()?.hasNext() ?? false
})
onMount(async () => {
const query: DocumentQuery<TestResult> = { attachedTo: object?.attachedTo } as any
await initializeIterator(query, object?._id)
hasNext = testResultIteratorProvider.getIterator()?.hasNext() ?? false
isLoading = false
})
onDestroy(() => {
testResultIteratorProvider.reset()
unsubscribe()
})
async function goToNextItem (): Promise<void> {
const iterator = testResultIteratorProvider.getIterator()
if (iterator !== undefined) {
const nextItem = iterator.next()
if (nextItem === undefined) {
console.error('No next item')
return
}
const link = await getLink(nextItem)
if (link !== undefined) {
navigate(link)
}
console.log('Next item:', nextItem)
}
}
async function getLink (object: Doc): Promise<Location> {
const { component } = hierarchy.classHierarchyMixin(testManagement.class.TestResult, view.mixin.ObjectPanel) as any
return await getObjectLinkFragment(hierarchy, object, {}, component)
}
</script>
{#if isLoading}
<Loading />
{:else}
<Button
label={testManagement.string.GoToNextTest}
kind={'primary'}
icon={view.icon.ArrowRight}
disabled={!hasNext}
on:click={goToNextItem}
showTooltip={{ label: testManagement.string.GoToNextTestTooltip }}
/>
{/if}

View File

@ -0,0 +1,76 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte'
import activity from '@hcengineering/activity'
import { AttachmentStyleBoxCollabEditor } from '@hcengineering/attachment-resources'
import { getClient } from '@hcengineering/presentation'
import { Doc, Mixin, WithLookup } from '@hcengineering/core'
import testManagement, { TestResult } from '@hcengineering/test-management'
import { DocAttributeBar, getDocMixins } from '@hcengineering/view-resources'
import { Component, Label } from '@hcengineering/ui'
import RightHeader from './RightHeader.svelte'
export let object: WithLookup<TestResult> | undefined
export let withoutActivity: boolean = false
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
let mixins: Mixin<Doc>[] = []
$: mixins = object !== undefined ? getDocMixins(object, false) : []
let descriptionBox: AttachmentStyleBoxCollabEditor
let content: HTMLElement
$: descriptionKey = hierarchy.getAttribute(testManagement.class.TestResult, 'description')
onMount(() => dispatch('open', { ignoreKeys: [] }))
</script>
{#if object}
<DocAttributeBar {object} {mixins} ignoreKeys={['name']} />
<RightHeader>
<Label label={testManagement.string.Comments} />
</RightHeader>
<div class="w-full mt-6 px-4">
<AttachmentStyleBoxCollabEditor
focusIndex={30}
{object}
key={{ key: 'description', attr: descriptionKey }}
bind:this={descriptionBox}
identifier={object?._id}
placeholder={testManagement.string.DescriptionPlaceholder}
boundary={content}
/>
</div>
{#if !withoutActivity}
<div class="w-full mt-6 p-4">
<Component
is={activity.component.Activity}
props={{
object,
showCommenInput: true,
focusIndex: 1000,
boundary: content
}}
/>
</div>
{/if}
{/if}

View File

@ -0,0 +1,82 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher, onMount, onDestroy } from 'svelte'
import { ActionContext } from '@hcengineering/presentation'
import { WithLookup } from '@hcengineering/core'
import testManagement, { TestResult, TestCase } from '@hcengineering/test-management'
import { Panel } from '@hcengineering/panel'
import { Button } from '@hcengineering/ui'
import { testResultIteratorProvider, testIteratorStore } from './store/testIteratorStore'
import TestResultAside from './TestResultAside.svelte'
import TestCaseDetails from '../test-case/TestCaseDetails.svelte'
import view from '@hcengineering/view'
const dispatch = createEventDispatcher()
let object: WithLookup<TestResult> | undefined = undefined
let testCase: TestCase | undefined = undefined
let hasNext = false
const unsubscribe = testIteratorStore.subscribe(() => {
hasNext = testResultIteratorProvider.getIterator()?.hasNext() ?? false
})
onMount(async () => {
object = testResultIteratorProvider.getIterator()?.next()
testCase = object?.$lookup?.testCase as TestCase | undefined
})
onDestroy(() => {
testResultIteratorProvider.reset()
unsubscribe()
})
async function goToNextItem (): Promise<void> {
object = testResultIteratorProvider.getIterator()?.next()
testCase = object?.$lookup?.testCase as TestCase | undefined
}
</script>
<ActionContext context={{ mode: 'editor' }} />
{#if object !== undefined}
<Panel
{object}
title={object?.name}
isHeader={false}
isAside={true}
isSub={false}
adaptive={'default'}
withoutActivity
on:open
on:close={() => dispatch('close')}
>
<svelte:fragment slot="extra">
<Button
label={testManagement.string.GoToNextTest}
kind={'primary'}
icon={view.icon.ArrowRight}
disabled={!hasNext}
on:click={goToNextItem}
showTooltip={{ label: testManagement.string.GoToNextTestTooltip }}
/>
</svelte:fragment>
<TestCaseDetails _id={object.testCase} object={testCase} _class={testManagement.class.TestCase} />
<svelte:fragment slot="aside">
<TestResultAside {object} withoutActivity={true} />
</svelte:fragment>
</Panel>
{/if}

View File

@ -17,11 +17,11 @@ import { writable, get } from 'svelte/store'
import {
type IteratorState,
type StoreAdapter,
type IteratorParams,
ObjectIteratorProvider,
getDefaultIteratorState
} from '@hcengineering/view-resources'
import testManagement, { type TestResult } from '@hcengineering/test-management'
import type { DocumentQuery, Ref } from '@hcengineering/core'
export const testIteratorStore = writable<IteratorState<TestResult>>(getDefaultIteratorState<TestResult>({}))
@ -39,11 +39,8 @@ const adapter: StoreAdapter<TestResult> = {
export const testResultIteratorProvider = new ObjectIteratorProvider<TestResult>(adapter)
export async function initializeIterator (
query: DocumentQuery<TestResult>,
currentObject: Ref<TestResult> | undefined
): Promise<void> {
await testResultIteratorProvider.initialize(testManagement.class.TestResult, query, currentObject)
export async function initializeIterator (options: IteratorParams<TestResult>): Promise<void> {
await testResultIteratorProvider.initialize(testManagement.class.TestResult, options)
}
export function resetTestObjectIterator (): void {

View File

@ -0,0 +1,41 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { IModeSelector, ModeSelector, resolvedLocationStore } from '@hcengineering/ui'
import { IntlString } from '@hcengineering/platform'
import { getCurrentMode, onModeChanged } from '../../navigation'
export let modes: [string, IntlString, object][]
let mode: string | undefined = undefined
let modeSelectorProps: IModeSelector | undefined = undefined
$: mode = getCurrentMode($resolvedLocationStore)
$: if (mode === undefined) {
;[[mode]] = modes
}
$: if (mode !== undefined) {
modeSelectorProps = {
config: modes,
mode,
onChange: onModeChanged
}
}
</script>
{#if modeSelectorProps !== undefined}
<ModeSelector kind={'subtle'} props={modeSelectorProps} />
{/if}

View File

@ -0,0 +1,45 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { DocumentQuery, Ref, Space } from '@hcengineering/core'
import { Button } from '@hcengineering/ui'
import type { TestProject, TestResult } from '@hcengineering/test-management'
import { selectionStore } from '@hcengineering/view-resources'
import testManagement from '../../plugin'
import { showTestRunnerPanel } from '../../utils'
export let query: DocumentQuery<TestResult> = {}
export let space: Ref<Space>
const project: Ref<TestProject> = space as any
const handleRun = async (): Promise<void> => {
const selectedDocs = $selectionStore?.docs ?? []
await showTestRunnerPanel({
query,
space: project,
selectedDocs: selectedDocs.length > 0 ? (selectedDocs as TestResult[]) : undefined
})
}
</script>
<Button
icon={testManagement.icon.Run}
justify={'left'}
kind={'primary'}
label={testManagement.string.RunTestCases}
on:click={handleRun}
/>

View File

@ -0,0 +1,35 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { IntlString } from '@hcengineering/platform'
import testManagement, { type TestResult } from '@hcengineering/test-management'
import { DocumentQuery, Ref, Space } from '@hcengineering/core'
import TestResultModeSelector from './TestResultModeSelector.svelte'
import TestRunButton from './TestRunButton.svelte'
export let query: DocumentQuery<TestResult> = {}
export let space: Ref<Space>
const modes: [string, IntlString, object][] = [
[testManagement.mode.AllTests, testManagement.string.AllTests, {}],
[testManagement.mode.MyTests, testManagement.string.MyTests, {}]
]
</script>
{#if modes !== undefined}
<TestResultModeSelector {modes} />
<TestRunButton {query} {space} />
{/if}

View File

@ -36,6 +36,8 @@ import TestResultPresenter from './components/test-result/TestResultPresenter.sv
import EditTestResult from './components/test-result/EditTestResult.svelte'
import TestResultHeader from './components/test-result/TestResultHeader.svelte'
import TestResultFooter from './components/test-result/TestResultFooter.svelte'
import TestRunHeader from './components/test-run/TestRunHeader.svelte'
import TestRunner from './components/test-result/TestRunner.svelte'
import { CreateChildTestSuiteAction, EditTestSuiteAction, RunSelectedTestsAction } from './utils'
import { resolveLocation, getAttachedObjectLink } from './navigation'
@ -63,7 +65,9 @@ export default async (): Promise<Resources> => ({
TestResultPresenter,
EditTestResult,
TestResultHeader,
TestResultFooter
TestResultFooter,
TestRunHeader,
TestRunner
},
function: {
GetTestSuiteLink: getAttachedObjectLink,

View File

@ -11,15 +11,21 @@
//
// See the License for the specific language governing permissions and
// limitations under the License.
import testManagement, { testManagementId, type TestSuite, type TestProject } from '@hcengineering/test-management'
import { type Doc, type Ref } from '@hcengineering/core'
import testManagement, {
testManagementId,
type TestSuite,
type TestProject,
type TestRun
} from '@hcengineering/test-management'
import { type Doc, type Ref, getCurrentAccount } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import {
getCurrentResolvedLocation,
getLocation,
getPanelURI,
type Location,
type ResolvedLocation
type ResolvedLocation,
navigate
} from '@hcengineering/ui'
import view, { type ObjectPanel } from '@hcengineering/view'
import { accessDeniedStore } from '@hcengineering/view-resources'
@ -63,7 +69,7 @@ async function generateProjectLocation (
export function getAttachedObjectLink (parentDoc: Ref<Doc>): Location {
const loc = getCurrentResolvedLocation()
loc.query = parentDoc === undefined ? undefined : { attachedTo: parentDoc }
loc.query = parentDoc === undefined ? undefined : { ...loc.query, attachedTo: parentDoc }
return loc
}
@ -79,6 +85,11 @@ export function getTestSuiteIdFromLocation (): Ref<TestSuite> {
return (location?.query?.[SUITE_KEY] as Ref<TestSuite>) ?? testManagement.ids.NoParent
}
export function getTestRunIdFromLocation (): Ref<TestRun> {
const location = getLocation()
return (location?.query?.[SUITE_KEY] as Ref<TestRun>) ?? testManagement.ids.NoTestRun
}
export function getTestRunsLink (space: Ref<TestProject>, parentDoc: Ref<Doc>): Location {
const loc = getCurrentResolvedLocation()
loc.path.length = 5
@ -91,6 +102,29 @@ export function getTestRunsLink (space: Ref<TestProject>, parentDoc: Ref<Doc>):
return loc
}
export function onModeChanged (newMode: string): void {
const loc = getCurrentResolvedLocation()
const { assignee, ...baseQuery } = loc.query ?? {}
const currentUser = getCurrentAccount()?.person
if (currentUser === undefined) {
console.error('Current user is not defined')
return
}
switch (newMode) {
case testManagement.mode.AllTests:
loc.query = baseQuery
break
case testManagement.mode.MyTests:
loc.query = { ...baseQuery, assignee: currentUser }
}
navigate(loc)
}
export function getCurrentMode (loc: Location): string {
const { assignee } = loc.query ?? {}
return assignee === getCurrentAccount()?.person ? testManagement.mode.MyTests : testManagement.mode.AllTests
}
export async function resolveLocation (loc: Location): Promise<ResolvedLocation | undefined> {
if (loc.path[2] !== testManagementId) {
return undefined

View File

@ -13,15 +13,19 @@
// limitations under the License.
//
import { Analytics } from '@hcengineering/analytics'
import type { Doc, DocumentQuery, Ref } from '@hcengineering/core'
import { showPopup } from '@hcengineering/ui'
import { type TestProject, type TestCase, type TestSuite } from '@hcengineering/test-management'
import { showPopup, showPanel } from '@hcengineering/ui'
import { type TestProject, type TestCase, type TestSuite, type TestResult } from '@hcengineering/test-management'
import testManagement from '@hcengineering/test-management'
import CreateTestSuiteComponent from './components/test-suite/CreateTestSuite.svelte'
import EditTestSuiteComponent from './components/test-suite/EditTestSuite.svelte'
import CreateTestCase from './components/test-case/CreateTestCase.svelte'
import CreateProject from './components/project/CreateProject.svelte'
import CreateTestRun from './components/test-run/CreateTestRun.svelte'
import { getTestRunIdFromLocation } from './navigation'
import { initializeIterator } from './components/test-result/store/testIteratorStore'
export async function showCreateTestSuitePopup (
space: Ref<TestProject> | undefined,
@ -50,6 +54,30 @@ export async function showCreateTestRunPopup (options: {
showPopup(CreateTestRun, options, 'top')
}
export async function showTestRunnerPanel (options: {
query?: DocumentQuery<TestResult>
space: Ref<TestProject>
selectedDocs?: TestResult[]
}): Promise<void> {
try {
const { query, space, selectedDocs } = options
await initializeIterator({
query: { ...query, space },
options: {
lookup: {
testCase: testManagement.class.TestCase
}
},
docs: selectedDocs
})
const testRunId = getTestRunIdFromLocation()
showPanel(testManagement.component.TestRunner, testRunId, testManagement.class.TestRun, 'content')
} catch (err: any) {
Analytics.handleError(err)
console.error('Failed to initialize test runner', err)
}
}
export async function CreateChildTestSuiteAction (doc: TestSuite): Promise<void> {
await showCreateTestSuitePopup(doc.space, doc._id)
}

View File

@ -187,7 +187,10 @@ export const testManagementPlugin = plugin(testManagementId, {
TestCaseDescription: '' as IntlString,
TestResultAttributes: '' as IntlString,
GoToNextTest: '' as IntlString,
GoToNextTestTooltip: '' as IntlString
GoToNextTestTooltip: '' as IntlString,
AllTests: '' as IntlString,
MyTests: '' as IntlString,
Comments: '' as IntlString
},
category: {
TestManagement: '' as Ref<ActionCategory>
@ -206,11 +209,17 @@ export const testManagementPlugin = plugin(testManagementId, {
TestResultStatusPresenter: '' as AnyComponent,
TestResultStatusEditor: '' as AnyComponent,
TestRunResult: '' as AnyComponent,
TestResultHeader: '' as AnyComponent
TestResultHeader: '' as AnyComponent,
TestRunner: '' as AnyComponent
},
ids: {
NoParent: '' as Ref<TestSuite>,
TestCaseUpdatedActivityViewlet: '' as Ref<TestCase>
TestCaseUpdatedActivityViewlet: '' as Ref<TestCase>,
NoTestRun: '' as Ref<TestRun>
},
mode: {
AllTests: '' as IntlString,
MyTests: '' as IntlString
},
spaceType: {
TestCaseType: '' as Ref<SpaceType>,

View File

@ -49,7 +49,7 @@ describe('ObjectIterator', () => {
const _class: Ref<Class<Doc>> = { id: 'class1' } as any
// eslint-disable-next-line no-new
new ObjectIterator(_class, query, storeAdapter)
new ObjectIterator(_class, storeAdapter, { query })
expect(storeAdapter.set).toHaveBeenCalledWith({
query,
currentObjects: [],
@ -64,7 +64,7 @@ describe('ObjectIterator', () => {
const query: DocumentQuery<Doc> = { key: 'value' }
const _class: Ref<Class<Doc>> = { id: 'class1' } as any
const iterator = new ObjectIterator(_class, query, storeAdapter)
const iterator = new ObjectIterator(_class, storeAdapter, { query })
await iterator.loadObjects(undefined)
expect(findAll).toHaveBeenCalledWith(_class, query, {
@ -81,7 +81,7 @@ describe('ObjectIterator', () => {
mockObjects = [{ id: '1' }, { id: '2' }, { id: '3' }] as any
const query: DocumentQuery<Doc> = { key: 'value' }
const _class: Ref<Class<Doc>> = { id: 'class1' } as any
const iterator = new ObjectIterator(_class, query, storeAdapter)
const iterator = new ObjectIterator(_class, storeAdapter, { query })
await iterator.loadObjects(undefined)
let nextObject = iterator.next()
@ -102,7 +102,7 @@ describe('ObjectIterator', () => {
mockObjects = [] as any
const query: DocumentQuery<Doc> = { key: 'value' }
const _class: Ref<Class<Doc>> = { id: 'class1' } as any
const iterator = new ObjectIterator(_class, query, storeAdapter)
const iterator = new ObjectIterator(_class, storeAdapter, { query })
await iterator.loadObjects(undefined)
const nextObject = iterator.next()
@ -114,10 +114,10 @@ describe('ObjectIterator', () => {
const _class: Ref<Class<Doc>> = { id: 'class1' } as any
const provider = new ObjectIteratorProvider(storeAdapter)
await provider.initialize(_class, query, undefined)
await provider.initialize(_class, { query })
const firstIterator = provider.getIterator()
await provider.initialize(_class, query, undefined)
await provider.initialize(_class, { query })
const secondIterator = provider.getIterator()
expect(firstIterator).toBe(secondIterator)
@ -129,7 +129,7 @@ describe('ObjectIterator', () => {
const _class: Ref<Class<Doc>> = { id: 'class1' } as any
const provider = new ObjectIteratorProvider(storeAdapter)
await provider.initialize(_class, query, undefined)
await provider.initialize(_class, { query })
provider.reset()
expect(storeAdapter.set).toHaveBeenCalledWith({

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type { DocumentQuery, Doc, Ref, Class } from '@hcengineering/core'
import type { DocumentQuery, Doc, Ref, Class, FindOptions } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
export interface IteratorState<T extends Doc> {
@ -28,10 +28,16 @@ export interface StoreAdapter<T extends Doc> {
get: () => IteratorState<T>
}
export function getDefaultIteratorState<T extends Doc> (query: DocumentQuery<T>): IteratorState<T> {
export interface IteratorParams<T extends Doc> {
docs?: T[]
query?: DocumentQuery<T>
options?: FindOptions<T> | undefined
}
export function getDefaultIteratorState<T extends Doc> (params: IteratorParams<T>): IteratorState<T> {
return {
query,
currentObjects: [],
query: params.query ?? {},
currentObjects: params.docs ?? [],
iteratorIndex: 0,
limit: 100
}
@ -41,25 +47,23 @@ export class ObjectIterator<T extends Doc> {
private readonly storeAdapter: StoreAdapter<T>
private readonly class: Ref<Class<T>>
constructor (_class: Ref<Class<T>>, query: DocumentQuery<T>, storeAdapter: StoreAdapter<T>) {
constructor (_class: Ref<Class<T>>, storeAdapter: StoreAdapter<T>, params: IteratorParams<T>) {
this.class = _class
this.storeAdapter = storeAdapter
this.storeAdapter.set(getDefaultIteratorState<T>(query))
this.storeAdapter.set(getDefaultIteratorState<T>(params))
}
async loadObjects (currentObject: Ref<Doc> | undefined): Promise<void> {
async loadObjects (options?: FindOptions<T> | undefined): Promise<void> {
const client = getClient()
const { query, limit } = this.storeAdapter.get()
const testResults = await client.findAll(this.class, query, {
...options,
limit,
total: true
})
this.storeAdapter.update((store) => {
store.currentObjects = [...store.currentObjects, ...testResults]
store.limit = testResults.total
if (currentObject !== undefined) {
store.iteratorIndex = store.currentObjects.findIndex((obj) => obj._id === currentObject) ?? 0
}
return store
})
}
@ -68,8 +72,8 @@ export class ObjectIterator<T extends Doc> {
let nextObject
this.storeAdapter.update((store) => {
if (store.iteratorIndex < store.currentObjects.length) {
store.iteratorIndex += 1
nextObject = store.currentObjects[store.iteratorIndex]
store.iteratorIndex += 1
}
return store
})
@ -78,7 +82,7 @@ export class ObjectIterator<T extends Doc> {
hasNext (): boolean {
const { currentObjects, iteratorIndex } = this.storeAdapter.get()
return iteratorIndex < currentObjects.length - 1
return iteratorIndex < currentObjects.length
}
}
@ -87,10 +91,12 @@ export class ObjectIteratorProvider<T extends Doc> {
constructor (private readonly storeAdapter: StoreAdapter<T>) {}
async initialize (_class: Ref<Class<T>>, query: DocumentQuery<T>, currentObject: Ref<Doc> | undefined): Promise<void> {
async initialize (_class: Ref<Class<T>>, params: IteratorParams<T>): Promise<void> {
if (this.objectIterator === undefined) {
this.objectIterator = new ObjectIterator(_class, query, this.storeAdapter)
await this.objectIterator.loadObjects(currentObject)
this.objectIterator = new ObjectIterator(_class, this.storeAdapter, params)
if (params.docs === undefined || params.docs.length === 0) {
await this.objectIterator.loadObjects(params.options)
}
}
}

View File

@ -85,6 +85,7 @@ import {
isDataField,
isOwner,
type JoinProps,
NumericTypes,
parseDoc,
parseDocWithProjection,
parseUpdate,
@ -920,7 +921,12 @@ abstract class PostgresAdapterBase implements DbAdapter {
continue
}
if (typeof val === 'number') {
res.push(`${this.getKey(_class, baseDomain, key, joins)} ${val === 1 ? 'ASC' : 'DESC'}`)
const attr = this.hierarchy.findAttribute(_class, key)
if (attr !== undefined && NumericTypes.includes(attr.type._class)) {
res.push(`(${this.getKey(_class, baseDomain, key, joins)})::numeric ${val === 1 ? 'ASC' : 'DESC'}`)
} else {
res.push(`${this.getKey(_class, baseDomain, key, joins)} ${val === 1 ? 'ASC' : 'DESC'}`)
}
} else {
// todo handle custom sorting
}

View File

@ -64,6 +64,13 @@ export async function retryTxn (
})
}
export const NumericTypes = [
core.class.TypeNumber,
core.class.TypeTimestamp,
core.class.TypeDate,
core.class.Collection
]
export async function createTables (client: postgres.Sql, domains: string[]): Promise<void> {
const filtered = domains.filter((d) => !loadedDomains.has(d))
if (filtered.length === 0) {