diff --git a/docker-compose.yml b/docker-compose.yml index 379f0f51..ac4f4400 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.2' services: web: - image: "hailstorm3/hailstorm-web-client:1.7.10" + image: "hailstorm3/hailstorm-web-client:1.8.10" ports: - "8080:80" networks: @@ -22,7 +22,7 @@ services: - "start.sh" hailstorm-api: - image: "hailstorm3/hailstorm-api:1.0.17" + image: "hailstorm3/hailstorm-api:1.0.18" ports: - "4567:8080" environment: diff --git a/hailstorm-api/app/api/jmeter_plans.rb b/hailstorm-api/app/api/jmeter_plans.rb index 391d03f5..f49d5b99 100644 --- a/hailstorm-api/app/api/jmeter_plans.rb +++ b/hailstorm-api/app/api/jmeter_plans.rb @@ -15,17 +15,20 @@ # @type [Hailstorm::Support::Configuration] hailstorm_config = deep_decode(project_config.stringified_config) - test_plans_attrs = (hailstorm_config.jmeter.test_plans || []).map { |e| { test_plan_name: e, jmx_file: true } } + test_plans_attrs = hailstorm_config.jmeter.all_test_plans_attrs data_files_attrs = (hailstorm_config.jmeter.data_files || []).map { |e| { test_plan_name: e, jmx_file: false } } files_attrs = test_plans_attrs + data_files_attrs - data_attrs = files_attrs.map { |partial_attrs| to_jmeter_attributes(hailstorm_config, project_id, partial_attrs) } + data_attrs = files_attrs.map do |partial_attrs| + deep_camelize_keys(to_jmeter_attributes(hailstorm_config, project_id, partial_attrs)) + end + JSON.dump(data_attrs) end post '/projects/:project_id/jmeter_plans' do |project_id| found_project = Hailstorm::Model::Project.find(project_id) request.body.rewind - jmeter_plan = configure_jmeter(found_project, request) + jmeter_plan = deep_camelize_keys(configure_jmeter(found_project, request)) JSON.dump(jmeter_plan) end @@ -40,16 +43,14 @@ test_plan_name = hailstorm_config.jmeter.test_plans.find { |e| e.to_java_string.hash_code == id.to_i } return not_found unless test_plan_name - hailstorm_config.jmeter.properties(test_plan: test_plan_name) { |map| update_map(map, data) } - project_config.update!(stringified_config: deep_encode(hailstorm_config)) + hailstorm_config + .jmeter + .properties(test_plan: test_plan_name) { |map| update_map(map, data) } unless data['properties'].blank? - path, name = test_plan_name.split('/') - JSON.dump( - id: test_plan_name.to_java_string.hash_code, - name: "#{name}.jmx", - path: path, - properties: hailstorm_config.jmeter.properties(test_plan: test_plan_name).entries - ) + handle_disabled(data, hailstorm_config, test_plan_name) + project_config.update!(stringified_config: deep_encode(hailstorm_config)) + resp = deep_camelize_keys(build_patch_response(hailstorm_config, test_plan_name, project_id)) + JSON.dump(resp) end delete '/projects/:project_id/jmeter_plans/:id' do |project_id, id| @@ -58,8 +59,13 @@ hailstorm_config = deep_decode(project_config.stringified_config) test_plan_name = hailstorm_config.jmeter.test_plans.find { |e| e.to_java_string.hash_code == id.to_i } - hailstorm_config.jmeter.test_plans.reject! { |e| e == test_plan_name } if test_plan_name - if hailstorm_config.jmeter.data_files + if test_plan_name + return 402 if client_stats?(project_id, File.basename(test_plan_name)) + + hailstorm_config.jmeter.test_plans.reject! { |e| e == test_plan_name } + end + + unless hailstorm_config.jmeter.data_files.blank? data_file_name = hailstorm_config.jmeter.data_files.find { |e| e.to_java_string.hash_code == id.to_i } hailstorm_config.jmeter.data_files.reject! { |e| e == data_file_name } if data_file_name end diff --git a/hailstorm-api/app/helpers/api_helper.rb b/hailstorm-api/app/helpers/api_helper.rb index a55d0a06..a379ead2 100644 --- a/hailstorm-api/app/helpers/api_helper.rb +++ b/hailstorm-api/app/helpers/api_helper.rb @@ -15,7 +15,7 @@ def deep_encode(obj) Base64.encode64(Marshal.dump(obj)) end - # @param [Sting] serz + # @param [String] serz # @return [Object] def deep_decode(serz) Marshal.load(Base64.decode64(serz)) # rubocop:disable Security/MarshalLoad diff --git a/hailstorm-api/app/helpers/jmeter_helper.rb b/hailstorm-api/app/helpers/jmeter_helper.rb index e83e395f..cc3fc781 100644 --- a/hailstorm-api/app/helpers/jmeter_helper.rb +++ b/hailstorm-api/app/helpers/jmeter_helper.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +require 'hailstorm/model/project' +require 'hailstorm/model/jmeter_plan' +require 'hailstorm/model/client_stat' + # Helper for JMeter API module JMeterHelper @@ -16,14 +20,13 @@ def to_jmeter_attributes(hailstorm_config, project_id, partial_attrs) obj[:projectId] = project_id obj[:path] = File.dirname(partial_attrs[:test_plan_name]) if partial_attrs[:jmx_file] - obj[:id] = compute_test_plan_id(partial_attrs[:test_plan_name]) - obj[:name] = "#{File.basename(partial_attrs[:test_plan_name])}.jmx" properties = hailstorm_config.jmeter.properties(test_plan: partial_attrs[:test_plan_name]) obj[:properties] = properties.entries + add_jmx_attributes(obj, partial_attrs, project_id) else obj[:id] = compute_data_file_id(partial_attrs[:test_plan_name]) obj[:name] = File.basename(partial_attrs[:test_plan_name]) - obj[:dataFile] = true + obj[:data_file] = true end obj @@ -59,6 +62,44 @@ def compute_data_file_id(data_file_name) data_file_name.to_java_string.hash_code end + # @param [Hash] data + # @param [Hailstorm::Support::Configuration] hailstorm_config + # @param [String] test_plan_name + def handle_disabled(data, hailstorm_config, test_plan_name) + return if data['disabled'].nil? + + if data['disabled'] + already_disabled = hailstorm_config.jmeter.disabled_test_plans.include?(test_plan_name) + hailstorm_config.jmeter.disabled_test_plans.push(test_plan_name) unless already_disabled + else + hailstorm_config.jmeter.disabled_test_plans.reject! { |e| e == test_plan_name } + end + end + + # @param [Hailstorm::Support::Configuration] hailstorm_config + # @param [String] test_plan_name + # @param [Integer] project_id + # @return [Hash] + def build_patch_response(hailstorm_config, test_plan_name, project_id) + path, name = test_plan_name.split('/') + resp = { id: test_plan_name.to_java_string.hash_code, + name: "#{name}.jmx", + path: path, + properties: hailstorm_config.jmeter.properties(test_plan: test_plan_name).entries, + plan_executed_before: client_stats?(project_id, name) } + + resp[:disabled] = true if hailstorm_config.jmeter.disabled_test_plans.include?(test_plan_name) + resp + end + + # @param [Integer] project_id + # @param [String] test_plan_name + def client_stats?(project_id, test_plan_name) + project = Hailstorm::Model::Project.find(project_id) + test_plan = project.jmeter_plans.where(test_plan_name: test_plan_name).first + test_plan && Hailstorm::Model::ClientStat.where(jmeter_plan_id: test_plan.id).count.positive? + end + private def jmeter_attributes(data, file_id, found_project) @@ -70,7 +111,7 @@ def jmeter_attributes(data, file_id, found_project) } jmeter_plan[:properties] = data['properties'] if data['properties'] - jmeter_plan[:dataFile] = true if data['dataFile'] + jmeter_plan[:data_file] = true if data['dataFile'] jmeter_plan end @@ -107,4 +148,11 @@ def validate_jmeter_plan(jmeter_plan, local_file_path, response_data) end end end + + def add_jmx_attributes(obj, partial_attrs, project_id) + obj[:id] = compute_test_plan_id(partial_attrs[:test_plan_name]) + obj[:name] = "#{File.basename(partial_attrs[:test_plan_name])}.jmx" + obj[:disabled] = partial_attrs[:disabled] if partial_attrs.key?(:disabled) + obj[:plan_executed_before] = client_stats?(project_id, File.basename(partial_attrs[:test_plan_name])) + end end diff --git a/hailstorm-api/app/helpers/projects_helper.rb b/hailstorm-api/app/helpers/projects_helper.rb index ae5b714a..d3dced78 100644 --- a/hailstorm-api/app/helpers/projects_helper.rb +++ b/hailstorm-api/app/helpers/projects_helper.rb @@ -79,7 +79,7 @@ def add_incomplete_attribute(project, project_attrs) project_config = ProjectConfiguration.where(project_id: project.id).first if project_config hailstorm_config = deep_decode(project_config.stringified_config) - if hailstorm_config.jmeter.test_plans.empty? || + if hailstorm_config.jmeter.enabled_test_plans.empty? || hailstorm_config.clusters.select { |e| e.active || e.active.nil? }.empty? project_attrs[:incomplete] = true end diff --git a/hailstorm-api/app/initializer/api_config.rb b/hailstorm-api/app/initializer/api_config.rb index 565a66ff..8c41b69e 100644 --- a/hailstorm-api/app/initializer/api_config.rb +++ b/hailstorm-api/app/initializer/api_config.rb @@ -8,6 +8,7 @@ require 'hailstorm/initializer/java_classpath' require 'initializer/db_config' require 'initializer/migrations' +require 'initializer/configuration_ext' require 'web_file_store' require 'version' diff --git a/hailstorm-api/app/initializer/configuration_ext.rb b/hailstorm-api/app/initializer/configuration_ext.rb new file mode 100644 index 00000000..dfd1259b --- /dev/null +++ b/hailstorm-api/app/initializer/configuration_ext.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'hailstorm/support/configuration' + +class Hailstorm::Support::Configuration + # JMeter extension + class JMeter + + def disabled_test_plans + # For backward compatibility. Existing marshalled representations do not have this field, and unmarshalling + # does not invoke the constructor. + @disabled_test_plans ||= [] + end + + attr_writer :disabled_test_plans + + alias original_initialize initialize + def initialize + original_initialize + self.disabled_test_plans = [] + end + + # @return [Array] hash hash.keys = [:test_plan_name, :jmx_file, :disabled] + # test_plan_name: String + # jmx_file: Boolean, true + # disabled: Boolean + def all_test_plans_attrs + return [] if self.test_plans.nil? + + self.test_plans.map do |plan| + attrs = { test_plan_name: plan, jmx_file: true } + attrs[:disabled] = true if self.disabled_test_plans.include?(plan) + attrs + end + end + + # All test plans that are not disabled. Does not include data files + # @return [Array] + def enabled_test_plans + self.test_plans.reject { |plan| self.disabled_test_plans.include?(plan) } + end + end +end diff --git a/hailstorm-api/app/version.rb b/hailstorm-api/app/version.rb index 88e36f49..9aa2c967 100644 --- a/hailstorm-api/app/version.rb +++ b/hailstorm-api/app/version.rb @@ -3,6 +3,6 @@ # Version module Hailstorm module Api - VERSION = '1.0.17' + VERSION = '1.0.18' end end diff --git a/hailstorm-api/spec/api/jmeter_plans_spec.rb b/hailstorm-api/spec/api/jmeter_plans_spec.rb index b09d0a60..756b214f 100644 --- a/hailstorm-api/spec/api/jmeter_plans_spec.rb +++ b/hailstorm-api/spec/api/jmeter_plans_spec.rb @@ -78,6 +78,42 @@ expect(data_file[:path]).to eq('234') expect(data_file[:dataFile]).to be true end + + it 'should include disabled plans in list' do + project = Hailstorm::Model::Project.create!(project_code: 'api_jmeter_plans_spec') + hailstorm_config = Hailstorm::Support::Configuration.new + hailstorm_config.jmeter do |jmeter| + jmeter.add_test_plan('123/a.jmx') + jmeter.properties(test_plan: '123/a.jmx') do |map| + map['NumUsers'] = '100' + end + + jmeter.add_test_plan('124/b.jmx') + jmeter.properties(test_plan: '124/b.jmx') do |map| + map['NumUsers'] = '20' + end + + jmeter.disabled_test_plans.push('124/b') + jmeter.data_files = %w[234/foo.csv] + end + + ProjectConfiguration.create!(project_id: project.id, stringified_config: deep_encode(hailstorm_config)) + + @browser.get("/projects/#{project.id}/jmeter_plans") + expect(@browser.last_response).to be_ok + # @type [Array] res + res = JSON.parse(@browser.last_response.body) + expect(res.size).to eq(3) + expect(res.first.symbolize_keys[:name]).to eq('a.jmx') + + jmeter_plan = res[1].symbolize_keys + expect(jmeter_plan[:name]).to eq('b.jmx') + expect(jmeter_plan[:disabled]).to be == true + + data_file = res[2].symbolize_keys + expect(data_file[:name]).to eq('foo.csv') + expect(data_file[:dataFile]).to be true + end end context 'PATCH /projects/:project_id/jmeter_plans/:id' do @@ -110,10 +146,63 @@ @browser.patch("/projects/#{project.id}/jmeter_plans/#{post_res[:id]}", JSON.dump(patch_params)) expect(@browser.last_response).to be_ok patch_res = JSON.parse(@browser.last_response.body).symbolize_keys - expect(patch_res.keys.sort).to eq(%i[name path properties id].sort) + expect(patch_res.keys.sort).to eq(%i[name path properties id planExecutedBefore].sort) expect(patch_res[:id]).to eq(post_res[:id]) expect(patch_res[:properties].to_h).to eq(patch_params[:properties].to_h) end + + it 'should disable a test plan' do + project = Hailstorm::Model::Project.create!(project_code: 'api_jmeter_plans_spec') + params = { + name: 'hailstorm.jmx', + path: '1234', + properties: [ + %w[NumUsers 10], + %w[RampUp 30], + %w[Duration 180], + %w[ServerName 152.36.34.28] + ] + } + + @browser.post("/projects/#{project.id}/jmeter_plans", JSON.dump(params)) + expect(@browser.last_response).to be_ok + post_res = JSON.parse(@browser.last_response.body).symbolize_keys + + patch_params = { disabled: true } + @browser.patch("/projects/#{project.id}/jmeter_plans/#{post_res[:id]}", JSON.dump(patch_params)) + expect(@browser.last_response).to be_ok + patch_res = JSON.parse(@browser.last_response.body).symbolize_keys + expect(patch_res.keys.sort).to eq(%i[name path properties id disabled planExecutedBefore].sort) + expect(patch_res[:id]).to eq(post_res[:id]) + expect(patch_res[:properties].to_h).to eq(params[:properties].to_h) + expect(patch_res[:disabled]).to eql(true) + end + + it 'should enable a previously disabled test plan' do + project = Hailstorm::Model::Project.create!(project_code: 'api_jmeter_plans_spec') + params = { + name: 'hailstorm.jmx', + path: '1234', + properties: [ + %w[NumUsers 10], + %w[RampUp 30], + %w[Duration 180], + %w[ServerName 152.36.34.28] + ] + } + + @browser.post("/projects/#{project.id}/jmeter_plans", JSON.dump(params)) + expect(@browser.last_response).to be_ok + post_res = JSON.parse(@browser.last_response.body).symbolize_keys + @browser.patch("/projects/#{project.id}/jmeter_plans/#{post_res[:id]}", JSON.dump({ disabled: true })) + expect(@browser.last_response).to be_ok + + @browser.patch("/projects/#{project.id}/jmeter_plans/#{post_res[:id]}", JSON.dump({ disabled: false })) + expect(@browser.last_response).to be_ok + patch_res = JSON.parse(@browser.last_response.body).symbolize_keys + expect(patch_res[:disabled]).to be_nil + expect(patch_res[:properties].to_h).to eq(params[:properties].to_h) + end end context 'DELETE /projects/:project_id/jmeter_plans/:id' do @@ -177,5 +266,32 @@ expect(updated_hailstorm_config.jmeter.test_plans.size).to eq(1) expect(updated_hailstorm_config.jmeter.data_files.size).to eq(0) end + + it 'should not delete a test plan if it has been used in a previous test run' do + allow(Hailstorm::Model::ClientStat).to receive_message_chain(:where, :count).and_return(1) + mock_test_plan = double(Hailstorm::Model::JmeterPlan, id: 12) + allow_any_instance_of(Hailstorm::Model::Project).to receive_message_chain(:jmeter_plans, + :where).and_return([mock_test_plan]) + + project = Hailstorm::Model::Project.create!(project_code: 'api_jmeter_plans_spec') + hailstorm_config = Hailstorm::Support::Configuration.new + hailstorm_config.jmeter do |jmeter| + jmeter.add_test_plan('1/a.jmx') + jmeter.properties(test_plan: '1/a.jmx') do |map| + map['NumUsers'] = 100 + end + + jmeter.data_files.push('2/a.csv') + end + + ProjectConfiguration.create!( + project_id: project.id, + stringified_config: deep_encode(hailstorm_config) + ) + + id = '1/a'.to_java_string.hash_code + @browser.delete("/projects/#{project.id}/jmeter_plans/#{id}") + expect(@browser.last_response).to_not be_successful + end end end diff --git a/hailstorm-api/spec/helpers/projects_helper_spec.rb b/hailstorm-api/spec/helpers/projects_helper_spec.rb index 981b2970..464830c1 100644 --- a/hailstorm-api/spec/helpers/projects_helper_spec.rb +++ b/hailstorm-api/spec/helpers/projects_helper_spec.rb @@ -90,5 +90,24 @@ expect(attrs).to_not include(:live) end end + + context 'when all JMeter test plans are disabled' do + it 'should add incomplete attribute' do + config = Hailstorm::Support::Configuration.new + config.jmeter.add_test_plan('123/a.jmx') + config.jmeter.disabled_test_plans.push('123/a') + config.jmeter.data_files.push('135/b.csv') + config.clusters(:amazon_cloud) do |amz| + amz.access_key = 'a' + amz.secret_key = 'x' + amz.region = 'us-east-1' + end + + ProjectConfiguration.create!(project: @project, stringified_config: deep_encode(config)) + attrs = @api_instance.project_attributes(@project) + expect(attrs).to include(:incomplete) + expect(attrs[:incomplete]).to be == true + end + end end end diff --git a/hailstorm-web-client/README.md b/hailstorm-web-client/README.md index c0f688a3..e81defe6 100644 --- a/hailstorm-web-client/README.md +++ b/hailstorm-web-client/README.md @@ -84,7 +84,6 @@ This section has moved here: https://facebook.github.io/create-react-app/docs/tr ## Principles - Use flat source structure as much as possible. -- Components with contained components become top level in source hierarchy even if they are not top level components in the DOM hierarchy. - Main component is responsible for connecting with global state & reducer. They get passed on as props to contained components. - Use CSS Modules for component CSS customizations, and SCSS for global styles. -- Write new tests with Enzyme, but if they are flaky, change to react-test-utils. A test can be considered flaky if the test fails when an implementation detail changes, or they are timing dependent. +- Write new tests with [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/). If existing Enyzme tests are flaky, change to React Testing Library. A test can be considered flaky if the test fails when an implementation detail changes, or they are timing dependent. diff --git a/hailstorm-web-client/package-lock.json b/hailstorm-web-client/package-lock.json index 974c9160..c55e3108 100644 --- a/hailstorm-web-client/package-lock.json +++ b/hailstorm-web-client/package-lock.json @@ -1,6 +1,6 @@ { "name": "hailstorm-web-client", - "version": "1.7.10", + "version": "1.8.10", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/hailstorm-web-client/package.json b/hailstorm-web-client/package.json index b5e258d4..ed567ea1 100644 --- a/hailstorm-web-client/package.json +++ b/hailstorm-web-client/package.json @@ -1,6 +1,6 @@ { "name": "hailstorm-web-client", - "version": "1.7.10", + "version": "1.8.10", "private": true, "dependencies": { "date-fns": "^2.6.0", diff --git a/hailstorm-web-client/src/ClusterConfiguration/AWSView.tsx b/hailstorm-web-client/src/ClusterConfiguration/AWSView.tsx index 6303769b..24a917a5 100644 --- a/hailstorm-web-client/src/ClusterConfiguration/AWSView.tsx +++ b/hailstorm-web-client/src/ClusterConfiguration/AWSView.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { Project, AmazonCluster } from '../domain'; import { RemoveCluster } from './RemoveCluster'; -import styles from './ClusterConfiguration.module.scss'; +import styles from '../NewProjectWizard/NewProjectWizard.module.scss'; import { ReadOnlyField } from './ReadOnlyField'; import { ClusterViewHeader } from './ClusterViewHeader'; import { MaxUsersByInstance } from './AWSInstanceChoice'; diff --git a/hailstorm-web-client/src/ClusterConfiguration/ClusterConfiguration.module.scss b/hailstorm-web-client/src/ClusterConfiguration/ClusterConfiguration.module.scss index a13d45e2..78b48ab3 100644 --- a/hailstorm-web-client/src/ClusterConfiguration/ClusterConfiguration.module.scss +++ b/hailstorm-web-client/src/ClusterConfiguration/ClusterConfiguration.module.scss @@ -12,11 +12,3 @@ } } } - -.disabledContent { - background-color: $white-ter; -} - -.titleLabel { - margin-left: 1rem; -} diff --git a/hailstorm-web-client/src/ClusterConfiguration/DataCenterView.tsx b/hailstorm-web-client/src/ClusterConfiguration/DataCenterView.tsx index ce18f39e..226dd661 100644 --- a/hailstorm-web-client/src/ClusterConfiguration/DataCenterView.tsx +++ b/hailstorm-web-client/src/ClusterConfiguration/DataCenterView.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { DataCenterCluster, Project } from '../domain'; import { RemoveCluster } from './RemoveCluster'; -import styles from './ClusterConfiguration.module.scss'; +import styles from '../NewProjectWizard/NewProjectWizard.module.scss'; import { ReadOnlyField } from './ReadOnlyField'; import { ClusterViewHeader } from './ClusterViewHeader'; diff --git a/hailstorm-web-client/src/ClusterList/ClusterList.test.tsx b/hailstorm-web-client/src/ClusterList/ClusterList.test.tsx index 0f3e2642..f2446642 100644 --- a/hailstorm-web-client/src/ClusterList/ClusterList.test.tsx +++ b/hailstorm-web-client/src/ClusterList/ClusterList.test.tsx @@ -47,22 +47,6 @@ describe('', () => { expect(wrapper.find('.button')).toBeDisabled(); }); - it('should display active cluster on top of list', async () => { - const {findAllByText, debug} = render( - - ); - - const clusters = await findAllByText(/AWS us\-/); - expect(clusters[0].textContent).toMatch(/AWS us-west-1/); - expect(clusters[1].textContent).toMatch(/AWS us-east-1/); - }); - it('should display disabled clusters at bottom', async () => { const {findAllByText, debug} = render( = ({clusters, showEdit, onSelectCluster, activeCluster, disableEdit, onEdit, showDisabledCluster}) => { const sortFn: (a: Cluster, b: Cluster) => number = (a, b) => { - if (activeCluster && activeCluster.id === b.id) { - return 1; - } - if (a.disabled) { return 1; } diff --git a/hailstorm-web-client/src/JMeterConfiguration/ActiveFileDetail.tsx b/hailstorm-web-client/src/JMeterConfiguration/ActiveFileDetail.tsx index 2ae97203..75a780dc 100644 --- a/hailstorm-web-client/src/JMeterConfiguration/ActiveFileDetail.tsx +++ b/hailstorm-web-client/src/JMeterConfiguration/ActiveFileDetail.tsx @@ -1,9 +1,8 @@ import React from 'react'; import { NewProjectWizardState } from "../NewProjectWizard/domain"; -import { MergeJMeterFileAction } from './actions'; +import { DisableJMeterFileAction, EnableJMeterFileAction, MergeJMeterFileAction } from './actions'; import { ApiFactory } from '../api'; import { JMeterFileMessage } from './JMeterFileMessage'; -import { FormikActions } from 'formik'; import { JMeterFileDetail } from './JMeterFileDetail'; import { FormikActionsHandler } from './domain'; import { useNotifications } from '../app-notifications'; @@ -45,6 +44,27 @@ export function ActiveFileDetail({ state, dispatch, setShowModal, setUploadAbort .then(() => setSubmitting(false)); }; + const toggleDisabled = (disabled: boolean) => { + if (state.wizardState && state.wizardState.activeJMeterFile && state.wizardState.activeJMeterFile.id) { + ApiFactory() + .jmeter() + .update( + state.activeProject!.id, + state.wizardState.activeJMeterFile.id, + {disabled} + ) + .then(() => { + if (disabled) { + dispatch(new DisableJMeterFileAction(state.wizardState!.activeJMeterFile!.id!)); + notifiers.notifyWarning(`JMeter plan "${state.wizardState!.activeJMeterFile!.name}" disabled`); + } else { + dispatch(new EnableJMeterFileAction(state.wizardState!.activeJMeterFile!.id!)); + notifiers.notifySuccess(`JMeter plan "${state.wizardState!.activeJMeterFile!.name}" enabled`); + } + }); + } + } + return ( <> {!state.wizardState!.activeJMeterFile && ( @@ -57,7 +77,7 @@ export function ActiveFileDetail({ state, dispatch, setShowModal, setUploadAbort {state.wizardState!.activeJMeterFile && } diff --git a/hailstorm-web-client/src/JMeterConfiguration/JMeterConfiguration.test.tsx b/hailstorm-web-client/src/JMeterConfiguration/JMeterConfiguration.test.tsx index a2627f33..b0a6c272 100644 --- a/hailstorm-web-client/src/JMeterConfiguration/JMeterConfiguration.test.tsx +++ b/hailstorm-web-client/src/JMeterConfiguration/JMeterConfiguration.test.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { mount } from "enzyme"; +import { mount, ReactWrapper } from "enzyme"; import { JMeterConfiguration } from "./JMeterConfiguration"; -import { AppStateContext } from '../appStateContext'; +import { AppStateProviderWithProps } from '../AppStateProvider'; import { AppState, Action } from '../store'; import { WizardTabTypes, JMeterFileUploadState } from '../NewProjectWizard/domain'; -import { JMeterFile, Project, JMeter, ValidationNotice } from '../domain'; +import { JMeterFile, Project, JMeter, ValidationNotice, ExecutionCycle, ExecutionCycleStatus } from '../domain'; import { JMeterValidationService } from "../services/JMeterValidationService"; import { JMeterService } from "../services/JMeterService"; import { SavedFile } from '../FileUpload/domain'; @@ -50,7 +50,12 @@ describe('', () => { } }; - function createComponent(attrs?: {plans?: JMeterFile[]}, incomplete: boolean = false) { + function createComponent( + attrs?: { + plans?: JMeterFile[] + }, + incomplete: boolean = false + ) { let activeProject: Project = {id: 1, code: 'a', title: 'A', running: false}; if (!incomplete) { activeProject.jmeter = attrs && attrs.plans ? {files: attrs.plans} : {files: []}; @@ -58,9 +63,9 @@ describe('', () => { appState.activeProject = activeProject; return ( - + - + ) } @@ -308,7 +313,11 @@ describe('', () => { component.update(); const propertiesForm = component.find('JMeterPropertiesMap'); expect(propertiesForm).toExist(); - expect(propertiesForm.prop('properties')).toEqual(properties); + expect(propertiesForm.prop('properties')).toEqual([ + {key: "foo", value: undefined}, + {key: "bar", value: "x"}, + {key: "baz", value: 1} + ]); }); it('should disable Next and Back buttons when there are unsaved properties', () => { @@ -538,6 +547,25 @@ describe('', () => { expect(dispatch).not.toBeCalled(); }); + it('should not delete the file if the configured plan could not be deleted', async () => { + const jmeterFile = {id: 100, name: 'a.jmx', properties: new Map([["foo", "10"]])}; + appState.wizardState!.activeJMeterFile = jmeterFile; + const component = mount(withNotificationContext(createComponent({plans: [jmeterFile]}))); + component.find('ActiveFileDetail button[role="Remove File"]').simulate('click'); + const destroyFile = Promise.reject("mock API error"); + const destroySpy = jest.spyOn(JMeterService.prototype, "destroy").mockReturnValue(destroyFile); + const removeSpy = jest.spyOn(FileServer, "removeFile"); + component.find('Modal button').simulate('click'); + try { + await destroyFile; + } catch(error) { + // noop + } + + expect(destroySpy).toBeCalled(); + expect(removeSpy).not.toBeCalled(); + }); + it('should set JMeter configuration as complete on click of Next button', () => { const jmeterFile = {id: 100, name: 'a.jmx', properties: new Map([["foo", "10"]])}; const dataFile = {id: 99, name: 'a.csv', dataFile: true}; @@ -567,4 +595,68 @@ describe('', () => { component.update(); expect(component).toContainExactlyOneMatchingElement('#modal'); }); + + describe('when a plan is enabled and executed at least once', () => { + let component: ReactWrapper, React.Component<{}, {}, any>>; + beforeEach(() => { + const jmeterFile = {id: 100, name: 'a.jmx', properties: new Map([["foo", "10"]]), planExecutedBefore: true}; + appState.wizardState!.activeJMeterFile = {...jmeterFile}; + component = mount(withNotificationContext(createComponent({plans: [jmeterFile]}))); + component.update(); + }); + + it('should show "Disable" instead of "Remove"', () => { + const disableBtn = component.find('button').findWhere((wrapper) => wrapper.text() === 'Disable').at(0); + expect(disableBtn).toExist(); + }); + + it('should disable a plan', async () => { + const file: JMeterFile = {id: 100, name: 'a.jmx'}; + const updatePromise = Promise.resolve({...file, disabled: true}); + const spy = jest.spyOn(JMeterService.prototype, 'update').mockReturnValue(updatePromise); + const disableBtn = component.find('button').findWhere((wrapper) => wrapper.text() === 'Disable').at(0); + disableBtn.simulate('click'); + await updatePromise; + expect(spy).toHaveBeenCalled(); + expect(dispatch).toHaveBeenCalled(); + }); + }); + + describe('when a plan is disabled', () => { + let component: ReactWrapper, React.Component<{}, {}, any>>; + beforeEach(() => { + const jmeterFile = {id: 100, name: 'a.jmx', properties: new Map([["foo", "10"]]), disabled: true}; + appState.wizardState!.activeJMeterFile = {...jmeterFile}; + component = mount(withNotificationContext(createComponent({plans: [jmeterFile]}))); + component.update(); + }); + + it('should show readonly fields in detail view', () => { + expect(component).toContainMatchingElement('input[name="foo"]'); + expect(component.find('input[name="foo"]')).toHaveProp('readOnly'); + const enableBtn = component.find('button').findWhere((wrapper) => wrapper.text() === 'Enable').at(0); + expect(enableBtn).toExist(); + }); + + it('should Enable a disabled plan', async () => { + const file: JMeterFile = {id: 100, name: 'a.jmx', disabled: true}; + const updatePromise = Promise.resolve({...file}); + const spy = jest.spyOn(JMeterService.prototype, 'update').mockReturnValue(updatePromise); + const enableBtn = component.find('button').findWhere((wrapper) => wrapper.text() === 'Enable').at(0); + enableBtn.simulate('click'); + await updatePromise; + expect(spy).toHaveBeenCalled(); + expect(dispatch).toHaveBeenCalled(); + }); + + it('should disable Next button when all test plans are disabled', () => { + const component = mount(createComponent({plans: [ + {id: 100, name: 'a.jmx', properties: new Map([["foo", "10"]]), disabled: true}, + {id: 110, name: 'a.csv', dataFile: true } + ]})); + + const nextButton = component.find('button').findWhere((wrapper) => wrapper.text() === 'Next').at(0); + expect(nextButton).toBeDisabled(); + }); + }); }); diff --git a/hailstorm-web-client/src/JMeterConfiguration/JMeterConfiguration.tsx b/hailstorm-web-client/src/JMeterConfiguration/JMeterConfiguration.tsx index dd17e27b..cd676b3f 100644 --- a/hailstorm-web-client/src/JMeterConfiguration/JMeterConfiguration.tsx +++ b/hailstorm-web-client/src/JMeterConfiguration/JMeterConfiguration.tsx @@ -71,7 +71,7 @@ export const JMeterConfiguration: React.FC = () => { export function isNextDisabled(state: NewProjectWizardState): boolean { return ( !state.activeProject!.jmeter || - state.activeProject!.jmeter.files.filter(value => !value.dataFile).length === 0 || + state.activeProject!.jmeter.files.filter(value => !value.dataFile && !value.disabled).length === 0 || isBackDisabled(state) ); } @@ -107,16 +107,23 @@ async function destroyFile({ dispatch: React.Dispatch; notifiers: AppNotificationContextProps; }) { + let removeFile = true; + if (file.id) { try { await ApiFactory().jmeter().destroy(projectId, file.id); - notifiers.notifySuccess(`Deleted the JMeter configuration`); + notifiers.notifySuccess(`Removed the ${file.dataFile ? 'data file' : 'JMeter plan'} from configuration`); } catch (reason) { + removeFile = false; notifiers.notifyError("Failed to remove JMeter from configuration", reason); } } + if (!removeFile) { + return; + } + try { await FileServer.removeFile({ name: file.name, path: file.path! }); notifiers.notifySuccess(`Deleted file ${file.name}`); diff --git a/hailstorm-web-client/src/JMeterConfiguration/JMeterFileDetail.tsx b/hailstorm-web-client/src/JMeterConfiguration/JMeterFileDetail.tsx index 7ed2b000..c5ba65e3 100644 --- a/hailstorm-web-client/src/JMeterConfiguration/JMeterFileDetail.tsx +++ b/hailstorm-web-client/src/JMeterConfiguration/JMeterFileDetail.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { JMeterFile } from '../domain'; -import { FormikActions } from 'formik'; import { JMeterPropertiesMap } from './JMeterPropertiesMap'; import { JMeterFileUploadState } from '../NewProjectWizard/domain'; import { isUploadInProgress } from './isUploadInProgress'; @@ -10,22 +9,27 @@ export function JMeterFileDetail({ setShowModal, jmeterFile, onSubmit, - headerTitle + headerTitle, + toggleDisabled }: { setShowModal?: React.Dispatch>; jmeterFile: JMeterFile; onSubmit?: FormikActionsHandler; headerTitle?: string; + toggleDisabled?: (disabled: boolean) => void; }) { - return ( <> {mayShowProperties(jmeterFile) && ( ({key: value[0], value: value[1]}))} onSubmit={onSubmit} onRemove={setShowModal ? () => setShowModal(true) : undefined} + disabled={jmeterFile.disabled} + planExecutedBefore={jmeterFile.planExecutedBefore} + {...{toggleDisabled}} + fileId={jmeterFile.id} />)} {isFileUploaded(jmeterFile) && ( diff --git a/hailstorm-web-client/src/JMeterConfiguration/JMeterFileMessage.tsx b/hailstorm-web-client/src/JMeterConfiguration/JMeterFileMessage.tsx index b0726d44..1f4704ef 100644 --- a/hailstorm-web-client/src/JMeterConfiguration/JMeterFileMessage.tsx +++ b/hailstorm-web-client/src/JMeterConfiguration/JMeterFileMessage.tsx @@ -12,7 +12,6 @@ export function JMeterFileMessage({ setUploadAborted: React.Dispatch>; disableAbort: boolean; }) { - console.debug(file); let notification: JSX.Element | null = null; if (isUploadInProgress(file)) { notification = notifyUploadInProgress(file, disableAbort, setUploadAborted); diff --git a/hailstorm-web-client/src/JMeterConfiguration/JMeterPropertiesMap.test.tsx b/hailstorm-web-client/src/JMeterConfiguration/JMeterPropertiesMap.test.tsx new file mode 100644 index 00000000..12055d97 --- /dev/null +++ b/hailstorm-web-client/src/JMeterConfiguration/JMeterPropertiesMap.test.tsx @@ -0,0 +1,30 @@ +import { render } from '@testing-library/react'; +import React from 'react'; +import { PropertiesForm } from './JMeterPropertiesMap'; + +describe('', () => { + + it('should change properties in form', () => { + const {getByTestId, rerender} = render() + + const fooElement = getByTestId("foo"); + expect(fooElement.getAttribute("value")).toBe("10"); + + rerender(); + + const barElement = getByTestId("bar"); + expect(barElement.getAttribute("value")).toBe("20"); + }); +}); diff --git a/hailstorm-web-client/src/JMeterConfiguration/JMeterPropertiesMap.tsx b/hailstorm-web-client/src/JMeterConfiguration/JMeterPropertiesMap.tsx index 2f5fb21c..9046762f 100644 --- a/hailstorm-web-client/src/JMeterConfiguration/JMeterPropertiesMap.tsx +++ b/hailstorm-web-client/src/JMeterConfiguration/JMeterPropertiesMap.tsx @@ -1,17 +1,26 @@ import React from 'react'; -import { Formik, FormikActions, Field, Form, ErrorMessage } from 'formik'; +import { Formik, Field, Form, ErrorMessage } from 'formik'; import { FormikActionsHandler } from './domain'; +import styles from '../NewProjectWizard/NewProjectWizard.module.scss'; export function JMeterPropertiesMap({ properties, onSubmit, onRemove, - headerTitle + headerTitle, + planExecutedBefore, + toggleDisabled, + disabled, + fileId }: { - properties: Map; + properties: {key: string, value: any}[]; onSubmit?: FormikActionsHandler; onRemove?: () => void; headerTitle?: string; + planExecutedBefore?: boolean; + toggleDisabled?: (disabled: boolean) => void; + disabled?: boolean; + fileId: number | undefined; }) { return ( @@ -26,10 +35,17 @@ export function JMeterPropertiesMap({

) : null} - {onSubmit && onRemove ? ( - + {onSubmit && onRemove && !disabled ? ( + !!toggleDisabled && toggleDisabled(true)} + /> ) : ( - + !!toggleDisabled && toggleDisabled(false)} + /> )} ); @@ -45,14 +61,22 @@ function externalKey(key: string) { function Properties({ properties, - readOnly + readOnly, + disabled, + onEnable, + planExecutedBefore, + onRemove }: { - properties: Map; + properties: {key: string, value: any}[]; readOnly?: boolean; + disabled?: boolean; + onEnable?: () => void; + planExecutedBefore?: boolean; + onRemove?: () => void; }) { const readWrite = !readOnly; - const elements = Array.from(properties.keys()).map((key) => ( + const elements = properties.map(({key, value}) => (
@@ -64,7 +88,7 @@ function Properties({ className="input is-static has-background-light has-text-dark is-size-5" type="text" name={internalKey(key)} - value={properties.get(key)} + {...{value}} /> )}
@@ -75,29 +99,47 @@ function Properties({ )); return ( -
+ <> +
{elements}
+ {disabled && ( +
+ {!planExecutedBefore && ( +
+ +
)} +
+ +
+
)} + ) } -function PropertiesForm({ +export function PropertiesForm({ properties, onSubmit, - onRemove + onRemove, + planExecutedBefore, + onDisable, + fileId }: { - properties: Map; + properties: {key: string, value: any}[]; onSubmit: FormikActionsHandler; onRemove: () => void; + planExecutedBefore?: boolean; + onDisable: () => void; + fileId: number | undefined; }) { const initialValues: {[key: string]: any} = {}; - for (const [key, value] of properties) { + for (const {key, value} of properties) { initialValues[internalKey(key)] = value || ''; } - const isInitialValid = Array.from(properties.values()).every((value) => value !== undefined); + const isInitialValid = properties.every(({key, value}) => value !== undefined); const validate: (values: {[key: string]: any}) => {[key: string]: string} = (values) => { const errors: {[key: string]: string} = {}; Object.entries(values).forEach(([key, value]) => { @@ -121,14 +163,20 @@ function PropertiesForm({ return ( {({isSubmitting, isValid, dirty}) => (
+ {!planExecutedBefore && (
-
+
)} + {!!fileId && ( +
+ +
)}
diff --git a/hailstorm-web-client/src/JMeterConfiguration/StepContent.tsx b/hailstorm-web-client/src/JMeterConfiguration/StepContent.tsx index 4eec0a05..56661238 100644 --- a/hailstorm-web-client/src/JMeterConfiguration/StepContent.tsx +++ b/hailstorm-web-client/src/JMeterConfiguration/StepContent.tsx @@ -4,7 +4,14 @@ import { JMeterPlanList } from '../JMeterPlanList'; import styles from '../NewProjectWizard/NewProjectWizard.module.scss'; import { SelectJMeterFileAction } from './actions'; import { ActiveFileDetail } from './ActiveFileDetail'; -export function StepContent({ dispatch, state, setShowModal, setUploadAborted, disableAbort }: { + +export function StepContent({ + dispatch, + state, + setShowModal, + setUploadAborted, + disableAbort +}: { dispatch: React.Dispatch; state: NewProjectWizardState; setShowModal: React.Dispatch>; @@ -13,7 +20,12 @@ export function StepContent({ dispatch, state, setShowModal, setUploadAborted, d }) { return (
- dispatch(new SelectJMeterFileAction(file))} jmeter={state.activeProject!.jmeter} activeFile={state.wizardState!.activeJMeterFile} /> + dispatch(new SelectJMeterFileAction(file))} + jmeter={state.activeProject!.jmeter} + activeFile={state.wizardState!.activeJMeterFile} + showDisabled={true} + />
diff --git a/hailstorm-web-client/src/JMeterConfiguration/actions.ts b/hailstorm-web-client/src/JMeterConfiguration/actions.ts index 33ee5c20..2641be3a 100644 --- a/hailstorm-web-client/src/JMeterConfiguration/actions.ts +++ b/hailstorm-web-client/src/JMeterConfiguration/actions.ts @@ -12,6 +12,8 @@ export enum JMeterConfigurationActionTypes { SelectJMeterFile = '[JMeterConfiguration] SelectJMeterFile', RemoveJMeterFile = '[JMeterConfiguration] RemoveJMeterFile', FileRemoveInProgress = '[JMeterConfiguration] FileRemoveInProgress', + DisableJMeterFile = '[JMeterConfiguration] DisableJMeterFile', + EnableJMeterFile = '[JMeterConfiguration] EnableJMeterFile' } export class SetDefaultJMeterVersionAction implements Action { @@ -59,6 +61,16 @@ export class FileRemoveInProgressAction implements Action { constructor(public payload: string) {} } +export class DisableJMeterFileAction implements Action { + readonly type = JMeterConfigurationActionTypes.DisableJMeterFile; + constructor(public payload: number) {} +} + +export class EnableJMeterFileAction implements Action { + readonly type = JMeterConfigurationActionTypes.EnableJMeterFile; + constructor(public payload: number) {} +} + export type JMeterConfigurationActions = | SetDefaultJMeterVersionAction | SetJMeterConfigurationAction @@ -68,4 +80,6 @@ export type JMeterConfigurationActions = | MergeJMeterFileAction | SelectJMeterFileAction | RemoveJMeterFileAction - | FileRemoveInProgressAction; + | FileRemoveInProgressAction + | DisableJMeterFileAction + | EnableJMeterFileAction; diff --git a/hailstorm-web-client/src/JMeterConfiguration/reducer.test.ts b/hailstorm-web-client/src/JMeterConfiguration/reducer.test.ts index d2f42724..d5fd33ef 100644 --- a/hailstorm-web-client/src/JMeterConfiguration/reducer.test.ts +++ b/hailstorm-web-client/src/JMeterConfiguration/reducer.test.ts @@ -1,6 +1,6 @@ import { reducer } from './reducer'; import { WizardTabTypes, JMeterFileUploadState, NewProjectWizardState } from '../NewProjectWizard/domain'; -import { AddJMeterFileAction, AbortJMeterFileUploadAction, CommitJMeterFileAction, MergeJMeterFileAction, SetJMeterConfigurationAction, SelectJMeterFileAction, RemoveJMeterFileAction, FileRemoveInProgressAction } from './actions'; +import { AddJMeterFileAction, AbortJMeterFileUploadAction, CommitJMeterFileAction, MergeJMeterFileAction, SetJMeterConfigurationAction, SelectJMeterFileAction, RemoveJMeterFileAction, FileRemoveInProgressAction, DisableJMeterFileAction, EnableJMeterFileAction } from './actions'; import { JMeterFile, JMeter } from '../domain'; describe('reducer', () => { @@ -384,4 +384,84 @@ describe('reducer', () => { expect(nextState.wizardState!.activeJMeterFile!.removeInProgress).toEqual('a.jmx'); }); + + it('should disable a test plan', () => { + const testPlanA = { name: 'a.jmx', id: 10, properties: new Map([["foo", "10"]]) }; + const dataFile = { name: 'a.csv', id: 11, dataFile: true }; + const testPlanB = { name: 'b.jmx', id: 12, properties: new Map([["foo", "10"]]) }; + const nextState = reducer({ + activeProject: { + id: 1, + code: 'a', + title: 'A', + running: false, + autoStop: false, + jmeter: { + files: [testPlanA, testPlanB, dataFile] + } + }, + wizardState: { + activeTab: WizardTabTypes.JMeter, + done: { [WizardTabTypes.Project]: true}, + activeJMeterFile: { ...testPlanB } + } + }, new DisableJMeterFileAction(12)); + + expect(nextState.activeProject!.jmeter!.files[1].disabled).toBe(true); + expect(nextState.wizardState!.activeJMeterFile!.disabled).toBe(true); + }); + + it('should enable a test plan', () => { + const testPlanB = { name: 'b.jmx', id: 12, properties: new Map([["foo", "10"]]), disabled: true }; + const nextState = reducer({ + activeProject: { + id: 1, + code: 'a', + title: 'A', + running: false, + autoStop: false, + jmeter: { + files: [testPlanB] + } + }, + wizardState: { + activeTab: WizardTabTypes.JMeter, + done: { [WizardTabTypes.Project]: true}, + activeJMeterFile: { ...testPlanB } + } + }, new EnableJMeterFileAction(12)); + + expect(nextState.activeProject!.jmeter!.files[0].disabled).toBeFalsy(); + expect(nextState.wizardState!.activeJMeterFile!.disabled).toBeFalsy(); + }); + + it('should mark project as incomplete if all test plans are disabled', () => { + const testPlanA = { name: 'a.jmx', id: 10, properties: new Map([["foo", "10"]]) }; + const dataFile = { name: 'a.csv', id: 11, dataFile: true }; + const testPlanB = { name: 'b.jmx', id: 12, properties: new Map([["foo", "10"]]) }; + const state = { + activeProject: { + id: 1, + code: 'a', + title: 'A', + running: false, + autoStop: false, + jmeter: { + files: [testPlanA, testPlanB, dataFile] + } + }, + wizardState: { + activeTab: WizardTabTypes.JMeter, + done: { [WizardTabTypes.Project]: true}, + activeJMeterFile: { ...testPlanB } + } + }; + + const state1 = reducer(state, new DisableJMeterFileAction(12)); + expect(state1.activeProject!.incomplete).toBeFalsy(); + + const state2 = {...state1, wizardState: {...state1.wizardState!, activeJMeterFile: {...testPlanA}}}; + const state3 = reducer(state2, new DisableJMeterFileAction(10)); + expect(state3.activeProject!.incomplete).toBe(true); + }); }); diff --git a/hailstorm-web-client/src/JMeterConfiguration/reducer.ts b/hailstorm-web-client/src/JMeterConfiguration/reducer.ts index 8c94e8b7..c2465e3f 100644 --- a/hailstorm-web-client/src/JMeterConfiguration/reducer.ts +++ b/hailstorm-web-client/src/JMeterConfiguration/reducer.ts @@ -49,6 +49,14 @@ export function reducer(state: NewProjectWizardState, action: JMeterConfiguratio nextState = onFileRemoveInProgress(state, action); break; + case JMeterConfigurationActionTypes.DisableJMeterFile: + nextState = onChangeJMeterFileDisability(state, {id: action.payload, disabled: true}); + break; + + case JMeterConfigurationActionTypes.EnableJMeterFile: + nextState = onChangeJMeterFileDisability(state, {id: action.payload, disabled: false}); + break; + default: nextState = state; break; @@ -147,11 +155,10 @@ function onCommitJMeterFile(state: NewProjectWizardState, action: CommitJMeterFi activeJMeterFile }; const activeProject = { ...state.activeProject! }; - if (action.payload.autoStop !== undefined) { - if (activeProject.autoStop === undefined || activeProject.autoStop) { - activeProject.autoStop = action.payload.autoStop; - } + if (action.payload.autoStop !== undefined && activeProject.autoStop !== false) { + activeProject.autoStop = action.payload.autoStop; } + return { ...state, wizardState, activeProject }; } @@ -194,3 +201,44 @@ function jmeterFileCompare(a: JMeterFile, b: JMeterFile): number { return (scoreA - scoreB); } + +function onChangeJMeterFileDisability( + state: NewProjectWizardState, { + id, + disabled + }: { + id: number, + disabled: boolean + }): NewProjectWizardState { + + const activeProject = {...state.activeProject!}; + const wizardState = {...state.wizardState!}; + const disableJMeterPlan: (plan: JMeterFile, disabled: boolean) => JMeterFile = (plan, disabled) => { + if (disabled === true) { + return {...plan, disabled}; + } else { + const vNext = {...plan}; + delete vNext.disabled; + return vNext; + } + }; + + activeProject.jmeter!.files = activeProject.jmeter!.files.map((v) => { + if (v.id === id) { + return disableJMeterPlan(v, disabled); + } + + return v; + }); + + wizardState.activeJMeterFile = disableJMeterPlan(wizardState.activeJMeterFile!, disabled); + if (activeProject.jmeter!.files.filter((v) => !v.dataFile).every((v) => v.disabled)) { + activeProject.incomplete = true; + } + + if (wizardState.done[WizardTabTypes.Review]) { + wizardState.modifiedAfterReview = true; + } + + return {...state, activeProject, wizardState}; +} diff --git a/hailstorm-web-client/src/JMeterPlanList/JMeterPlanList.test.tsx b/hailstorm-web-client/src/JMeterPlanList/JMeterPlanList.test.tsx index 60407323..9e493cd1 100644 --- a/hailstorm-web-client/src/JMeterPlanList/JMeterPlanList.test.tsx +++ b/hailstorm-web-client/src/JMeterPlanList/JMeterPlanList.test.tsx @@ -68,4 +68,38 @@ describe('', () => { const wrapper = shallow(); expect(wrapper.find('.button')).toBeDisabled(); }); + + describe('when a plan is disabled', () => { + it('should show disabled tag in the list', () => { + const component = mount( + + ); + + const blocks = component.find('a').findWhere((wrapper) => wrapper.hasClass('panel-block')); + expect(blocks.at(0)).toContainExactlyOneMatchingElement('span.tag'); + }); + + it('should not show disabled plans by default', () => { + const component = mount( + + ); + + expect(component).toContainMatchingElements(1, 'a'); + }); + }); }); diff --git a/hailstorm-web-client/src/JMeterPlanList/JMeterPlanList.tsx b/hailstorm-web-client/src/JMeterPlanList/JMeterPlanList.tsx index afeb8156..3c5e62c6 100644 --- a/hailstorm-web-client/src/JMeterPlanList/JMeterPlanList.tsx +++ b/hailstorm-web-client/src/JMeterPlanList/JMeterPlanList.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { JMeter, JMeterFile } from '../domain'; import { EmptyPanel } from '../EmptyPanel'; +import styles from '../NewProjectWizard/NewProjectWizard.module.scss'; export interface JMeterPlanListProps { showEdit?: boolean; @@ -9,6 +10,7 @@ export interface JMeterPlanListProps { activeFile?: JMeterFile; disableEdit?: boolean; onEdit?: () => void; + showDisabled?: boolean; } export const JMeterPlanList: React.FC = ({ @@ -17,8 +19,14 @@ export const JMeterPlanList: React.FC = ({ onSelect, activeFile, disableEdit, - onEdit + onEdit, + showDisabled }) => { + let fileList: JMeterFile[] = []; + if (jmeter) { + fileList = showDisabled ? jmeter.files : jmeter.files.filter((v) => !v.disabled); + } + return (
@@ -39,17 +47,17 @@ export const JMeterPlanList: React.FC = ({
- {jmeter && jmeter.files.length > 0 ? renderPlanList(jmeter, onSelect, activeFile) : renderEmptyList()} + {fileList.length > 0 ? renderPlanList(fileList, onSelect, activeFile) : renderEmptyList()}
); } function renderPlanList( - jmeter: JMeter, + fileList: JMeterFile[], handleSelect?: (file: JMeterFile) => void, activeFile?: JMeterFile ): React.ReactNode { - return jmeter.files.map((plan) => { + return fileList.map((plan) => { const item = ( <> @@ -60,6 +68,7 @@ function renderPlanList( )} {plan.name} + {plan.disabled && (disabled)} ); diff --git a/hailstorm-web-client/src/NewProjectWizard/NewProjectWizard.module.scss b/hailstorm-web-client/src/NewProjectWizard/NewProjectWizard.module.scss index db2626fb..f9b93944 100644 --- a/hailstorm-web-client/src/NewProjectWizard/NewProjectWizard.module.scss +++ b/hailstorm-web-client/src/NewProjectWizard/NewProjectWizard.module.scss @@ -90,3 +90,11 @@ .dangerSettings { margin-top: 20rem; } + +.disabledContent { + background-color: $white-ter; +} + +.titleLabel { + margin-left: 1rem; +} diff --git a/hailstorm-web-client/src/NewProjectWizard/SummaryView.test.tsx b/hailstorm-web-client/src/NewProjectWizard/SummaryView.test.tsx index 62c59f52..0364e4ab 100644 --- a/hailstorm-web-client/src/NewProjectWizard/SummaryView.test.tsx +++ b/hailstorm-web-client/src/NewProjectWizard/SummaryView.test.tsx @@ -32,6 +32,14 @@ describe('', () => { id: 5, name: 'testdroid_accounts.csv', dataFile: true + }, + { + name: 'a.jmx', + id: 6, + properties: new Map([ + ["NumThreads", "10"] + ]), + disabled: true } ] }, @@ -136,4 +144,14 @@ describe('', () => { expect(dispatch).toBeCalled(); expect(dispatch.mock.calls[0][0]).toBeInstanceOf(ReviewCompletedAction); }); + + it('should not show disabled test plans', () => { + const component = mount( + + + + ); + + expect(component.text()).not.toMatch(/a\.jmx/); + }); }); diff --git a/hailstorm-web-client/src/NewProjectWizard/SummaryView.tsx b/hailstorm-web-client/src/NewProjectWizard/SummaryView.tsx index 64acb657..d88a666a 100644 --- a/hailstorm-web-client/src/NewProjectWizard/SummaryView.tsx +++ b/hailstorm-web-client/src/NewProjectWizard/SummaryView.tsx @@ -92,7 +92,7 @@ function JMeterSection({
- {jmeter.files.map((jmeterFile) => ( + {jmeter.files.filter((v) => !v.disabled).map((jmeterFile) => ( ))}
diff --git a/hailstorm-web-client/src/domain.ts b/hailstorm-web-client/src/domain.ts index 6ad44cb6..2ef7664f 100644 --- a/hailstorm-web-client/src/domain.ts +++ b/hailstorm-web-client/src/domain.ts @@ -75,6 +75,7 @@ export interface JMeterFile { dataFile?: boolean; disabled?: boolean; path?: string; + planExecutedBefore?: boolean; } export interface ValidationNotice {