-
-
-
-
-
-
-
-
-
-
-
-
- {{ flat? $options.icons.mdiFormatAlignRight : $options.icons.mdiFormatAlignJustify }}
-
-
- {{ flat ? "Show Families" : "Hide Families" }}
-
-
-
- {{ $options.icons.mdiPlus }}
- Expand all
-
-
- {{ $options.icons.mdiMinus }}
- Collapse all
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/tests/e2e/specs/table.cy.js b/tests/e2e/specs/table.cy.js
index 26df1dee7..e1c583572 100644
--- a/tests/e2e/specs/table.cy.js
+++ b/tests/e2e/specs/table.cy.js
@@ -43,14 +43,12 @@ describe('Table view', () => {
it('Should filter by ID', () => {
cy.get('.c-table table > tbody > tr')
.should('have.length', initialNumRows)
- cy.get('[data-cy=filter-id] input')
+ cy.get('[data-cy=control-taskIDFilter] input')
.should('be.empty')
- cy.get('[data-cy="filter task state"] input')
- .should('have.value', '')
cy.get('td [data-cy-task-name=sleepy]')
.should('be.visible')
for (const id of ['eep', '/sle']) {
- cy.get('[data-cy=filter-id] input')
+ cy.get('[data-cy=control-taskIDFilter] input')
.clear()
.type(id)
cy.get('td [data-cy-task-name=sleepy]')
@@ -69,7 +67,7 @@ describe('Table view', () => {
.get('td [data-cy-task-name=failed]')
.should('be.visible')
cy
- .get('[data-cy="filter task state"]')
+ .get('[data-cy=control-taskStateFilter]')
.click()
cy
.get('.v-list-item')
@@ -89,7 +87,7 @@ describe('Table view', () => {
.get('.c-table table > tbody > tr')
.should('have.length', initialNumRows)
cy
- .get('[data-cy="filter task state"]')
+ .get('[data-cy=control-taskStateFilter]')
.click()
cy
.get('.v-list-item')
@@ -100,7 +98,7 @@ describe('Table view', () => {
.should('have.length', 2)
.should('be.visible')
cy
- .get('[data-cy=filter-id] input')
+ .get('[data-cy=control-taskIDFilter] input')
.type('eventually')
cy
.get('td [data-cy-task-name=eventually_succeeded]')
@@ -182,18 +180,17 @@ describe('State saving', () => {
.get('.c-table table > tbody > tr')
.should('have.length', initialNumRows)
cy
- .get('[data-cy="filter task state"]:last')
+ .get('[data-cy=control-taskStateFilter]:last')
.click()
cy
- .get('.v-list-item')
- .contains(TaskState.SUCCEEDED.name)
- .click({ force: true })
+ .get('.v-list-item[state=succeeded]')
+ .click({ force: true, multiple: true })
cy
.get('.c-table table > tbody > tr')
.should('have.length', 2)
.should('be.visible')
cy
- .get('[data-cy=filter-id] input:last')
+ .get('[data-cy=control-taskIDFilter] input:last')
.type('eventually')
cy
.get('td [data-cy-task-name=eventually_succeeded]')
diff --git a/tests/e2e/specs/tree.cy.js b/tests/e2e/specs/tree.cy.js
index 525cb733d..9ea18b958 100644
--- a/tests/e2e/specs/tree.cy.js
+++ b/tests/e2e/specs/tree.cy.js
@@ -174,7 +174,7 @@ describe('Tree view', () => {
.should('have.length', initialNumTasks)
.contains('waiting')
for (const id of ['eed', '/suc', 'GOOD', 'SUC']) {
- cy.get('[data-cy=filter-id] input')
+ cy.get('.c-view-toolbar input')
.clear()
.type(id)
cy.get('.node-data-task:visible')
@@ -184,12 +184,12 @@ describe('Tree view', () => {
.should('not.be.visible')
}
// It should stop filtering when input is cleared
- cy.get('[data-cy=filter-id] input')
+ cy.get('.c-view-toolbar input')
.clear()
.get('.node-data-task:visible')
.should('have.length', initialNumTasks)
// It should filter by cycle point
- cy.get('[data-cy=filter-id] input')
+ cy.get('.c-view-toolbar input')
.type('2000') // (matches all tasks)
.get('.node-data-task:visible')
.should('have.length', initialNumTasks)
@@ -202,7 +202,7 @@ describe('Tree view', () => {
.contains(name)
.should('be.visible')
}
- cy.get('[data-cy="filter task state"]')
+ cy.get('[data-cy="control-taskStateFilter"]')
.click()
.get('.v-list-item')
.contains(new RegExp(`^${TaskState.FAILED.name}$`))
@@ -226,10 +226,10 @@ describe('Tree view', () => {
.contains('failed')
.should('be.visible')
cy
- .get('[data-cy=filter-id]')
+ .get('[data-cy="control-taskIDFilter"]')
.type('i')
cy
- .get('[data-cy="filter task state"]')
+ .get('[data-cy="control-taskStateFilter"]')
.click()
.get('.v-list-item')
.contains(TaskState.WAITING.name)
@@ -247,10 +247,10 @@ describe('Tree view', () => {
.contains('failed')
.should('be.visible')
cy
- .get('[data-cy=filter-id]')
+ .get('[data-cy="control-taskIDFilter"]')
.type('i')
cy
- .get('[data-cy="filter task state"]')
+ .get('[data-cy="control-taskStateFilter"]')
.click()
.get('.v-list-item')
.contains(TaskState.WAITING.name)
@@ -265,19 +265,19 @@ describe('Tree view', () => {
.contains('retrying')
})
- it('Provides a select all functionality', () => {
- cy.visit('/#/tree/one')
- cy.get('[data-cy="filter task state"]')
- .get('.v-list-item--active')
- .should('have.length', 0)
- cy.get('[data-cy="filter task state"]')
- .click()
- .get('[data-cy=task-filter-select-all]')
- .click()
- cy.get('[data-cy="filter task state"]')
- .get('.v-list-item--active')
- .should('have.length', 8)
- })
+ // it('Provides a select all functionality', () => {
+ // cy.visit('/#/tree/one')
+ // cy.get('[data-cy="control-taskStateFilter"]')
+ // .get('.v-list-item--active')
+ // .should('have.length', 0)
+ // cy.get('[data-cy="control-taskStateFilter"]')
+ // .click()
+ // .get('[data-cy=task-filter-select-all]')
+ // .click()
+ // cy.get('[data-cy="control-taskStateFilter"]')
+ // .get('.v-list-item--active')
+ // .should('have.length', 8)
+ // })
})
describe('Expand/collapse all buttons', () => {
@@ -287,11 +287,11 @@ describe('Tree view', () => {
.contains('sleepy')
.as('sleepyTask')
.should('be.visible')
- cy.get('[data-cy=collapse-all]')
+ cy.get('[data-cy=control-CollapseAll]')
.click()
.get('@sleepyTask')
.should('not.be.visible')
- .get('[data-cy=expand-all]')
+ .get('[data-cy=control-ExpandAll]')
.click()
.get('@sleepyTask')
.should('be.visible')
@@ -299,7 +299,7 @@ describe('Tree view', () => {
it('Does not expand jobs but can collapse them', () => {
cy.visit('/#/tree/one')
- .get('[data-cy=expand-all]')
+ .get('[data-cy=control-ExpandAll]')
.click()
.get('.node-data-job:first')
.should('not.exist')
@@ -308,14 +308,14 @@ describe('Tree view', () => {
.click()
.get('.node-data-job:first')
.should('be.visible')
- cy.get('[data-cy=expand-all]')
+ cy.get('[data-cy=control-ExpandAll]')
.click()
// The job should remain expanded
.get('.node-data-job:first')
.should('be.visible')
- cy.get('[data-cy=collapse-all]')
+ cy.get('[data-cy=control-CollapseAll]')
.click()
- .get('[data-cy=expand-all]')
+ .get('[data-cy=control-ExpandAll]')
.click()
// The job should be collapsed now
.get('.node-data-job:first')
@@ -328,41 +328,24 @@ describe('Tree view', () => {
.contains('sleepy')
.as('sleepyTask')
.should('be.visible')
- cy.get('[data-cy=filter-id]')
+ cy.get('[data-cy="control-taskIDFilter"]')
.type('sleep')
- cy.get('[data-cy=collapse-all]')
+ cy.get('[data-cy=control-CollapseAll]')
.click()
.get('@sleepyTask')
.should('not.be.visible')
- .get('[data-cy=expand-all]')
+ .get('[data-cy=control-ExpandAll]')
.click()
.get('@sleepyTask')
.should('be.visible')
})
})
- it('should show a summary of tasks if the number of selected items is greater than the maximum limit', () => {
- cy.visit('/#/tree/one')
- cy.get('[data-cy="filter task state"]')
- .click()
- // eslint-disable-next-line no-lone-blocks
- TaskState.enumValues.forEach(state => {
- cy.get('.v-list-item')
- .contains(state.name)
- .click({ force: true })
- })
- // Click outside to close dropdown
- cy.get('noscript')
- .click({ force: true })
- cy.get('[data-cy="filter task state"]')
- .contains('.v-select__selection', '(+')
- })
-
describe('Toggle families', () => {
it('Toggles between flat and hierarchical modes', () => {
cy.visit('/#/tree/one')
cy.get('.node-data-family').should('have.length', 3)
- cy.get('[data-cy=toggle-families]').click()
+ cy.get('[data-cy=control-flat]').click()
cy.get('.node-data-family').should('have.length', 0)
})
})
diff --git a/tests/unit/components/cylc/common/filter.spec.js b/tests/unit/components/cylc/common/filter.spec.js
index cc965ad74..f9ffa14ee 100644
--- a/tests/unit/components/cylc/common/filter.spec.js
+++ b/tests/unit/components/cylc/common/filter.spec.js
@@ -15,7 +15,12 @@
* along with this program. If not, see
.
*/
-import { matchID, matchNode, matchState } from '@/components/cylc/common/filter'
+import {
+ globToRegex,
+ matchID,
+ matchNode,
+ matchState,
+} from '@/components/cylc/common/filter'
const taskNode = {
id: '~user/one//20000102T0000Z/succeeded',
@@ -35,9 +40,12 @@ const taskNode = {
id: '~user/one//20000102T0000Z/succeeded',
name: 'succeeded',
state: 'succeeded',
- isHeld: false,
+ isHeld: true,
isQueued: false,
isRunahead: false,
+ isRetry: true,
+ isWallclock: false,
+ isXtriggered: false,
cyclePoint: '20000102T0000Z',
firstParent: {
id: '~user/one//20000102T0000Z/SUCCEEDED',
@@ -53,19 +61,21 @@ const taskNode = {
describe('task filtering', () => {
describe('matchID', () => {
it.each([
- { node: taskNode, id: '', expected: true },
- { node: taskNode, id: ' ', expected: true },
- { node: taskNode, id: '2000', expected: true },
- { node: taskNode, id: 'succeeded', expected: true },
- { node: taskNode, id: '20000102T0000Z/suc', expected: true },
- { node: taskNode, id: '2001', expected: false },
- { node: taskNode, id: 'darmok', expected: false },
+ { node: taskNode, regex: null, expected: true },
+ { node: taskNode, regex: /2000/, expected: true },
+ { node: taskNode, regex: /succeeded/, expected: true },
+ { node: taskNode, regex: /20000102T0000Z\/suc/, expected: true },
+ { node: taskNode, regex: /2001/, expected: false },
+ { node: taskNode, regex: globToRegex('*'), expected: true },
+ { node: taskNode, regex: globToRegex('suc*'), expected: true },
+ { node: taskNode, regex: /darmok/, expected: false },
+ { node: taskNode, regex: globToRegex('darmok*'), expected: false },
// Only matches relative ID:
- { node: taskNode, id: 'user/one', expected: false },
+ { node: taskNode, regex: /user\/one/, expected: false },
// Case sensitive:
- { node: taskNode, id: 'SUC', expected: false },
- ])('matchID(<$node.id>, $id)', ({ node, id, expected }) => {
- expect(matchID(node, id)).toBe(expected)
+ { node: taskNode, regex: /SUC/, expected: false },
+ ])('matchID(<$node.id>, $regex)', ({ node, regex, expected }) => {
+ expect(matchID(node, regex)).toBe(expected)
})
})
@@ -76,21 +86,63 @@ describe('task filtering', () => {
{ node: taskNode, states: ['succeeded'], expected: true },
{ node: taskNode, states: ['succeeded', 'failed'], expected: true },
{ node: taskNode, states: ['failed'], expected: false },
- ])('matchState(<$node.node.state>, $states)', ({ node, states, expected }) => {
- expect(matchState(node, states)).toBe(expected)
+ { node: taskNode, states: ['failed'], expected: false },
+ { node: taskNode, states: ['waiting'], waitingStateModifiers: ['isRetry'], expected: false },
+ { node: taskNode, genericModifiers: ['isHeld'], expected: true },
+ { node: taskNode, genericModifiers: ['isHeld', 'isRunahead'], expected: true },
+ { node: taskNode, genericModifiers: ['isRunahead'], expected: false },
+ { node: taskNode, states: ['succeeded'], genericModifiers: ['isHeld'], expected: true },
+ { node: taskNode, states: ['waiting'], genericModifiers: ['isHeld'], expected: false },
+ ])('matchState(<$node.node.state>, $states)', ({
+ node,
+ states,
+ waitingStateModifiers,
+ genericModifiers,
+ expected,
+ }) => {
+ expect(matchState(
+ node,
+ states,
+ waitingStateModifiers,
+ genericModifiers,
+ )).toBe(expected)
})
})
describe('matchNode', () => {
it.each([
- { node: taskNode, id: '', states: [], expected: true },
- { node: taskNode, id: '2000', states: [], expected: true },
- { node: taskNode, id: '', states: ['succeeded', 'failed'], expected: true },
- { node: taskNode, id: '2000', states: ['succeeded', 'failed'], expected: true },
- { node: taskNode, id: 'darmok', states: ['succeeded', 'failed'], expected: false },
- { node: taskNode, id: '2000', states: ['failed'], expected: false },
- ])('matchNode(<$node.id>, $id, $states)', ({ node, id, states, expected }) => {
- expect(matchNode(node, id, states)).toBe(expected)
+ { node: taskNode, regex: null, states: [], expected: true },
+ { node: taskNode, regex: /2000/, states: [], expected: true },
+ { node: taskNode, regex: /2000/, states: ['succeeded', 'failed'], expected: true },
+ { node: taskNode, regex: /darmok/, states: ['succeeded', 'failed'], expected: false },
+ { node: taskNode, regex: /2000/, states: ['failed'], expected: false },
+ ])('matchNode(<$node.id>, $regex, $states)', ({ node, regex, states, expected }) => {
+ expect(matchNode(node, regex, states)).toBe(expected)
+ })
+ })
+
+ describe('globToRegex', () => {
+ // NOTE: The functionality tested here requires Node 24+
+ it.each([
+ // no pattern specified
+ { glob: '', regex: null },
+ { glob: ' ', regex: null },
+
+ // plain text
+ { glob: 'foo', regex: /foo/ },
+
+ // globs
+ { glob: 'f*o', regex: /f.*o/ },
+ { glob: 'f?o', regex: /f.o/ },
+ { glob: 'f[o]o', regex: /f[o]o/ },
+ { glob: 'f[!o]o', regex: /f[^o]o/ },
+
+ // regex escapes
+ { glob: '.*', regex: /\..*/ },
+ { glob: '(x)', regex: /\(x\)/ },
+ { glob: '\\w\\d\\s', regex: /\\w\\d\\s/ },
+ ])('globToRegex($glob) => $regex', ({ glob, regex }) => {
+ expect(String(globToRegex(glob))).toBe(String(regex))
})
})
})
diff --git a/tests/unit/views/table.vue.spec.js b/tests/unit/views/table.vue.spec.js
index 8314cc4ea..14022cae6 100644
--- a/tests/unit/views/table.vue.spec.js
+++ b/tests/unit/views/table.vue.spec.js
@@ -124,11 +124,19 @@ describe('Table view', () => {
})
it('should filter by ID', async () => {
+ // plain ID
wrapper.vm.tasksFilter = {
id: 'taskA'
}
await nextTick()
expect(wrapper.vm.filteredTasks.length).to.equal(1)
+
+ // glob ID
+ wrapper.vm.tasksFilter = {
+ id: 'task[A]'
+ }
+ await nextTick()
+ expect(wrapper.vm.filteredTasks.length).to.equal(1)
})
it('should filter by task state', async () => {
diff --git a/tests/unit/views/tree.vue.spec.js b/tests/unit/views/tree.vue.spec.js
index a8bd5a38f..421ec7abf 100644
--- a/tests/unit/views/tree.vue.spec.js
+++ b/tests/unit/views/tree.vue.spec.js
@@ -55,7 +55,7 @@ const workflowNode = {
{
...expandID('~user/workflow1//1/foo'),
type: 'task',
- node: { state: 'failed' },
+ node: { state: 'failed', isHeld: true },
children: [
{
...expandID('~user/workflow1//1/foo/1'),
@@ -114,19 +114,33 @@ describe('Tree view', () => {
})
it.each([
+ // the task should be displayed
{ tasksFilter: { id: 'foo' }, filteredOut: false },
{ tasksFilter: { states: ['failed'] }, filteredOut: false },
{ tasksFilter: { id: 'foo', states: ['failed'] }, filteredOut: false },
+ { tasksFilter: { id: 'foo', states: ['isHeld'] }, filteredOut: false },
+ { tasksFilter: { id: 'foo', states: ['failed', 'isHeld'] }, filteredOut: false },
+ { tasksFilter: { id: 'f*', states: ['failed', 'isHeld'] }, filteredOut: false },
+ { tasksFilter: { id: 'f?', states: ['failed', 'isHeld'] }, filteredOut: false },
+ { tasksFilter: { id: 'f[o]o', states: ['failed', 'isHeld'] }, filteredOut: false },
+ { tasksFilter: { id: 'f[!z]o', states: ['failed', 'isHeld'] }, filteredOut: false },
+ // the task should *not* be displayed
{ tasksFilter: { id: 'asdf' }, filteredOut: true },
{ tasksFilter: { states: ['running'] }, filteredOut: true },
{ tasksFilter: { id: 'foo', states: ['running'] }, filteredOut: true },
{ tasksFilter: { id: 'asdf', states: ['failed'] }, filteredOut: true },
+ { tasksFilter: { id: 'asdf', states: ['failed', 'isRunahead'] }, filteredOut: true },
+ { tasksFilter: { id: 'asdf*' }, filteredOut: true },
+ { tasksFilter: { id: 'asdf?' }, filteredOut: true },
+ { tasksFilter: { id: 'asd[f]' }, filteredOut: true },
+ { tasksFilter: { id: 'asd[!f]' }, filteredOut: true },
])('filters by $tasksFilter', async ({ tasksFilter, filteredOut }) => {
const wrapper = mountFunction()
wrapper.vm.tasksFilter = tasksFilter
await nextTick()
- expect(wrapper.vm.filterState).toMatchObject(tasksFilter)
+ expect(wrapper.vm.filterState[0]).toMatchObject(tasksFilter.id)
+ expect(wrapper.vm.filterState[1]).toMatchObject(tasksFilter.states)
const filteredOutNodesCache = new Map()
expect(wrapper.vm.filterNode(workflowNode, filteredOutNodesCache)).toEqual(!filteredOut)
expect(getIDMap(filteredOutNodesCache)).toEqual({
diff --git a/vite.config.js b/vite.config.js
index f7aec3560..b552c78e4 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -88,7 +88,8 @@ export default defineConfig(({ mode }) => {
},
watch: {
ignored: [
- path.resolve(__dirname, './coverage')
+ path.resolve(__dirname, './coverage'),
+ path.resolve(__dirname, '**/.nfs*'),
]
},
warmup: {
diff --git a/yarn.lock b/yarn.lock
index 81d16c381..daa1a4754 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3957,9 +3957,9 @@ __metadata:
linkType: hard
"caniuse-lite@npm:^1.0.30001524, caniuse-lite@npm:^1.0.30001718":
- version: 1.0.30001723
- resolution: "caniuse-lite@npm:1.0.30001723"
- checksum: 10c0/e019503061759b96017c4d27ddd7ca1b48533eabcd0431b51d2e3156f99f6b031075e46c279c0db63424cdfc874bba992caec2db51b922a0f945e686246886f6
+ version: 1.0.30001757
+ resolution: "caniuse-lite@npm:1.0.30001757"
+ checksum: 10c0/3ccb71fa2bf1f8c96ff1bf9b918b08806fed33307e20a3ce3259155fda131eaf96cfcd88d3d309c8fd7f8285cc71d89a3b93648a1c04814da31c301f98508d42
languageName: node
linkType: hard