diff --git a/resources/css/common.css b/resources/css/common.css index d6a7ceffce..1b8798106d 100644 --- a/resources/css/common.css +++ b/resources/css/common.css @@ -47,6 +47,13 @@ body { text-decoration: underline; } +/* Bootstrap forces a focus ring on every a:focus, which shows up on mouse + clicks too. Suppress it only when the browser wouldn't draw one itself, so + keyboard navigation keeps a visible focus indicator. */ +a.cdash-link:focus:not(:focus-visible) { + outline: none; +} + .na { background-color : #cccccc; } .measurement { background-color : #b0c4de; } diff --git a/resources/js/angular/controllers/index.js b/resources/js/angular/controllers/index.js index 50b99d19a1..020aac9b72 100644 --- a/resources/js/angular/controllers/index.js +++ b/resources/js/angular/controllers/index.js @@ -201,9 +201,9 @@ export function IndexController($scope, $rootScope, $location, $http, $filter, $ $scope.cdash.buildgroups[i].builds = $filter('orderBy')($scope.cdash.buildgroups[i].builds, $scope.cdash.buildgroups[i].orderByFields); $scope.cdash.buildgroups[i].builds = $filter('showEmptyBuildsLast')($scope.cdash.buildgroups[i].builds, $scope.cdash.buildgroups[i].orderByFields); - // Initialize expectedInGroup property for each build to avoid checkbox binding conflicts + // Initialize per-build admin UI state (default move target = current group) for (var j = 0; j < $scope.cdash.buildgroups[i].builds.length; j++) { - $scope.cdash.buildgroups[i].builds[j].expectedInGroup = {}; + $scope.cdash.buildgroups[i].builds[j].moveTargetGroup = String($scope.cdash.buildgroups[i].id); } // Initialize bulk selection properties @@ -491,6 +491,15 @@ export function IndexController($scope, $rootScope, $location, $http, $filter, $ }; $scope.moveToGroup = function(build, groupid) { + if (!groupid) { + return; + } + + groupid = parseInt(groupid, 10); + if (!groupid || groupid === parseInt(build.buildgroupid, 10)) { + return; + } + if (build.expectedandmissing == 1) { var parameters = { siteid: build.siteid, @@ -507,16 +516,11 @@ export function IndexController($scope, $rootScope, $location, $http, $filter, $ alert('An error occurred while moving the build. Please try again.'); }); } else { - // Use the checkbox value for this specific group, default to current expected value - var expectedInNewGroup = build.expectedInGroup && build.expectedInGroup[groupid] !== undefined - ? (build.expectedInGroup[groupid] ? 1 : 0) - : build.expected; - - // Use the build API with the correct parameters + // Preserve the build's current expected status when moving groups. var parameters = { buildid: build.id, newgroupid: groupid, - expected: expectedInNewGroup + expected: build.expected || 0 }; $http.post('api/v1/build.php', parameters) .then(function success() { @@ -578,17 +582,13 @@ export function IndexController($scope, $rootScope, $location, $http, $filter, $ var targetGroupId = parseInt(buildgroup.bulkTargetGroup, 10); var movePromises = []; - // Move each selected build using the build API + // Move each selected build using the build API, preserving expected status for (var i = 0; i < buildgroup.selectedBuilds.length; i++) { var build = buildgroup.selectedBuilds[i]; - var expectedInNewGroup = build.expectedInGroup && build.expectedInGroup[targetGroupId] !== undefined - ? (build.expectedInGroup[targetGroupId] ? 1 : 0) - : (build.expected || 0); - var parameters = { buildid: build.id, newgroupid: targetGroupId, - expected: expectedInNewGroup + expected: build.expected || 0 }; movePromises.push($http.post('api/v1/build.php', parameters)); } diff --git a/resources/js/angular/views/partials/build.html b/resources/js/angular/views/partials/build.html index ed5b8dc1a9..4d7e2de8a4 100644 --- a/resources/js/angular/views/partials/build.html +++ b/resources/js/angular/views/partials/build.html @@ -175,64 +175,60 @@ - -
-
- - - - - - - - - - - -
- {{::group.name}}: - - - - - - -
- -
- -
+ +
+
+
+ + + + + + +
+ +
+ +
diff --git a/tests/cypress/e2e/expected-build.cy.js b/tests/cypress/e2e/expected-build.cy.js index cd13343cf1..a582660bc9 100644 --- a/tests/cypress/e2e/expected-build.cy.js +++ b/tests/cypress/e2e/expected-build.cy.js @@ -4,9 +4,10 @@ describe('expected_build', () => { cy.login(); cy.visit('index.php?project=InsightExample&date=2018-08-09'); cy.get('[data-cy="build-admin-options"]').first().click(); - cy.get('table.animate-show').find('tr').eq(2).then((row) => { - if (row.find('[data-cy="mark-as-non-expected-btn"]').length > 0) { - row.find('button').click(); + cy.get('[data-cy="build-admin-options-panel"]').then(($panel) => { + const btn = $panel.find('[data-cy="mark-as-non-expected-btn"]'); + if (btn.length > 0) { + cy.wrap(btn).click(); } }); }); @@ -24,14 +25,18 @@ describe('expected_build', () => { cy.get('@build_td').should('contain', 'test-build-relationships'); cy.get('@build_td').find('[data-cy="build-admin-options"]').click(); - // find the 'Mark as Expected' button and click it - cy.get('[data-cy="mark-as-expected-btn"]').click(); + // status should start as not expected + cy.get('[data-cy="build-admin-options-panel"]').should('be.visible'); + cy.get('[data-cy="mark-as-expected-btn"]') + .should('contain', 'Mark this build as expected') + .click(); // refresh the page to make sure this build is now expected cy.reload(); cy.get('[data-cy="build-admin-options"]').first().click(); cy.get('[data-cy="mark-as-expected-btn"]').should('not.exist'); cy.get('[data-cy="mark-as-non-expected-btn"]').should('exist'); + cy.get('[data-cy="mark-as-non-expected-btn"]').should('contain', 'Mark this build as not expected'); // 'latest' should now display 'test-build-relationships' with unknown start time cy.get('a').contains('Latest').click(); @@ -50,6 +55,86 @@ describe('expected_build', () => { cy.get('[data-cy="mark-as-expected-btn"]').should('exist'); }); + it('defaults move dropdown to the current group and enables Move only for a different group', () => { + cy.visit('index.php?project=InsightExample&date=2018-08-09'); + + cy.get('#project_5_15').parents('.buildgroup').first().as('buildgroup'); + cy.get('@buildgroup').find('a.grouptrigger').invoke('text').then((currentGroupName) => { + const groupName = currentGroupName.trim(); + + cy.get('#project_5_15').find('tbody').find('tr').first().find('td').eq(1).as('build_td'); + cy.get('@build_td').find('[data-cy="build-admin-options"]').click(); + + cy.get('[data-cy="build-admin-options-panel"]').should('be.visible'); + cy.get('[data-cy="mark-as-expected-btn"]').should('contain', 'Mark this build as expected'); + cy.get('[data-cy="move-to-group-btn"]').should('contain', 'Move to group').and('be.disabled'); + cy.contains('-- Select Group --').should('not.exist'); + + cy.get('[data-cy="move-to-group-select"]').find('option').should('have.length.at.least', 2); + cy.get('[data-cy="move-to-group-select"] option:selected') + .should('contain', groupName); + + cy.get('[data-cy="move-to-group-select"]').find('option').then(($options) => { + const other = [...$options].find((o) => o.textContent.trim() !== groupName); + expect(other).to.exist; + cy.get('[data-cy="move-to-group-select"]').select(other.value); + }); + cy.get('[data-cy="move-to-group-btn"]').should('not.be.disabled'); + + cy.get('[data-cy="move-to-group-select"]').select(groupName); + cy.get('[data-cy="move-to-group-btn"]').should('be.disabled'); + }); + }); + + it('moves a build to another group via dropdown', () => { + cy.visit('index.php?project=InsightExample&date=2018-08-09'); + + const buildName = 'test-build-relationships'; + cy.get('#project_5_15').parents('.buildgroup').first().as('source_group'); + cy.get('@source_group').find('a.grouptrigger').invoke('text').then((sourceGroupName) => { + const sourceName = sourceGroupName.trim(); + + cy.get('#project_5_15').find('tbody').find('tr').first().find('td').eq(1).as('build_td'); + cy.get('@build_td').should('contain', buildName); + cy.get('@build_td').find('[data-cy="build-admin-options"]').click(); + + cy.get('[data-cy="move-to-group-select"]').find('option').then(($options) => { + const other = [...$options].find((o) => o.textContent.trim() !== sourceName); + expect(other).to.exist; + const targetGroupId = other.value; + const targetGroupName = other.textContent.trim(); + + cy.window().then((w) => { + w.beforeMoveReload = true; + }); + cy.get('[data-cy="move-to-group-select"]').select(targetGroupId); + cy.get('[data-cy="move-to-group-btn"]').should('not.be.disabled').click(); + + cy.window().should('not.have.property', 'beforeMoveReload'); + cy.url().should('contain', 'index.php?project=InsightExample&date=2018-08-09'); + + // build should appear in the destination group + cy.contains('.buildgroup', targetGroupName).should('contain', buildName); + + // move it back to the original group + cy.contains('.buildgroup', targetGroupName).within(() => { + cy.contains('tr', buildName).find('[data-cy="build-admin-options"]').click(); + cy.get('[data-cy="move-to-group-select"] option:selected') + .should('contain', targetGroupName); + cy.get('[data-cy="move-to-group-btn"]').should('be.disabled'); + cy.get('[data-cy="move-to-group-select"]').select(sourceName); + cy.window().then((w) => { + w.beforeMoveBackReload = true; + }); + cy.get('[data-cy="move-to-group-btn"]').should('not.be.disabled').click(); + }); + + cy.window().should('not.have.property', 'beforeMoveBackReload'); + cy.contains('.buildgroup', sourceName).should('contain', buildName); + }); + }); + }); + it('batch marks multiple builds as expected and not expected', () => { // navigate to the page with builds cy.visit('index.php?project=InsightExample&date=2010-07-07'); @@ -76,15 +161,16 @@ describe('expected_build', () => { // verify first build is now expected cy.get('#project_5_13').find('tbody').find('tr').eq(0).find('[data-cy="build-admin-options"]').click(); - cy.get('#project_5_13').find('tbody').find('tr').eq(0).find('table.animate-show').should('be.visible'); - cy.get('[data-cy="mark-as-non-expected-btn"]').first().should('exist'); + cy.get('#project_5_13').find('tbody').find('tr').eq(0).find('[data-cy="build-admin-options-panel"]').should('be.visible'); + cy.get('[data-cy="mark-as-non-expected-btn"]').first().should('exist') + .and('contain', 'Mark this build as not expected'); cy.get('[data-cy="mark-as-expected-btn"]').should('not.exist'); // close admin options cy.get('#project_5_13').find('tbody').find('tr').eq(0).find('[data-cy="build-admin-options"]').click(); // Check second build cy.get('#project_5_13').find('tbody').find('tr').eq(1).find('[data-cy="build-admin-options"]').click(); - cy.get('#project_5_13').find('tbody').find('tr').eq(1).find('table.animate-show').should('be.visible'); + cy.get('#project_5_13').find('tbody').find('tr').eq(1).find('[data-cy="build-admin-options-panel"]').should('be.visible'); cy.get('[data-cy="mark-as-non-expected-btn"]').should('exist'); cy.get('[data-cy="mark-as-expected-btn"]').should('not.exist'); // close admin options @@ -107,14 +193,15 @@ describe('expected_build', () => { // verify first build is now not expected cy.get('#project_5_13').find('tbody').find('tr').eq(0).find('[data-cy="build-admin-options"]').click(); - cy.get('#project_5_13').find('tbody').find('tr').eq(0).find('table.animate-show').should('be.visible'); - cy.get('[data-cy="mark-as-expected-btn"]').first().should('exist'); + cy.get('#project_5_13').find('tbody').find('tr').eq(0).find('[data-cy="build-admin-options-panel"]').should('be.visible'); + cy.get('[data-cy="mark-as-expected-btn"]').first().should('exist') + .and('contain', 'Mark this build as expected'); cy.get('[data-cy="mark-as-non-expected-btn"]').should('not.exist'); // close admin options cy.get('#project_5_13').find('tbody').find('tr').eq(0).find('[data-cy="build-admin-options"]').click(); cy.get('#project_5_13').find('tbody').find('tr').eq(1).find('[data-cy="build-admin-options"]').click(); - cy.get('#project_5_13').find('tbody').find('tr').eq(1).find('table.animate-show').should('be.visible'); + cy.get('#project_5_13').find('tbody').find('tr').eq(1).find('[data-cy="build-admin-options-panel"]').should('be.visible'); cy.get('[data-cy="mark-as-expected-btn"]').should('exist'); cy.get('[data-cy="mark-as-non-expected-btn"]').should('not.exist'); // close admin options