.
:node="child"
:depth="depth + 1"
:mean-elapsed-time="meanElapsedTime ?? node.node.task?.meanElapsedTime"
- v-bind="{ hoverable, autoExpandTypes, cyclePointsOrderDesc, indent }"
- v-on="passthroughHandlers"
+ v-bind="{ hoverable, autoExpandTypes, cyclePointsOrderDesc, expandAll, filteredOutNodesCache, indent }"
/>
@@ -193,19 +192,6 @@ import {
} from '@/utils/tasks'
import { getNodeChildren } from '@/components/cylc/tree/util'
-/**
- * Events that are passed through up the chain from child TreeItems.
- *
- * i.e. they are re-emitted by this TreeItem when they occur on a
- * child TreeItem, all the way up to the parent Tree component.
- */
-const passthroughEvents = [
- 'tree-item-created',
- 'tree-item-destroyed',
- 'tree-item-expanded',
- 'tree-item-collapsed',
-]
-
export const defaultNodeIndent = 28 // px
/** Margin between expand/collapse btn & node content */
@@ -239,11 +225,21 @@ export default {
default: true
},
hoverable: Boolean,
+ /** Render expanded initially if node is one of these types. */
autoExpandTypes: {
type: Array,
required: false,
default: () => ['workflow', 'cycle', 'family']
},
+ /** When this changes, will expand if node is one of these types, otherwise collapse. */
+ expandAll: {
+ type: Array,
+ required: false,
+ },
+ filteredOutNodesCache: {
+ type: WeakMap,
+ required: true,
+ },
/** Indent in px; default is expand/collapse btn width */
indent: {
type: Number,
@@ -257,15 +253,8 @@ export default {
},
},
- emits: [
- ...passthroughEvents
- ],
-
data () {
return {
- active: false,
- selected: false,
- filtered: true,
manuallyExpanded: null,
}
},
@@ -311,7 +300,6 @@ export default {
nodeClass () {
return {
'node--hoverable': this.hoverable,
- 'node--active': this.active,
expanded: this.isExpanded
}
},
@@ -334,39 +322,19 @@ export default {
},
},
- created () {
- this.$emit('tree-item-created', this)
- this.passthroughHandlers = Object.fromEntries(
- passthroughEvents.map((eventName) => (
- [eventName, (treeItem) => this.$emit(eventName, treeItem)]
- ))
- )
- },
-
- beforeUnmount () {
- this.$emit('tree-item-destroyed', this)
- },
-
- beforeMount () {
- this.emitExpandCollapseEvent(this.isExpanded)
+ watch: {
+ expandAll (nodeTypes) {
+ if (nodeTypes?.includes(this.node.type)) {
+ this.isExpanded = true
+ } else if (nodeTypes?.length === 0) {
+ this.isExpanded = false // manually collapsed
+ }
+ }
},
methods: {
toggleExpandCollapse () {
this.isExpanded = !this.isExpanded
- this.emitExpandCollapseEvent(this.isExpanded)
- },
- /**
- * Emits an event `tree-item-expanded` if `expanded` is true, or emits
- * `tree-item-collapsed` if `expanded` is false.
- * @param {boolean} expanded whether the node is expanded or not
- */
- emitExpandCollapseEvent (expanded) {
- if (expanded) {
- this.$emit('tree-item-expanded', this)
- } else {
- this.$emit('tree-item-collapsed', this)
- }
},
latestJob
},
diff --git a/src/model/JobState.model.js b/src/model/JobState.model.js
index 81870882d..64746c284 100644
--- a/src/model/JobState.model.js
+++ b/src/model/JobState.model.js
@@ -40,4 +40,6 @@ class JobState extends Enumify {
}
}
+export const JobStateNames = JobState.enumValues.map(({ name }) => name)
+
export default JobState
diff --git a/src/model/TaskState.model.js b/src/model/TaskState.model.js
index 7b3358ae9..63501409a 100644
--- a/src/model/TaskState.model.js
+++ b/src/model/TaskState.model.js
@@ -55,12 +55,6 @@ export const TaskStateUserOrder = [
TaskState.EXPIRED
]
-export const JobStates = [
- TaskState.SUBMITTED,
- TaskState.RUNNING,
- TaskState.SUCCEEDED,
- TaskState.FAILED,
- TaskState.SUBMIT_FAILED
-]
+export const TaskStateNames = TaskStateUserOrder.map(({ name }) => name)
export default TaskState
diff --git a/src/services/mock/json/index.cjs b/src/services/mock/json/index.cjs
index b974ceea5..29951a5a6 100644
--- a/src/services/mock/json/index.cjs
+++ b/src/services/mock/json/index.cjs
@@ -25,6 +25,8 @@ const { LogData } = require('./logData.cjs')
const { LogFiles } = require('./logFiles.cjs')
const analysisQuery = require('./analysisQuery.json')
+const workflows = [workflowOne, ...workflowsMulti]
+
module.exports = {
IntrospectionQuery,
taskProxy,
@@ -32,7 +34,10 @@ module.exports = {
userProfile,
LogData,
LogFiles,
- App: [workflowOne, ...workflowsMulti],
- Workflow: workflowOne,
+ App: workflows,
+ Workflow ({ workflowId }) {
+ return workflows.find(({ deltas }) => deltas.id === workflowId) || {}
+ },
+ Test: workflowOne,
analysisQuery
}
diff --git a/src/services/mock/json/workflows/multi.json b/src/services/mock/json/workflows/multi.json
index f85a30173..871918d17 100644
--- a/src/services/mock/json/workflows/multi.json
+++ b/src/services/mock/json/workflows/multi.json
@@ -61,13 +61,13 @@
"added": {
"workflow": {
"id": "~user/other/multi/run2",
- "status": "stopped",
- "statusMsg": "not yet run",
+ "status": "paused",
+ "statusMsg": "paused",
"owner": "user",
- "host": "",
- "port": 0,
+ "host": "ncc1701.starfleet.gov",
+ "port": 43078,
"stateTotals": {
- "waiting": 0,
+ "waiting": 2,
"expired": 0,
"preparing": 0,
"submit-failed": 0,
@@ -76,9 +76,103 @@
"failed": 0,
"succeeded": 0
},
- "latestStateTasks": {},
+ "latestStateTasks": {
+ "failed": [],
+ "preparing": [],
+ "submit-failed": [],
+ "submitted": [],
+ "running": []
+ },
"__typename": "Workflow"
},
+ "cyclePoints": [
+ {
+ "__typename": "FamilyProxy",
+ "id": "~user/other/multi/run2//1/root",
+ "state": "waiting",
+ "ancestors": [],
+ "childTasks": [
+ {
+ "id": "~user/other/multi/run2//1/foo",
+ "__typename": "TaskProxy"
+ }
+ ]
+ },
+ {
+ "__typename": "FamilyProxy",
+ "id": "~user/other/multi/run2//2/root",
+ "state": "waiting",
+ "ancestors": [],
+ "childTasks": [
+ {
+ "id": "~user/other/multi/run2//2/foo",
+ "__typename": "TaskProxy"
+ }
+ ]
+ }
+ ],
+ "familyProxies": [
+ {
+ "__typename": "FamilyProxy",
+ "id": "~user/other/multi/run2//1/root",
+ "state": "waiting",
+ "ancestors": [],
+ "childTasks": [
+ {
+ "id": "~user/other/multi/run2//1/foo",
+ "__typename": "TaskProxy"
+ }
+ ]
+ },
+ {
+ "__typename": "FamilyProxy",
+ "id": "~user/other/multi/run2//2/root",
+ "state": "waiting",
+ "ancestors": [],
+ "childTasks": [
+ {
+ "id": "~user/other/multi/run2//2/foo",
+ "__typename": "TaskProxy"
+ }
+ ]
+ }
+ ],
+ "taskProxies": [
+ {
+ "id": "~user/other/multi/run2//1/foo",
+ "state": "waiting",
+ "isHeld": false,
+ "isQueued": true,
+ "isRunahead": false,
+ "task": {
+ "meanElapsedTime": 0,
+ "__typename": "Task"
+ },
+ "firstParent": {
+ "id": "~user/other/multi/run2//1/root",
+ "__typename": "FamilyProxy"
+ },
+ "__typename": "TaskProxy"
+ },
+ {
+ "id": "~user/other/multi/run2//2/foo",
+ "state": "waiting",
+ "isHeld": false,
+ "isQueued": false,
+ "isRunahead": false,
+ "task": {
+ "meanElapsedTime": 0,
+ "__typename": "Task"
+ },
+ "firstParent": {
+ "id": "~user/other/multi/run2//2/root",
+ "__typename": "FamilyProxy"
+ },
+ "__typename": "TaskProxy"
+ }
+ ],
+ "jobs": [],
+ "edges": [],
"__typename": "Added"
}
}
diff --git a/src/services/mock/json/workflows/one.json b/src/services/mock/json/workflows/one.json
index 2795cb255..45ea00f2b 100644
--- a/src/services/mock/json/workflows/one.json
+++ b/src/services/mock/json/workflows/one.json
@@ -1,5 +1,6 @@
{
"deltas": {
+ "id": "~user/one",
"added": {
"workflow": {
"id": "~user/one",
diff --git a/src/store/workflows.module.js b/src/store/workflows.module.js
index 6de6ec7a4..7d1766e7c 100644
--- a/src/store/workflows.module.js
+++ b/src/store/workflows.module.js
@@ -253,7 +253,7 @@ function removeTree (state, node, removeParent = true) {
* */
function cleanParents (state, node) {
let pointer = node
- while (pointer.parent) {
+ while (pointer?.parent) {
if (pointer.type !== 'workflow') {
// don't prune workflow nodes
// (this requires an explicit instruction to do so)
diff --git a/src/views/Tree.vue b/src/views/Tree.vue
index f191e7c56..90acb3d68 100644
--- a/src/views/Tree.vue
+++ b/src/views/Tree.vue
@@ -17,29 +17,81 @@ along with this program. If not, see
.
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ {{ $options.icons.mdiPlus }}
+ Expand all
+
+
+ {{ $options.icons.mdiMinus }}
+ Collapse all
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/e2e/specs/aotf.cy.js b/tests/e2e/specs/aotf.cy.js
index c771bf71d..e171c7988 100644
--- a/tests/e2e/specs/aotf.cy.js
+++ b/tests/e2e/specs/aotf.cy.js
@@ -21,7 +21,7 @@ import {
} from '@/utils/aotf'
import {
MUTATIONS
-} from '../support/graphql'
+} from '$tests/e2e/support/graphql'
import { cloneDeep } from 'lodash'
function mockApolloClient () {
diff --git a/tests/e2e/specs/graphiql.cy.js b/tests/e2e/specs/graphiql.cy.js
index 650ad34b9..c5e96ea46 100644
--- a/tests/e2e/specs/graphiql.cy.js
+++ b/tests/e2e/specs/graphiql.cy.js
@@ -16,7 +16,7 @@
*/
/** query used for the graphiql test */
-const query = `query Workflow {
+const query = `query Test {
workflows {
id
}
diff --git a/tests/e2e/specs/gscan.cy.js b/tests/e2e/specs/gscan.cy.js
index e7265970e..fa5d6ec14 100644
--- a/tests/e2e/specs/gscan.cy.js
+++ b/tests/e2e/specs/gscan.cy.js
@@ -27,7 +27,7 @@ describe('GScan component', () => {
.should('have.length', 5)
})
- it('should filter by workflow name', () => {
+ it('filters by workflow name', () => {
cy.get('#c-gscan-search-workflows')
.type('level')
.get('.treeitem:visible')
@@ -38,29 +38,34 @@ describe('GScan component', () => {
.should('have.length', 0)
})
- it('should filter by workflow state', () => {
+ it('filters by workflow state', () => {
cy.get('[data-cy=gscan-filter-btn]')
.click()
.get('[data-cy="filter workflow state"]')
.click()
cy.get('.v-select__content')
- .contains('.v-list-item', 'paused')
+ .contains('.v-list-item', 'stopping')
.click({ force: true })
.get('.treeitem:visible')
.should('have.length', 0)
cy.get('.v-select__content')
- .contains('.v-list-item', 'running')
+ .contains('.v-list-item', 'paused')
.click({ force: true })
- .get('.treeitem:visible')
+ .get('.treeitem [data-c-interactive]:visible')
.should('have.length', 1)
+ cy.get('.v-select__content')
+ .contains('.v-list-item', 'running')
+ .click({ force: true })
+ .get('.treeitem [data-c-interactive]:visible')
+ .should('have.length', 2)
cy.get('.v-select__content')
.contains('.v-list-item', 'stopped')
.click({ force: true })
- .get('.treeitem:visible')
- .should('have.length', 5)
+ .get('.treeitem [data-c-interactive]:visible')
+ .should('have.length', 4)
})
- it('should filter by task state', () => {
+ it('filters by task state', () => {
cy.get('[data-cy=gscan-filter-btn]')
.click()
.get('[data-cy="filter task state"]')
@@ -72,7 +77,7 @@ describe('GScan component', () => {
.should('have.length', 0)
})
- it('should filter by workflow name, state, and tasks states', () => {
+ it('filters by workflow name, state, and tasks states', () => {
cy.get('#c-gscan-search-workflows')
.type('on')
cy.get('[data-cy=gscan-filter-btn]')
diff --git a/tests/e2e/specs/mutation.cy.js b/tests/e2e/specs/mutation.cy.js
index 43303f873..aa8ea1763 100644
--- a/tests/e2e/specs/mutation.cy.js
+++ b/tests/e2e/specs/mutation.cy.js
@@ -19,8 +19,8 @@ import { processMutations } from '@/utils/aotf'
import { cloneDeep, upperFirst } from 'lodash'
import {
MUTATIONS
-} from '../support/graphql'
-import { Deferred } from '../../util'
+} from '$tests/e2e/support/graphql'
+import { Deferred } from '$tests/util'
describe('Mutations component', () => {
beforeEach(() => {
diff --git a/tests/e2e/specs/tree.cy.js b/tests/e2e/specs/tree.cy.js
index 5b407d11f..366b8a06e 100644
--- a/tests/e2e/specs/tree.cy.js
+++ b/tests/e2e/specs/tree.cy.js
@@ -62,7 +62,7 @@ describe('Tree view', () => {
})
it('Displays job details when expanded', () => {
// this is testing that there is a margin, not necessarily that the leaf node's triangle is exactly under the node
- cy.visit('/#/workspace/one')
+ cy.visit('/#/tree/one')
cy.get('.node-data-task')
.contains('eventually_succeeded')
.parents('.node')
@@ -272,6 +272,34 @@ describe('Tree view', () => {
.get('@sleepyTask')
.should('be.visible')
})
+
+ it('Does not expand jobs but can collapse them', () => {
+ cy.visit('/#/tree/one')
+ .get('[data-cy=expand-all]')
+ .click()
+ .get('.node-data-job:first')
+ .should('not.exist')
+ cy.get('.node-data-task')
+ .contains('failed')
+ .parents('.node')
+ .find('.node-expand-collapse-button')
+ .click()
+ .get('.node-data-job:first')
+ .should('be.visible')
+ cy.get('[data-cy=expand-all]')
+ .click()
+ // The job should remain expanded
+ .get('.node-data-job:first')
+ .should('be.visible')
+ cy.get('[data-cy=collapse-all]')
+ .click()
+ .get('[data-cy=expand-all]')
+ .click()
+ // The job should be collapsed now
+ .get('.node-data-job:first')
+ .should('not.be.visible')
+ })
+
it('Works when tasks are being filtered', () => {
cy.visit('/#/tree/one')
cy.get('.node-data-task')
diff --git a/tests/e2e/specs/userprofile.cy.js b/tests/e2e/specs/userprofile.cy.js
index 57b55fae2..3e5493ed5 100644
--- a/tests/e2e/specs/userprofile.cy.js
+++ b/tests/e2e/specs/userprofile.cy.js
@@ -133,6 +133,9 @@ describe('User Profile', () => {
.get('[data-cy=select-default-view-menu] [role=listbox]')
.contains('Table')
.click()
+ // Wait for menu to close before navigation to avoid FF ResizeObserver error
+ .get('[data-cy=select-default-view-menu]')
+ .should('not.exist')
cy.visit('/#/workspace/one')
.get('[data-cy=workspace-view] .c-table')
.should('be.visible')
diff --git a/tests/e2e/specs/workflowservice.cy.js b/tests/e2e/specs/workflowservice.cy.js
index 8a2675ba4..2c15e27c4 100644
--- a/tests/e2e/specs/workflowservice.cy.js
+++ b/tests/e2e/specs/workflowservice.cy.js
@@ -15,7 +15,7 @@
* along with this program. If not, see
.
*/
-import { Deferred } from '../../util'
+import { Deferred } from '$tests/util'
// Tests for the WorkflowService subscriptions. Not necessarily GraphQL subscriptions!
diff --git a/tests/unit/components/cylc/gscan/gscan.vue.spec.js b/tests/unit/components/cylc/gscan/gscan.vue.spec.js
index 191e475bf..f0bb7c54d 100644
--- a/tests/unit/components/cylc/gscan/gscan.vue.spec.js
+++ b/tests/unit/components/cylc/gscan/gscan.vue.spec.js
@@ -15,26 +15,38 @@
* along with this program. If not, see
.
*/
+import { mount } from '@vue/test-utils'
import { createStore } from 'vuex'
+import { createVuetify } from 'vuetify'
import storeOptions from '@/store/options'
+import GScan from '@/components/cylc/gscan/GScan.vue'
+import CylcObjectPlugin from '@/components/cylc/cylcObject/plugin'
import {
WorkflowState,
WorkflowStateOrder
} from '@/model/WorkflowState.model'
import TaskState from '@/model/TaskState.model'
-import GScan from '@/components/cylc/gscan/GScan.vue'
import {
getWorkflowTreeSortValue,
sortedWorkflowTree
} from '@/components/cylc/gscan/sort.js'
-import {
- filterHierarchically
-} from '@/components/cylc/gscan/filters'
import {
TEST_TREE,
listTree
} from './utils'
+import { getIDMap } from '$tests/util'
+
+const vuetify = createVuetify()
+
+/**
+ * Helper function to run filtering.
+ */
+function filterNodes (wrapper, filteredOutNodesCache) {
+ for (const node of wrapper.vm.workflows) {
+ wrapper.vm.filterNode(node, filteredOutNodesCache)
+ }
+}
describe('GScan component', () => {
const store = createStore(storeOptions)
@@ -44,7 +56,7 @@ describe('GScan component', () => {
beforeEach(resetState)
describe('Sorting', () => {
- it('should set workflow sort order by status', () => {
+ it('sets workflow sort order by status', () => {
// for each worflow state ...
for (const workflowState of WorkflowState) {
// ... except ERROR
@@ -81,7 +93,8 @@ describe('GScan component', () => {
).to.equal(WorkflowStateOrder.get(workflowState.name))
}
})
- it('should sort workflows', () => {
+
+ it('sorts workflows', () => {
// it should sort by status then name
expect(
listTree(sortedWorkflowTree(TEST_TREE))
@@ -92,150 +105,158 @@ describe('GScan component', () => {
})
describe('Filters', () => {
- it("shouldn't filter out workflows incorrectly", () => {
- expect(
- listTree(
- filterHierarchically(
- sortedWorkflowTree(TEST_TREE),
- // don't filter by name
- null,
- // filter for all workflow states
- [...WorkflowStateOrder.keys()],
- // filter for all task states
- []
- )
- )
- ).to.deep.equal(['~u/b', '~u/c', '~u/a/x1', '~u/a/x2'])
+ const mountFunction = (options) => mount(GScan, {
+ global: {
+ plugins: [vuetify, CylcObjectPlugin],
+ },
+ props: {
+ workflowTree: TEST_TREE,
+ isLoading: false,
+ },
+ ...options
})
- it('should filter by workflow state', () => {
- expect(
- listTree(
- filterHierarchically(
- sortedWorkflowTree(TEST_TREE),
- // don't filter by name
- null,
- [WorkflowState.RUNNING.name],
- // filter for all task states
- []
- )
- )
- ).to.deep.equal(['~u/c'])
- expect(
- listTree(
- filterHierarchically(
- sortedWorkflowTree(TEST_TREE),
- // don't filter by name
- null,
- [WorkflowState.STOPPING.name],
- // filter for all task states
- []
- )
- )
- ).to.deep.equal(['~u/b'])
- expect(
- listTree(
- filterHierarchically(
- sortedWorkflowTree(TEST_TREE),
- // don't filter by name
- null,
- [WorkflowState.STOPPED.name],
- // filter for all task states
- []
- )
- )
- ).to.deep.equal(['~u/a/x1', '~u/a/x2'])
- })
- it('should filter by workflow name', () => {
- expect(
- listTree(
- filterHierarchically(
- sortedWorkflowTree(TEST_TREE),
- 'x',
- // filter for all workflow states
- [...WorkflowStateOrder.keys()],
- // filter for all task states
- []
- )
- )
- ).to.deep.equal(['~u/a/x1', '~u/a/x2'])
- // check it isn't matching the user name
- expect(
- listTree(
- filterHierarchically(
- sortedWorkflowTree(TEST_TREE),
- 'u',
- // filter for all workflow states
- [...WorkflowStateOrder.keys()],
- // filter for all task states
- []
- )
- )
- ).to.deep.equal([])
+
+ it('has null filterState when filters are empty', async () => {
+ const wrapper = mountFunction()
+ expect(wrapper.vm.searchWorkflows).toEqual('')
+ expect(wrapper.vm.filters).toEqual({
+ 'workflow state': [],
+ 'task state': [],
+ })
+ await wrapper.setData({
+ searchWorkflows: ' ',
+ filters: {
+ 'workflow state': [],
+ 'task state': [],
+ }
+ })
+ expect(wrapper.vm.filterState).toBeNull()
})
- it('should filter by workflow state totals', () => {
- expect(
- listTree(
- filterHierarchically(
- sortedWorkflowTree(TEST_TREE),
- null,
- // filter for all workflow states
- [...WorkflowStateOrder.keys()],
- // filter for all task states
- [TaskState.RUNNING.name]
- )
- )
- ).to.deep.equal(['~u/b'])
- expect(
- listTree(
- filterHierarchically(
- sortedWorkflowTree(TEST_TREE),
- null,
- // filter for all workflow states
- [...WorkflowStateOrder.keys()],
- // filter for all task states
- [TaskState.SUBMITTED.name]
- )
- )
- ).to.deep.equal(['~u/c'])
+
+ it("shouldn't filter out workflows incorrectly", async () => {
+ const wrapper = mountFunction()
+ const filteredOutNodesCache = new Map()
+ // filter for all workflow states
+ await wrapper.setData({
+ filters: { 'workflow state': WorkflowStateOrder.keys() },
+ })
+ filterNodes(wrapper, filteredOutNodesCache)
+ expect(getIDMap(filteredOutNodesCache)).toEqual({
+ '~u/a': false,
+ '~u/a/x1': false,
+ '~u/a/x2': false,
+ '~u/b': false,
+ '~u/c': false,
+ })
})
- })
- describe('Toggle items values', () => {
- it('should toggle items values to true', () => {
- const items = [
- {
- model: false
+ it('filters by workflow state', async () => {
+ const wrapper = mountFunction()
+ const filteredOutNodesCache = new Map()
+
+ await wrapper.setData({
+ filters: { 'workflow state': [WorkflowState.RUNNING.name] },
+ })
+ filterNodes(wrapper, filteredOutNodesCache)
+ expect(getIDMap(filteredOutNodesCache)).toEqual({
+ '~u/a': true,
+ '~u/a/x1': true,
+ '~u/a/x2': true,
+ '~u/b': true,
+ '~u/c': false,
+ })
+
+ await wrapper.setData({
+ filters: {
+ 'workflow state': [
+ WorkflowState.STOPPING.name,
+ WorkflowState.STOPPED.name,
+ ]
},
- {
- model: false
- }
- ]
- GScan.methods.toggleItemsValues(items)
- expect(items.every(item => item.model))
+ })
+ filterNodes(wrapper, filteredOutNodesCache)
+ expect(getIDMap(filteredOutNodesCache)).toEqual({
+ '~u/a': false,
+ '~u/a/x1': false,
+ '~u/a/x2': false,
+ '~u/b': false,
+ '~u/c': true,
+ })
})
- it('should toggle items values to false', () => {
- const items = [
- {
- model: true
- },
- {
- model: true
- }
- ]
- GScan.methods.toggleItemsValues(items)
- expect(!items.every(item => item.model))
+
+ it('filters by workflow name', async () => {
+ const wrapper = mountFunction()
+ const filteredOutNodesCache = new Map()
+
+ await wrapper.setData({ searchWorkflows: 'x' })
+ filterNodes(wrapper, filteredOutNodesCache)
+ expect(getIDMap(filteredOutNodesCache)).toEqual({
+ '~u/a': false,
+ '~u/a/x1': false,
+ '~u/a/x2': false,
+ '~u/b': true,
+ '~u/c': true,
+ })
+
+ await wrapper.setData({ searchWorkflows: 'u' })
+ filterNodes(wrapper, filteredOutNodesCache)
+ expect(getIDMap(filteredOutNodesCache)).toEqual({
+ '~u/a': true,
+ '~u/a/x1': true,
+ '~u/a/x2': true,
+ '~u/b': true,
+ '~u/c': true,
+ })
})
- it('should toggle items values to false (mixed values)', () => {
- const items = [
- {
- model: true
+
+ it('filters by task state', async () => {
+ const wrapper = mountFunction()
+ const filteredOutNodesCache = new Map()
+
+ await wrapper.setData({
+ filters: { 'task state': [TaskState.RUNNING.name] }
+ })
+ filterNodes(wrapper, filteredOutNodesCache)
+ expect(getIDMap(filteredOutNodesCache)).toEqual({
+ '~u/a': true,
+ '~u/a/x1': true,
+ '~u/a/x2': true,
+ '~u/b': false,
+ '~u/c': true,
+ })
+
+ await wrapper.setData({
+ filters: { 'task state': [TaskState.SUBMITTED.name] }
+ })
+ filterNodes(wrapper, filteredOutNodesCache)
+ expect(getIDMap(filteredOutNodesCache)).toEqual({
+ '~u/a': true,
+ '~u/a/x1': true,
+ '~u/a/x2': true,
+ '~u/b': true,
+ '~u/c': false,
+ })
+ })
+
+ it('filters by workflow name & workflow state & task state', async () => {
+ const wrapper = mountFunction()
+ const filteredOutNodesCache = new Map()
+
+ await wrapper.setData({
+ searchWorkflows: 'a',
+ filters: {
+ 'workflow state': [WorkflowState.STOPPED.name],
+ 'task state': [TaskState.SUBMIT_FAILED.name],
},
- {
- model: false
- }
- ]
- GScan.methods.toggleItemsValues(items)
- expect(!items.every(item => item.model))
+ })
+ filterNodes(wrapper, filteredOutNodesCache)
+ expect(getIDMap(filteredOutNodesCache)).toEqual({
+ '~u/a': false,
+ '~u/a/x1': false,
+ '~u/a/x2': true,
+ '~u/b': true,
+ '~u/c': true,
+ })
})
})
})
diff --git a/tests/unit/components/cylc/gscan/utils.js b/tests/unit/components/cylc/gscan/utils.js
index 0886442d5..96765575b 100644
--- a/tests/unit/components/cylc/gscan/utils.js
+++ b/tests/unit/components/cylc/gscan/utils.js
@@ -17,52 +17,55 @@
import { WorkflowState } from '@/model/WorkflowState.model'
import TaskState from '@/model/TaskState.model'
+import { Tokens } from '@/utils/uid'
-const RUNNING_STATE_TOTALS = {}
-RUNNING_STATE_TOTALS[TaskState.RUNNING.name] = 1
-RUNNING_STATE_TOTALS[TaskState.SUBMITTED.name] = 0
+const RUNNING_STATE_TOTALS = {
+ [TaskState.RUNNING.name]: 1,
+ [TaskState.SUBMITTED.name]: 0,
+}
+
+const SUBMITTED_STATE_TOTALS = {
+ [TaskState.RUNNING.name]: 0,
+ [TaskState.SUBMITTED.name]: 1,
+}
-const SUBMITTED_STATE_TOTALS = {}
-SUBMITTED_STATE_TOTALS[TaskState.RUNNING.name] = 0
-SUBMITTED_STATE_TOTALS[TaskState.SUBMITTED.name] = 1
+const SUBMIT_FAILED_STATE_TOTALS = {
+ [TaskState.SUBMIT_FAILED.name]: 1,
+}
export const TEST_TREE = {
children: [
{
id: '~u',
type: 'user',
- tokens: {
- user: 'u'
- },
+ tokens: new Tokens('u'),
children: [
{
id: '~u/a',
name: 'a',
type: 'workflow-part',
- tokens: {
- user: 'u',
- workflow: 'a'
- },
+ tokens: new Tokens('~u/a'),
children: [
{
id: '~u/a/x1',
name: 'x1',
type: 'workflow',
- tokens: {
- user: 'u',
- workflow: 'a/x1'
- },
- node: { status: WorkflowState.STOPPED.name }
+ tokens: new Tokens('~u/a/x1'),
+ node: {
+ status: WorkflowState.STOPPED.name,
+ stateTotals: SUBMIT_FAILED_STATE_TOTALS,
+ latestStateTasks: [],
+ }
},
{
id: '~u/a/x2',
name: 'x2',
type: 'workflow',
- tokens: {
- user: 'u',
- workflow: 'a/x2'
- },
- node: { status: WorkflowState.STOPPED.name }
+ tokens: new Tokens('~u/a/x2'),
+ node: {
+ status: WorkflowState.STOPPED.name,
+ latestStateTasks: [],
+ }
}
]
},
@@ -70,26 +73,22 @@ export const TEST_TREE = {
id: '~u/b',
name: 'b',
type: 'workflow',
- tokens: {
- user: 'u',
- workflow: 'b'
- },
+ tokens: new Tokens('~u/b'),
node: {
status: WorkflowState.STOPPING.name,
- stateTotals: RUNNING_STATE_TOTALS
+ stateTotals: RUNNING_STATE_TOTALS,
+ latestStateTasks: [],
}
},
{
id: '~u/c',
name: 'c',
type: 'workflow',
- tokens: {
- user: 'u',
- workflow: 'c'
- },
+ tokens: new Tokens('~u/c'),
node: {
status: WorkflowState.RUNNING.name,
- stateTotals: SUBMITTED_STATE_TOTALS
+ stateTotals: SUBMITTED_STATE_TOTALS,
+ latestStateTasks: [],
}
}
]
@@ -97,18 +96,25 @@ export const TEST_TREE = {
]
}
-export function listTree (gscanTree) {
- // return a flat list of workflow ids for all workflow nodes in the
- // tree, preserving sort order
+/**
+ * Return a flat list of workflow ids for all workflow nodes in the
+ * tree, preserving sort order
+ *
+ * @param {Object} tree
+ * @param {boolean} filter - whether to remove items that have been filtered out
+ */
+export function listTree (tree, filter = false) {
const ret = []
- const stack = [...gscanTree]
+ const stack = [...tree]
let item
while (stack.length) {
item = stack.splice(0, 1)[0]
- if (['workflow-part', 'user'].includes(item.type)) {
- stack.push(...item.children)
- } else if (item.type === 'workflow') {
- ret.push(item.id)
+ if (!filter || !item.filteredOut) {
+ if (['workflow-part', 'user'].includes(item.type)) {
+ stack.push(...item.children)
+ } else if (item.type === 'workflow') {
+ ret.push(item.id)
+ }
}
}
return ret
diff --git a/tests/unit/components/cylc/tree/tree.data.js b/tests/unit/components/cylc/tree/tree.data.js
index fd50167d5..a728cb9d3 100644
--- a/tests/unit/components/cylc/tree/tree.data.js
+++ b/tests/unit/components/cylc/tree/tree.data.js
@@ -15,6 +15,8 @@
* along with this program. If not, see
.
*/
+import { Tokens } from '@/utils/uid'
+
/*
* Test data for Tree component tests.
*/
@@ -216,6 +218,7 @@ const simpleWorkflowTree4Nodes = [
{
id: '~user/workflow1',
name: 'workflow1',
+ tokens: new Tokens('~user/workflow1'),
type: 'workflow',
node: {
__typename: 'Workflow',
@@ -228,6 +231,7 @@ const simpleWorkflowTree4Nodes = [
{
id: '~user/workflow1//20100101T0000Z',
name: '20100101T0000Z',
+ tokens: new Tokens('~user/workflow1//20100101T0000Z'),
type: 'cycle',
node: {
__typename: 'CyclePoint',
@@ -238,6 +242,7 @@ const simpleWorkflowTree4Nodes = [
{
id: '~user/workflow1//20100101T0000Z/root',
name: 'root',
+ tokens: new Tokens('~user/workflow1//20100101T0000Z/root'),
type: 'family',
node: {
state: 'failed'
@@ -246,16 +251,17 @@ const simpleWorkflowTree4Nodes = [
{
id: '~user/workflow1//20100101T0000Z/foo',
name: 'foo',
+ tokens: new Tokens('~user/workflow1//20100101T0000Z/foo'),
type: 'task',
node: {
__typename: 'TaskProxy',
state: 'failed'
},
- expanded: false,
children: [
{
id: '~user/workflow1//20100101T0000Z/foo/01',
name: '01',
+ tokens: new Tokens('~user/workflow1//20100101T0000Z/foo/01'),
type: 'job',
node: {
__typename: 'Job',
diff --git a/tests/unit/components/cylc/tree/tree.vue.spec.js b/tests/unit/components/cylc/tree/tree.vue.spec.js
index c8e21d82e..10dcf7a81 100644
--- a/tests/unit/components/cylc/tree/tree.vue.spec.js
+++ b/tests/unit/components/cylc/tree/tree.vue.spec.js
@@ -17,307 +17,63 @@
// we mount the tree to include the TreeItem component and other vuetify children components
import { mount } from '@vue/test-utils'
+import { vi } from 'vitest'
import { createVuetify } from 'vuetify'
-import sinon from 'sinon'
+import { cloneDeep } from 'lodash'
import Tree from '@/components/cylc/tree/Tree.vue'
import { simpleWorkflowTree4Nodes } from './tree.data'
import CylcObjectPlugin from '@/components/cylc/cylcObject/plugin'
-import cloneDeep from 'lodash/cloneDeep'
-import WorkflowService from '@/services/workflow.service'
-const $eventBus = {
- emit () {}
-}
-const $workflowService = sinon.createStubInstance(WorkflowService)
const vuetify = createVuetify()
describe('Tree component', () => {
- /**
- * @param options
- * @returns {Wrapper
}
- */
- const mountFunction = (options) => mount(Tree, {
+ const mountFunction = (props) => mount(Tree, {
global: {
plugins: [vuetify, CylcObjectPlugin],
- mocks: {
- $eventBus,
- $workflowService
- }
},
- ...options
+ props: {
+ workflows: cloneDeep(simpleWorkflowTree4Nodes),
+ autoStripTypes: ['workflow'],
+ filterState: null,
+ ...props,
+ }
})
- it('should display the tree with valid data', () => {
+ it.each([
+ { autoStripTypes: [], expected: simpleWorkflowTree4Nodes },
+ { autoStripTypes: ['workflow'], expected: simpleWorkflowTree4Nodes[0].children },
+ ])('auto strips $autoStripTypes', ({ autoStripTypes, expected }) => {
const wrapper = mountFunction({
- props: {
- workflows: simpleWorkflowTree4Nodes[0].children
- }
- })
- expect(wrapper.props().workflows[0].node.__typename).to.equal('CyclePoint')
- expect(wrapper.find('div')).to.not.equal(null)
- })
- describe('Filter', () => {
- describe('Default', () => {
- it('should not filter by name or state by default', () => {
- const wrapper = mountFunction({
- props: {
- workflows: simpleWorkflowTree4Nodes[0].children
- }
- })
- expect(wrapper.vm.tasksFilter).to.deep.equal({})
- })
- })
- })
- describe('Caches', () => {
- /**
- * Create a tree item for tests with the caches.
- * @param {string} id - node ID
- * @param {boolean} expanded - whether node is expanded or not
- * @returns {{isExpanded: *, $props: {node: {id: *}}}}
- */
- const createTreeItem = (id, expanded) => {
- return {
- isExpanded: expanded,
- $props: {
- node: {
- id
- }
- }
- }
- }
- it('should all be initialized to empty caches', () => {
- const wrapper = mountFunction({
- props: {
- workflows: []
- }
- })
- expect(Object.keys(wrapper.vm.treeItemCache).length).to.equal(0)
- expect(wrapper.vm.expandedCache.size).to.equal(0)
- })
- it('should add to the tree item cache', () => {
- const wrapper = mountFunction({
- props: {
- workflows: []
- }
- })
- const treeItem = createTreeItem('1', false)
- wrapper.vm.onTreeItemCreated(treeItem)
- expect(Object.keys(wrapper.vm.treeItemCache).length).to.equal(1)
- expect(wrapper.vm.expandedCache.size).to.equal(0)
- })
- it('should remove from the tree item cache', () => {
- const wrapper = mountFunction({
- props: {
- workflows: []
- }
- })
- const treeItem = createTreeItem('1', false)
- wrapper.vm.onTreeItemCreated(treeItem)
- expect(Object.keys(wrapper.vm.treeItemCache).length).to.equal(1)
- wrapper.vm.onTreeItemDestroyed(treeItem)
- expect(Object.keys(wrapper.vm.treeItemCache).length).to.equal(0)
- })
- it('should add to the expanded cache', () => {
- const wrapper = mountFunction({
- props: {
- workflows: []
- }
- })
- const treeItem = createTreeItem('1', true)
- wrapper.vm.onTreeItemCreated(treeItem)
- expect(Object.keys(wrapper.vm.treeItemCache).length).to.equal(1)
- expect(wrapper.vm.expandedCache.size).to.equal(1)
- })
- it('should remove from the expanded cache', () => {
- const wrapper = mountFunction({
- props: {
- workflows: []
- }
- })
- const treeItem = createTreeItem('1', true)
- wrapper.vm.onTreeItemCreated(treeItem)
- expect(Object.keys(wrapper.vm.treeItemCache).length).to.equal(1)
- expect(wrapper.vm.expandedCache.size).to.equal(1)
- wrapper.vm.onTreeItemCollapsed(treeItem)
- expect(Object.keys(wrapper.vm.treeItemCache).length).to.equal(1)
- expect(wrapper.vm.expandedCache.size).to.equal(0)
+ autoStripTypes,
})
+ expect(wrapper.vm.rootChildren).toEqual(expected)
})
- describe('Expand-Collapse', () => {
- // Three collections, one with mixed (expanded/collapsed),
- // one with all items collapsed, and one with all items expanded.
- // Enough to cover the possible test scenarios for the
- // expand-collapse toggle functions.
- const mixed = {
- '~user/workflow': {
- id: '~user/workflow',
- type: 'workflow',
- isExpanded: false
- },
- '~user/workflow//1': {
- id: '~user/workflow//1',
- type: 'cyclepoint',
- isExpanded: true
- }
- }
- const allCollapsed = {
- '~user/workflow': {
- id: '~user/workflow',
- type: 'workflow',
- isExpanded: false
- },
- '~user/workflow//1': {
- id: '~user/workflow//1',
- type: 'cyclepoint',
- isExpanded: false
- }
- }
- const allExpanded = {
- '~user/workflow': {
- id: '~user/workflow',
- type: 'workflow',
- isExpanded: true
- },
- '~user/workflow//1': {
- id: '~user/workflow//1',
- type: 'cyclepoint',
- isExpanded: true
- }
- }
- const filterWorkflows = (item) => item.type === 'workflow'
- const filterCyclepoints = (item) => item.type === 'cyclepoint'
- const filterTaskProxies = (item) => item.type === 'task-proxy'
- it.each([
- // everything is expanded
- {
- filter: null,
- items: mixed,
- expectedExpandedItems: 2
- },
- {
- filter: null,
- items: allExpanded,
- expectedExpandedItems: 2
- },
- {
- filter: null,
- items: allCollapsed,
- expectedExpandedItems: 2
- },
- // will expand the collapsed workflow (found with the filter)
- {
- filter: filterWorkflows,
- items: allCollapsed,
- expectedExpandedItems: 1
- },
- // workflow will stay collapsed
- {
- filter: filterCyclepoints,
- items: allCollapsed,
- expectedExpandedItems: 1
- },
- // filter won't find any task-proxies, so everything is still collapsed
- {
- filter: filterTaskProxies,
- items: allCollapsed,
- expectedExpandedItems: 0
- },
- ])('should expand items %#', ({ filter, items, expectedExpandedItems }) => {
- // we clone the test data structures as the function mutates the objects
- const wrapper = mountFunction({
- props: {
- workflows: [],
- expandCollapseToggle: true
- },
- data () {
- return {
- treeItemCache: cloneDeep(items),
- }
- }
- })
- const { treeItemCache } = wrapper.vm
-
- wrapper.vm.expandAll(filter)
- expect(wrapper.vm.expanded).to.equal(true)
-
- const collection = filter ? Object.values(treeItemCache).filter(filter) : Object.values(treeItemCache)
- for (const item of collection) {
- expect(item.isExpanded).to.equal(true)
- }
- expect(
- Object.values(treeItemCache).filter(item => item.isExpanded).length,
- `Failed case: ${JSON.stringify(items)}`
- ).to.equal(expectedExpandedItems)
- })
-
- it.each([
- // everything is collapsed
- {
- filter: null,
- items: cloneDeep(mixed),
- expectedCollapsedItems: 2
- },
- {
- filter: null,
- items: cloneDeep(allExpanded),
- expectedCollapsedItems: 2
- },
- {
- filter: null,
- items: cloneDeep(allCollapsed),
- expectedCollapsedItems: 2
- },
- // will collapse the expanded workflow (found with the filter)
- {
- filter: filterWorkflows,
- items: cloneDeep(allExpanded),
- expectedCollapsedItems: 1
- },
- // workflow will stay expanded
- {
- filter: filterCyclepoints,
- items: cloneDeep(allExpanded),
- expectedCollapsedItems: 1
- },
- // filter won't find any task-proxies, so everything is still expanded
- {
- filter: filterTaskProxies,
- items: cloneDeep(allExpanded),
- expectedCollapsedItems: 0
- }
- ])('should collapse items %#', ({ filter, items, expectedCollapsedItems }) => {
- // we clone the test data structures as the function mutates the objects
+ describe('Filter', () => {
+ it('only runs filtering when applicable', async () => {
+ const nodeFilter = vi.fn()
const wrapper = mountFunction({
- props: {
- workflows: [],
- expandCollapseToggle: true
- },
- data () {
- return {
- treeItemCache: items,
- }
- }
+ nodeFilterFunc: nodeFilter,
+ filterState: {},
})
- // the collapseAll rely on expandedCache being filled correctly
- Object.values(items).forEach(item => {
- if (item.isExpanded) {
- wrapper.vm.expandedCache.add(item)
- }
+ // Does not run filtering when filterState changes & is falsy
+ await wrapper.setProps({ filterState: null })
+ expect(nodeFilter).not.toHaveBeenCalled()
+ // Runs filtering when filterState changes & is truthy
+ await wrapper.setProps({ filterState: {} })
+ expect(nodeFilter.mock.calls).toEqual([
+ [simpleWorkflowTree4Nodes[0].children[0], wrapper.vm.filteredOutNodesCache],
+ ])
+ nodeFilter.mockClear()
+ // Runs filtering when tree changes
+ const newWorkflows = cloneDeep(simpleWorkflowTree4Nodes)
+ newWorkflows[0].children[0].node.state = 'frobnicated'
+ await wrapper.setProps({
+ workflows: newWorkflows,
})
- const treeItemCache = wrapper.vm.treeItemCache
-
- wrapper.vm.collapseAll(filter)
- expect(wrapper.vm.expanded).to.equal(filter !== null)
-
- const collection = filter ? Object.values(treeItemCache).filter(filter) : Object.values(treeItemCache)
- for (const item of collection) {
- expect(item.isExpanded).to.equal(false)
- }
- expect(
- Object.values(treeItemCache).filter(item => !item.isExpanded).length,
- `Failed case: ${JSON.stringify(items)}`
- ).to.equal(expectedCollapsedItems)
+ expect(nodeFilter.mock.calls).toEqual([
+ [newWorkflows[0].children[0], wrapper.vm.filteredOutNodesCache],
+ ])
})
})
})
diff --git a/tests/unit/components/cylc/tree/treeitem.vue.spec.js b/tests/unit/components/cylc/tree/treeitem.vue.spec.js
index 0a653a985..d0ce28de8 100644
--- a/tests/unit/components/cylc/tree/treeitem.vue.spec.js
+++ b/tests/unit/components/cylc/tree/treeitem.vue.spec.js
@@ -66,36 +66,35 @@ describe('TreeItem component', () => {
plugins: [createVuetify(), CylcObjectPlugin],
mock: { $workflowService, $eventBus }
},
- props: {
- node: simpleWorkflowNode
- },
...options
})
it('should display the treeitem with valid data', () => {
- const wrapper = mountFunction()
+ const wrapper = mountFunction({
+ props: {
+ node: simpleWorkflowNode,
+ filteredOutNodesCache: new WeakMap(),
+ },
+ })
expect(wrapper.props().node.node.__typename).to.equal('Workflow')
- expect(wrapper.vm.$data.filtered).to.equal(true)
})
describe('expanded', () => {
// using simpleJobNode as it has only one child so it is easier/quicker to test
- it('should expand nodes when configured', () => {
- let wrapper = mountFunction({
- props: {
- node: simpleCyclepointNode,
- autoExpandTypes: ['cycle']
- }
- })
- expect(wrapper).to.be.expanded()
-
- wrapper = mountFunction({
+ it.each([
+ [simpleCyclepointNode, true],
+ [simpleTaskNode, false],
+ ])('should expand nodes when configured %#', (node, expected) => {
+ const wrapper = mountFunction({
props: {
- node: simpleTaskNode,
+ node,
+ filteredOutNodesCache: new WeakMap(),
autoExpandTypes: ['cycle']
}
})
- expect(wrapper).to.not.be.expanded()
+ expected
+ ? expect(wrapper).to.be.expanded()
+ : expect(wrapper).to.not.be.expanded()
})
})
@@ -103,6 +102,7 @@ describe('TreeItem component', () => {
const wrapper = mountFunction({
props: {
node: simpleTaskNode,
+ filteredOutNodesCache: new WeakMap(),
}
})
expect(wrapper).to.not.be.expanded()
@@ -126,6 +126,7 @@ describe('TreeItem component', () => {
const wrapper = mountFunction({
props: {
node: simpleWorkflowNode,
+ filteredOutNodesCache: new WeakMap(),
},
data: () => ({
manuallyExpanded,
@@ -136,113 +137,6 @@ describe('TreeItem component', () => {
.map((vm) => vm.props().node.node.__typename)
).to.deep.equal(expected)
})
-
- // })
- // describe('mixin', () => {
- // const sortTestsData = [
- // // invalid values
- // {
- // args: {
- // type: '',
- // children: []
- // },
- // expected: []
- // },
- // {
- // args: {
- // type: null,
- // children: null
- // },
- // expected: null
- // },
- // // workflow children (cycle points) are sorted by ID in descending order
- // {
- // args: {
- // type: 'workflow',
- // children: [
- // { id: 'workflow//1' },
- // { id: 'workflow//2' }
- // ]
- // },
- // expected: [
- // { id: 'workflow//2' },
- // { id: 'workflow//1' }
- // ]
- // },
- // // cycle point children (family proxies and task proxies) are sorted by type in ascending order, and then name in ascending order
- // {
- // args: {
- // type: 'cyclepoint',
- // children: [
- // { node: { name: 'foo' }, type: 'task-proxy' },
- // { node: { name: 'FAM1' }, type: 'family-proxy' },
- // { node: { name: 'bar' }, type: 'task-proxy' }
- // ]
- // },
- // expected: [
- // { node: { name: 'FAM1' }, type: 'family-proxy' },
- // { node: { name: 'bar' }, type: 'task-proxy' },
- // { node: { name: 'foo' }, type: 'task-proxy' }
- // ]
- // },
- // // family proxy children (family proxies and task proxies) are sorted by type in ascending order, and then name in ascending order
- // {
- // args: {
- // type: 'family-proxy',
- // children: [
- // { node: { name: 'foo' }, type: 'task-proxy' },
- // { node: { name: 'FAM1' }, type: 'family-proxy' },
- // { node: { name: 'bar' }, type: 'task-proxy' }
- // ]
- // },
- // expected: [
- // { node: { name: 'FAM1' }, type: 'family-proxy' },
- // { node: { name: 'bar' }, type: 'task-proxy' },
- // { node: { name: 'foo' }, type: 'task-proxy' }
- // ]
- // },
- // {
- // args: {
- // type: 'family-proxy',
- // children: [
- // { node: { name: 'f01' }, type: 'task-proxy' },
- // { node: { name: 'f1' }, type: 'task-proxy' },
- // { node: { name: 'f10' }, type: 'task-proxy' },
- // { node: { name: 'f0' }, type: 'task-proxy' },
- // { node: { name: 'f2' }, type: 'task-proxy' }
- // ]
- // },
- // expected: [
- // { node: { name: 'f0' }, type: 'task-proxy' },
- // { node: { name: 'f01' }, type: 'task-proxy' },
- // { node: { name: 'f1' }, type: 'task-proxy' },
- // { node: { name: 'f2' }, type: 'task-proxy' },
- // { node: { name: 'f10' }, type: 'task-proxy' }
- // ]
- // },
- // // task proxy children (jobs) are sorted by job submit number in descending order
- // {
- // args: {
- // type: 'task-proxy',
- // children: [
- // { node: { submitNum: '2' } },
- // { node: { submitNum: '1' } },
- // { node: { submitNum: '3' } }
- // ]
- // },
- // expected: [
- // { node: { submitNum: '3' } },
- // { node: { submitNum: '2' } },
- // { node: { submitNum: '1' } }
- // ]
- // }
- // ]
- // sortTestsData.forEach((test) => {
- // it('should order elements correctly', () => {
- // const sorted = treeitem.methods.sortedChildren(test.args.type, test.args.children)
- // expect(sorted).to.deep.equal(test.expected)
- // })
- // })
})
})
@@ -259,6 +153,7 @@ describe('GScanTreeItem', () => {
const wrapper = mountFunction({
props: {
node: flattenWorkflowParts(stateTotalsTestWorkflowNodes),
+ filteredOutNodesCache: new WeakMap(),
}
})
it('combines all descendant tasks', () => {
@@ -284,6 +179,7 @@ describe('GScanTreeItem', () => {
node: {
type: 'barbenheimer',
},
+ filteredOutNodesCache: new WeakMap(),
},
shallow: true,
})
@@ -296,6 +192,7 @@ describe('GScanTreeItem', () => {
type: 'workflow',
tokens: { workflow: 'a/b/c' }
},
+ filteredOutNodesCache: new WeakMap(),
},
shallow: true,
})
diff --git a/tests/unit/views/tree.vue.spec.js b/tests/unit/views/tree.vue.spec.js
new file mode 100644
index 000000000..38f5cc423
--- /dev/null
+++ b/tests/unit/views/tree.vue.spec.js
@@ -0,0 +1,139 @@
+/* Copyright (C) NIWA & British Crown (Met Office) & Contributors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see . */
+
+import { mount } from '@vue/test-utils'
+import { createStore } from 'vuex'
+import { createVuetify } from 'vuetify'
+import sinon from 'sinon'
+import storeOptions from '@/store/options'
+import Tree from '@/views/Tree.vue'
+import User from '@/model/User.model'
+import WorkflowService from '@/services/workflow.service'
+import CylcObjectPlugin from '@/components/cylc/cylcObject/plugin'
+import { Tokens } from '@/utils/uid'
+import { getIDMap } from '$tests/util'
+
+const $workflowService = sinon.createStubInstance(WorkflowService)
+const vuetify = createVuetify()
+
+const expandID = (id) => ({
+ id,
+ tokens: new Tokens(id),
+ node: {},
+})
+
+const workflowNode = {
+ ...expandID('~user/workflow1'),
+ type: 'workflow',
+ children: [
+ {
+ ...expandID('~user/workflow1//1'),
+ type: 'cycle',
+ children: [],
+ familyTree: [
+ {
+ ...expandID('~user/workflow1//1/root'),
+ type: 'family',
+ children: [
+ {
+ ...expandID('~user/workflow1//1/FAM'),
+ type: 'family',
+ children: [
+ {
+ ...expandID('~user/workflow1//1/foo'),
+ type: 'task',
+ node: { state: 'failed' },
+ children: [
+ {
+ ...expandID('~user/workflow1//1/foo/1'),
+ type: 'job',
+ },
+ ]
+ },
+ {
+ ...expandID('~user/workflow1//1/bar'),
+ type: 'task',
+ node: { state: 'waiting' },
+ children: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+}
+
+describe('Tree view', () => {
+ let mountFunction
+ beforeEach(() => {
+ const store = createStore(storeOptions)
+ const user = new User('cylc', [], new Date(), true, 'localhost', 'owner')
+ store.commit('user/SET_USER', user)
+ mountFunction = (options) => mount(Tree, {
+ global: {
+ plugins: [vuetify, CylcObjectPlugin, store],
+ mocks: {
+ $workflowService
+ }
+ },
+ props: {
+ workflowName: 'workflow1',
+ },
+ ...options
+ })
+ })
+
+ describe('Filter', () => {
+ it.each([
+ {},
+ { id: null, states: null },
+ { id: ' ', states: [] }
+ ])('has null filterState when filters are empty: %o', async (tasksFilter) => {
+ const wrapper = mountFunction()
+ expect(wrapper.vm.tasksFilter).toEqual({
+ id: null,
+ states: null,
+ })
+ await wrapper.setData({ tasksFilter })
+ expect(wrapper.vm.filterState).toBeNull()
+ })
+
+ it.each([
+ { tasksFilter: { id: 'foo' }, filteredOut: false },
+ { tasksFilter: { states: ['failed'] }, filteredOut: false },
+ { tasksFilter: { id: 'foo', states: ['failed'] }, filteredOut: false },
+
+ { tasksFilter: { id: 'asdf' }, filteredOut: true },
+ { tasksFilter: { states: ['running'] }, filteredOut: true },
+ { tasksFilter: { id: 'foo', states: ['running'] }, filteredOut: true },
+ { tasksFilter: { id: 'asdf', states: ['failed'] }, filteredOut: true },
+ ])('filters by $tasksFilter', async ({ tasksFilter, filteredOut }) => {
+ const wrapper = mountFunction()
+ await wrapper.setData({ tasksFilter })
+ expect(wrapper.vm.filterState).toMatchObject(tasksFilter)
+ const filteredOutNodesCache = new Map()
+ expect(wrapper.vm.filterNode(workflowNode, filteredOutNodesCache)).toEqual(!filteredOut)
+ expect(getIDMap(filteredOutNodesCache)).toEqual({
+ '~user/workflow1': filteredOut,
+ '~user/workflow1//1': filteredOut,
+ '~user/workflow1//1/FAM': filteredOut,
+ '~user/workflow1//1/foo': filteredOut,
+ '~user/workflow1//1/bar': true, // always filtered out
+ })
+ })
+ })
+})
diff --git a/tests/util.js b/tests/util.js
index 811b9b756..212cdc81b 100644
--- a/tests/util.js
+++ b/tests/util.js
@@ -30,3 +30,17 @@ export class Deferred {
})
}
}
+
+/**
+ * Convert filteredOutNodesCache to a simpler object for easier comparison.
+ *
+ * Used by Tree view & GScan tests.
+ *
+ * @param {Map} filteredOutNodesCache
+ * @returns {Object}
+ */
+export function getIDMap (filteredOutNodesCache) {
+ return Object.fromEntries(
+ Array.from(filteredOutNodesCache.entries(), ([node, val]) => [node.id, val])
+ )
+}
diff --git a/vite.config.js b/vite.config.js
index 33f8a858a..d47c23b1f 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -50,6 +50,7 @@ export default defineConfig(({ mode }) => {
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
+ $tests: path.resolve(__dirname, './tests'),
react: 'preact/compat',
'react-dom': 'preact/compat',
}
@@ -98,6 +99,7 @@ export default defineConfig(({ mode }) => {
environment: 'jsdom',
globals: true, // auto-import `describe`, `it`, `beforeEach` etc.
setupFiles: ['./tests/unit/setup.js'],
+ restoreMocks: true,
deps: {
// inline vuetify to prevent 'TypeError: Unknown file extension ".css"
inline: ['vuetify']