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 (
-
+ <>
+
+ {disabled && (
+
)}
+ >
)
}
-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}) => (
)}
+ {!!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 {