Merge branch 'staging' of https://github.com/FarmBot/Farmbot-Web-App into lockout_prep
commit
fed516ea13
|
@ -1 +1 @@
|
|||
2.5.0
|
||||
2.5.1
|
||||
|
|
|
@ -4,7 +4,7 @@ node_js:
|
|||
before_script:
|
||||
- sudo apt-get install curl -y
|
||||
- mv node_modules/.bin/which node_modules/.bin/which.backup
|
||||
- rvm install 2.5.0 && rvm use 2.5.0
|
||||
- rvm install 2.5.1 && rvm use 2.5.1
|
||||
- mv node_modules/.bin/which.backup node_modules/.bin/which
|
||||
- cp config/database.travis.yml config/database.yml
|
||||
- mkdir -p public/app
|
||||
|
|
5
Gemfile
5
Gemfile
|
@ -1,5 +1,5 @@
|
|||
source "https://rubygems.org"
|
||||
ruby "2.5.0"
|
||||
ruby "2.5.1"
|
||||
|
||||
gem "rails"
|
||||
gem "thin"
|
||||
|
@ -27,8 +27,9 @@ gem "request_store"
|
|||
gem "secure_headers"
|
||||
gem "valid_url"
|
||||
gem "font-awesome-rails"
|
||||
gem "discard", "~> 1.0"
|
||||
gem "discard"
|
||||
gem "scenic"
|
||||
gem "bullet"
|
||||
|
||||
group :development, :test do
|
||||
gem "lol_dba"
|
||||
|
|
107
Gemfile.lock
107
Gemfile.lock
|
@ -6,7 +6,7 @@ GIT
|
|||
|
||||
GIT
|
||||
remote: https://github.com/fog/fog-google
|
||||
revision: ee58e2a4d9502f2a4dc102ca6a4b664656551e3f
|
||||
revision: c1be700a0c9557e8cf58b0eaa5d95bb078ca7fe5
|
||||
specs:
|
||||
fog-google (1.3.3)
|
||||
fog-core
|
||||
|
@ -17,25 +17,25 @@ GIT
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (5.1.5)
|
||||
actionpack (= 5.1.5)
|
||||
actioncable (5.1.6)
|
||||
actionpack (= 5.1.6)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (~> 0.6.1)
|
||||
actionmailer (5.1.5)
|
||||
actionpack (= 5.1.5)
|
||||
actionview (= 5.1.5)
|
||||
activejob (= 5.1.5)
|
||||
actionmailer (5.1.6)
|
||||
actionpack (= 5.1.6)
|
||||
actionview (= 5.1.6)
|
||||
activejob (= 5.1.6)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (5.1.5)
|
||||
actionview (= 5.1.5)
|
||||
activesupport (= 5.1.5)
|
||||
actionpack (5.1.6)
|
||||
actionview (= 5.1.6)
|
||||
activesupport (= 5.1.6)
|
||||
rack (~> 2.0)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionview (5.1.5)
|
||||
activesupport (= 5.1.5)
|
||||
actionview (5.1.6)
|
||||
activesupport (= 5.1.6)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
|
@ -45,18 +45,18 @@ GEM
|
|||
activemodel (>= 4.1, < 6)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
activejob (5.1.5)
|
||||
activesupport (= 5.1.5)
|
||||
activejob (5.1.6)
|
||||
activesupport (= 5.1.6)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (5.1.5)
|
||||
activesupport (= 5.1.5)
|
||||
activerecord (5.1.5)
|
||||
activemodel (= 5.1.5)
|
||||
activesupport (= 5.1.5)
|
||||
activemodel (5.1.6)
|
||||
activesupport (= 5.1.6)
|
||||
activerecord (5.1.6)
|
||||
activemodel (= 5.1.6)
|
||||
activesupport (= 5.1.6)
|
||||
arel (~> 8.0)
|
||||
activesupport (5.1.5)
|
||||
activesupport (5.1.6)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (~> 0.7)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.5.2)
|
||||
|
@ -65,15 +65,18 @@ GEM
|
|||
arel (8.0.0)
|
||||
bcrypt (3.1.11)
|
||||
builder (3.2.3)
|
||||
bullet (5.7.5)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11.0)
|
||||
bunny (2.9.2)
|
||||
amq-protocol (~> 2.3.0)
|
||||
capybara (2.18.0)
|
||||
capybara (3.0.1)
|
||||
addressable
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (>= 1.3.3)
|
||||
rack (>= 1.0.0)
|
||||
rack-test (>= 0.5.4)
|
||||
xpath (>= 2.0, < 4.0)
|
||||
nokogiri (~> 1.8)
|
||||
rack (>= 1.6.0)
|
||||
rack-test (>= 0.6.3)
|
||||
xpath (~> 3.0)
|
||||
case_transform (0.2)
|
||||
activesupport
|
||||
childprocess (0.9.0)
|
||||
|
@ -86,7 +89,7 @@ GEM
|
|||
url
|
||||
coderay (1.1.2)
|
||||
concurrent-ruby (1.0.5)
|
||||
crass (1.0.3)
|
||||
crass (1.0.4)
|
||||
daemons (1.2.6)
|
||||
database_cleaner (1.6.2)
|
||||
declarative (0.0.10)
|
||||
|
@ -108,7 +111,7 @@ GEM
|
|||
docile (1.3.0)
|
||||
erubi (1.7.1)
|
||||
eventmachine (1.2.5)
|
||||
excon (0.61.0)
|
||||
excon (0.62.0)
|
||||
factory_bot (4.8.2)
|
||||
activesupport (>= 3.0.0)
|
||||
factory_bot_rails (4.8.2)
|
||||
|
@ -132,8 +135,8 @@ GEM
|
|||
fog-xml (0.1.3)
|
||||
fog-core
|
||||
nokogiri (>= 1.5.11, < 2.0.0)
|
||||
font-awesome-rails (4.7.0.3)
|
||||
railties (>= 3.2, < 5.2)
|
||||
font-awesome-rails (4.7.0.4)
|
||||
railties (>= 3.2, < 6.0)
|
||||
foreman (0.84.0)
|
||||
thor (~> 0.19.1)
|
||||
formatador (0.2.5)
|
||||
|
@ -156,7 +159,7 @@ GEM
|
|||
signet (~> 0.7)
|
||||
hashdiff (0.3.7)
|
||||
httpclient (2.8.3)
|
||||
i18n (0.9.5)
|
||||
i18n (1.0.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.1.0)
|
||||
jsonapi-renderer (0.2.0)
|
||||
|
@ -212,22 +215,22 @@ GEM
|
|||
pry (>= 0.10.4)
|
||||
public_suffix (3.0.2)
|
||||
rack (2.0.4)
|
||||
rack-attack (5.1.0)
|
||||
rack-attack (5.2.0)
|
||||
rack
|
||||
rack-cors (1.0.2)
|
||||
rack-test (0.8.3)
|
||||
rack-test (1.0.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (5.1.5)
|
||||
actioncable (= 5.1.5)
|
||||
actionmailer (= 5.1.5)
|
||||
actionpack (= 5.1.5)
|
||||
actionview (= 5.1.5)
|
||||
activejob (= 5.1.5)
|
||||
activemodel (= 5.1.5)
|
||||
activerecord (= 5.1.5)
|
||||
activesupport (= 5.1.5)
|
||||
rails (5.1.6)
|
||||
actioncable (= 5.1.6)
|
||||
actionmailer (= 5.1.6)
|
||||
actionpack (= 5.1.6)
|
||||
actionview (= 5.1.6)
|
||||
activejob (= 5.1.6)
|
||||
activemodel (= 5.1.6)
|
||||
activerecord (= 5.1.6)
|
||||
activesupport (= 5.1.6)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 5.1.5)
|
||||
railties (= 5.1.6)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
|
@ -237,16 +240,16 @@ GEM
|
|||
activesupport (>= 3.2)
|
||||
choice (~> 0.2.0)
|
||||
ruby-graphviz (~> 1.2)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
rails-html-sanitizer (1.0.4)
|
||||
loofah (~> 2.2, >= 2.2.2)
|
||||
rails_12factor (0.0.3)
|
||||
rails_serve_static_assets
|
||||
rails_stdout_logging
|
||||
rails_serve_static_assets (0.0.5)
|
||||
rails_stdout_logging (0.0.5)
|
||||
railties (5.1.5)
|
||||
actionpack (= 5.1.5)
|
||||
activesupport (= 5.1.5)
|
||||
railties (5.1.6)
|
||||
actionpack (= 5.1.6)
|
||||
activesupport (= 5.1.6)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
|
@ -304,7 +307,7 @@ GEM
|
|||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
skylight (1.6.0)
|
||||
skylight (1.6.1)
|
||||
activesupport (>= 3.0.0)
|
||||
sprockets (3.7.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
|
@ -324,6 +327,7 @@ GEM
|
|||
tzinfo (1.2.5)
|
||||
thread_safe (~> 0.1)
|
||||
uber (0.1.0)
|
||||
uniform_notifier (1.11.0)
|
||||
url (0.3.2)
|
||||
useragent (0.16.10)
|
||||
valid_url (0.0.4)
|
||||
|
@ -344,6 +348,7 @@ PLATFORMS
|
|||
|
||||
DEPENDENCIES
|
||||
active_model_serializers
|
||||
bullet
|
||||
bunny
|
||||
capybara
|
||||
codecov
|
||||
|
@ -351,7 +356,7 @@ DEPENDENCIES
|
|||
delayed_job
|
||||
delayed_job_active_record
|
||||
devise
|
||||
discard (~> 1.0)
|
||||
discard
|
||||
factory_bot_rails
|
||||
faker
|
||||
figaro
|
||||
|
@ -389,7 +394,7 @@ DEPENDENCIES
|
|||
webpack-rails
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.5.0p0
|
||||
ruby 2.5.1p57
|
||||
|
||||
BUNDLED WITH
|
||||
1.16.1
|
||||
|
|
|
@ -162,7 +162,7 @@ private
|
|||
render json: {error: "Upgrade to latest FarmBot OS"}, status: 426
|
||||
end
|
||||
|
||||
EXPECTED_VER = Gem::Version::new('5.0.0')
|
||||
EXPECTED_VER = Gem::Version::new GlobalConfig.dump["MINIMUM_FBOS_VERSION"]
|
||||
|
||||
# Try to extract FarmBot OS version from user agent.
|
||||
# If none found, return lowest allowable version + 1 "tiny" bump to prevent
|
||||
|
|
|
@ -16,16 +16,8 @@ module Api
|
|||
render json: current_device.logs.where(*args_).limit(limit)
|
||||
end
|
||||
|
||||
# This is one of the "oddball" endpoints for the FarmBot API.
|
||||
# It is unique because it allows batch creation of logs.
|
||||
# When creating in batches, it is a "best effort" approach.
|
||||
# If some logs fail to save, they will fail silently.
|
||||
# As a matter of policy, not all log types are stored in the DB.
|
||||
def create
|
||||
case raw_json
|
||||
when Array then handle_many_logs
|
||||
when Hash then handle_single_log
|
||||
end
|
||||
mutate Logs::Create.run(raw_json, device: current_device)
|
||||
end
|
||||
|
||||
def index
|
||||
|
@ -46,22 +38,5 @@ private
|
|||
.not(id: current_device.limited_log_list.pluck(:id))
|
||||
.delete_all
|
||||
end
|
||||
|
||||
def handle_many_logs
|
||||
mutate Logs::BatchCreate.run(device: current_device, logs: raw_json)
|
||||
end
|
||||
|
||||
def handle_single_log
|
||||
outcome = Logs::Create.run(raw_json, device: current_device)
|
||||
if outcome.success?
|
||||
outcome.result.save!
|
||||
maybe_deliver(outcome.result)
|
||||
end
|
||||
mutate outcome
|
||||
end
|
||||
|
||||
def maybe_deliver(log_or_logs)
|
||||
LogDispatch.delay.deliver(current_device, log_or_logs)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,9 +33,7 @@ module Api
|
|||
end
|
||||
|
||||
def sequences
|
||||
@sequences ||= Sequence
|
||||
.includes(:primary_nodes, :edge_nodes)
|
||||
.where(device: current_device)
|
||||
@sequences ||= Sequence.with_usage_reports.where(device: current_device)
|
||||
end
|
||||
|
||||
def sequence
|
||||
|
|
|
@ -44,7 +44,9 @@ module Api
|
|||
# Every time a token is created, sweep the old TokenIssuances out of the
|
||||
# database.
|
||||
def clean_out_old_tokens
|
||||
TokenIssuance.where("exp < ?", Time.now.to_i).destroy_all
|
||||
TokenIssuance
|
||||
.where("exp < ?", Time.now.to_i)
|
||||
.destroy_all
|
||||
end
|
||||
|
||||
def if_properly_formatted
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
module Api
|
||||
class ToolsController < Api::AbstractController
|
||||
INDEX_QUERY = 'SELECT "tools".*, points.id as tool_slot_id FROM "tools" ' \
|
||||
'INNER JOIN "points" ON "points"."tool_id" = "tools"."id" ' \
|
||||
'AND "points"."pointer_type" IN (\'ToolSlot\') WHERE "tools"'\
|
||||
'."device_id" = %s;'
|
||||
|
||||
def index
|
||||
render json: tools
|
||||
end
|
||||
|
@ -29,11 +34,11 @@ private
|
|||
end
|
||||
|
||||
def tools
|
||||
Tool.includes(:tool_slot).where(device: current_device)
|
||||
Tool.find_by_sql(INDEX_QUERY % current_device.id)
|
||||
end
|
||||
|
||||
def tool
|
||||
@tool ||= tools.find(params[:id])
|
||||
@tool ||= Tool.where(device: current_device).find(params[:id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
class BatchLogDispatchJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(device, logs)
|
||||
LogDispatch.deliver(device, Log.create!(logs))
|
||||
end
|
||||
end
|
|
@ -86,11 +86,11 @@ module CeleryScript
|
|||
def misc_fields
|
||||
return {
|
||||
id: sequence.id,
|
||||
name: sequence.name,
|
||||
color: sequence.color,
|
||||
created_at: sequence.created_at,
|
||||
updated_at: sequence.updated_at,
|
||||
args: Sequence::DEFAULT_ARGS
|
||||
args: Sequence::DEFAULT_ARGS,
|
||||
color: sequence.color,
|
||||
name: sequence.name
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -110,11 +110,8 @@ module CeleryScript
|
|||
|
||||
def execute
|
||||
canonical_form = misc_fields.merge!(recurse_into_node(entry_node))
|
||||
canonical_form[:in_use] = \
|
||||
EdgeNode.where(kind: "sequence_id", value: sequence.id).exists? ||
|
||||
RegimenItem.where(sequence_id: sequence.id).exists? ||
|
||||
FarmEvent.where(executable: sequence).exists?
|
||||
s = canonical_form.with_indifferent_access
|
||||
s[:in_use] = sequence.in_use?
|
||||
# HISTORICAL NOTE:
|
||||
# When I prototyped the variables declaration stuff, a few (failed)
|
||||
# iterations snuck into the DB. Gradually migrating is easier than
|
||||
|
|
|
@ -8,12 +8,20 @@ class GlobalConfig < ApplicationRecord
|
|||
validates_presence_of :key
|
||||
|
||||
LONG_REVISION = ENV["BUILT_AT"] || ENV["HEROKU_SLUG_COMMIT"] || "NONE"
|
||||
DEFAULTS = { NODE_ENV: Rails.env || "development",
|
||||
TOS_URL: ENV.fetch("TOS_URL", ""),
|
||||
PRIV_URL: ENV.fetch("PRIV_URL", ""),
|
||||
LONG_REVISION: LONG_REVISION,
|
||||
SHORT_REVISION: LONG_REVISION.first(8),
|
||||
FBOS_END_OF_LIFE_VERSION: "0.0.0" }
|
||||
# Bootstrap initial defaults:
|
||||
{
|
||||
"NODE_ENV" => Rails.env || "development",
|
||||
"TOS_URL" => ENV.fetch("TOS_URL", ""),
|
||||
"PRIV_URL" => ENV.fetch("PRIV_URL", ""),
|
||||
"LONG_REVISION" => LONG_REVISION,
|
||||
"SHORT_REVISION" => LONG_REVISION.first(8),
|
||||
"FBOS_END_OF_LIFE_VERSION" => "0.0.0",
|
||||
"MINIMUM_FBOS_VERSION" => "6.0.0"
|
||||
}.map do |(key, value)|
|
||||
self.find_or_create_by(key: key) do |conf|
|
||||
conf.assign_attributes(key: key, value: value)
|
||||
end
|
||||
end
|
||||
|
||||
# Memoized version of every GlobalConfig, with key/values layed out in a hash.
|
||||
# Database values prempt values set in ::DEFAULTS
|
||||
|
@ -22,11 +30,8 @@ class GlobalConfig < ApplicationRecord
|
|||
end
|
||||
|
||||
def self.reload_
|
||||
config_hash = GlobalConfig
|
||||
.all
|
||||
.map(&:reload)
|
||||
.map{ |x| {x.key => x.value} }
|
||||
.reduce({}, :merge)
|
||||
@dump = DEFAULTS.merge(config_hash)
|
||||
@dump = {}
|
||||
GlobalConfig.all.map { |x| @dump[x.key] = x.value }
|
||||
@dump
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@ class Log < ApplicationRecord
|
|||
# pagination, but could later on.
|
||||
PAGE_SIZE = 25
|
||||
|
||||
DISCARD = ["fun", "debug"]
|
||||
DISCARD = ["fun", "debug"]
|
||||
# self.meta[:type] is used by the bot and the frontend as a sort of
|
||||
TYPES = CeleryScriptSettingsBag::ALLOWED_MESSAGE_TYPES
|
||||
# The means by which the message will be sent. Ex: frontend toast notification
|
||||
|
|
|
@ -8,25 +8,20 @@ class LogDispatch < ApplicationRecord
|
|||
self.max_per_hour = 20
|
||||
|
||||
# If this method grows, create a mutation.
|
||||
def self.deliver(device, log_or_logs)
|
||||
list = Array.wrap(log_or_logs)
|
||||
send_routine_emails(list, device)
|
||||
send_fatal_emails(list, device)
|
||||
def self.deliver(device, log)
|
||||
send_routine_emails(log, device)
|
||||
send_fatal_emails(log, device)
|
||||
end
|
||||
|
||||
def self.send_routine_emails(log_array, device)
|
||||
log_array
|
||||
.select { |log | (log["channels"] || []).include?("email") }
|
||||
.map { |log | { device: device, log: log }}
|
||||
.tap { |logs| self.create!(logs) }
|
||||
|
||||
def self.send_routine_emails(log, device)
|
||||
return unless (log["channels"] || []).include?("email")
|
||||
self.create!(device: device, log: log)
|
||||
LogDeliveryMailer.log_digest(device).deliver_later
|
||||
end
|
||||
|
||||
def self.send_fatal_emails(log_array, device)
|
||||
log_array
|
||||
.select { |log| (log["channels"] || []).include?("fatal_email") }
|
||||
.map { |log| FatalErrorMailer.fatal_error(device, log).deliver_later }
|
||||
def self.send_fatal_emails(log, device)
|
||||
return unless (log["channels"] || []).include?("fatal_email")
|
||||
FatalErrorMailer.fatal_error(device, log).deliver_later
|
||||
end
|
||||
|
||||
def broadcast?
|
||||
|
|
|
@ -16,10 +16,11 @@ class Sequence < ApplicationRecord
|
|||
include CeleryScriptSettingsBag
|
||||
|
||||
belongs_to :device
|
||||
has_many :farm_events, as: :executable
|
||||
has_many :regimen_items
|
||||
has_many :primary_nodes, dependent: :destroy
|
||||
has_many :edge_nodes, dependent: :destroy
|
||||
has_one :sequence_usage_report
|
||||
has_many :farm_events, as: :executable
|
||||
has_many :regimen_items
|
||||
has_many :primary_nodes, dependent: :destroy
|
||||
has_many :edge_nodes, dependent: :destroy
|
||||
|
||||
# allowable label colors for the frontend.
|
||||
[ :name, :kind ].each { |n| validates n, presence: true }
|
||||
|
@ -62,6 +63,20 @@ class Sequence < ApplicationRecord
|
|||
false unless destroyed?
|
||||
end
|
||||
|
||||
# Determines if the current sequence is used by any farmevents, regimens or
|
||||
# sequences.
|
||||
def in_use?
|
||||
[sequence_usage_report.edge_node_count,
|
||||
sequence_usage_report.farm_event_count,
|
||||
sequence_usage_report.regimen_items_count].max != 0
|
||||
end
|
||||
|
||||
# Eagerly load edge_node, primary_node and usage_report. This is a big deal
|
||||
# for performance when serializing sequences.
|
||||
def self.with_usage_reports
|
||||
self.includes(:sequence_usage_report, :edge_nodes, :primary_nodes)
|
||||
end
|
||||
|
||||
# THIS IS AN OVERRIDE - Special serialization required for auto sync.
|
||||
# When a sequence is created, we save it to the database to create a primary
|
||||
# key, then we iterate over `EdgeNode` and `PrimaryNode`s, assigning that
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# THIS IS A SQL VIEW. IT IS NOT A REAL TABLE.
|
||||
# Tracks how many resources are using the sequence (if any)
|
||||
class SequenceUsageReport < ApplicationRecord
|
||||
def readonly?
|
||||
true
|
||||
end
|
||||
end
|
|
@ -5,7 +5,7 @@ class Tool < ApplicationRecord
|
|||
belongs_to :device
|
||||
has_one :tool_slot
|
||||
validates :device, presence: true
|
||||
validates :name, uniqueness: { scope: :device }
|
||||
validates :name, uniqueness: { scope: :device }
|
||||
|
||||
IN_USE = "Tool in use by the following sequences: %s"
|
||||
end
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
module Logs
|
||||
class BatchCreate < Mutations::Command
|
||||
required do
|
||||
model :device, class: Device
|
||||
array :logs do
|
||||
hash do
|
||||
string :message
|
||||
optional do
|
||||
array :channels,
|
||||
class: String,
|
||||
in: CeleryScriptSettingsBag::ALLOWED_CHANNEL_NAMES
|
||||
hash :meta do
|
||||
string :type, in: Log::TYPES
|
||||
optional do
|
||||
integer :x
|
||||
integer :y
|
||||
integer :z
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def execute
|
||||
BatchLogDispatchJob.perform_later(device, clean_logs)
|
||||
return clean_logs
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clean_logs
|
||||
@clean_logs ||= logs
|
||||
.last(device.max_log_count)
|
||||
.map { |i| Logs::Create.run(i, device: device) }
|
||||
.select { |i| i.success? } # <= Ignore rejects
|
||||
.map { |i| i.result }
|
||||
.reject { |i| Log::DISCARD.include?(i.type) } # Discard jokes
|
||||
.map { |i| i.as_json.except("id") }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -51,6 +51,7 @@ module Logs
|
|||
end
|
||||
|
||||
def validate
|
||||
add_error :log, :private, BAD_WORDS if has_bad_words
|
||||
@log = Log.new
|
||||
@log.device = device
|
||||
@log.message = message
|
||||
|
@ -63,15 +64,19 @@ module Logs
|
|||
@log.minor_version = transitional_field(:minor_version)
|
||||
@log.type = transitional_field(:type)
|
||||
@log.validate!
|
||||
add_error :log, :private, BAD_WORDS if has_bad_words
|
||||
end
|
||||
|
||||
def execute
|
||||
@log.save! && maybe_deliver
|
||||
@log
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def maybe_deliver
|
||||
LogDispatch.delay.deliver(device, @log)
|
||||
end
|
||||
|
||||
def has_bad_words
|
||||
!!inputs[:message].upcase.match(BLACKLIST)
|
||||
end
|
||||
|
|
|
@ -13,7 +13,6 @@ module Points
|
|||
end
|
||||
|
||||
def validate
|
||||
puts "Convert this to use InUseTool after STI refactor."
|
||||
nope! if still_in_use?
|
||||
end
|
||||
|
||||
|
@ -34,21 +33,11 @@ module Points
|
|||
end
|
||||
|
||||
def nope!
|
||||
add_error :in_use, :in_use, (IN_USE % [names])
|
||||
end
|
||||
|
||||
def current_tool_id
|
||||
point.tool_id
|
||||
add_error :in_use, :in_use, (IN_USE % [deps.join(", ")])
|
||||
end
|
||||
|
||||
def deps
|
||||
@deps ||= Sequence
|
||||
.where(id: EdgeNode.where(kind: "tool_id", value: current_tool_id)
|
||||
.pluck(:sequence_id))
|
||||
end
|
||||
|
||||
def names
|
||||
@names ||= deps.pluck(:name).join(", ")
|
||||
@deps ||= InUseTool.where(tool_id: point.tool_id).pluck(:sequence_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,8 @@ class ToolSerializer < ActiveModel::Serializer
|
|||
attributes :id, :name, :status
|
||||
|
||||
def status
|
||||
active = ToolSlot.where(tool_id: object.id).any?
|
||||
active ? "active" : "inactive"
|
||||
# The attribute `tool_slot_id` is added via a special SQL query.
|
||||
# SEE: ToolsController::INDEX_QUERY
|
||||
object[:tool_slot_id] ? "active" : "inactive"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,10 @@ require "rails/all"
|
|||
Bundler.require(:default, Rails.env)
|
||||
module FarmBot
|
||||
class Application < Rails::Application
|
||||
config.after_initialize do
|
||||
Bullet.enable = true
|
||||
Bullet.console = true
|
||||
end
|
||||
config.active_job.queue_adapter = :delayed_job
|
||||
config.action_dispatch.perform_deep_munge = false
|
||||
I18n.enforce_available_locales = false
|
||||
|
|
|
@ -11,7 +11,11 @@ FarmBot::Application.configure do
|
|||
config.log_formatter = ::Logger::Formatter.new
|
||||
config.log_level = :info
|
||||
config.public_file_server.enabled = false
|
||||
|
||||
config.after_initialize do
|
||||
Bullet.enable = true
|
||||
Bullet.console = true
|
||||
Bullet.rollbar = true if ENV["ROLLBAR_ACCESS_TOKEN"]
|
||||
end
|
||||
# HACK AHEAD! Here's why:
|
||||
# 1. FarmBot Inc. Uses Sendgrid for email.
|
||||
# 2. FarmBot is an open source project that must be vendor neutral.
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
FarmBot::Application.configure do
|
||||
config.after_initialize do
|
||||
Bullet.enable = true
|
||||
Bullet.console = true
|
||||
Bullet.raise = true
|
||||
end
|
||||
|
||||
# The test environment is used exclusively to run your application's
|
||||
# test suite. You never need to work with it otherwise. Remember that
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
class Rack::Attack
|
||||
### Throttle Spammy Clients ###
|
||||
# Throttle all requests by IP 100 req/min
|
||||
throttle('req/ip', limit: 500, period: 5.minutes) do |req|
|
||||
throttle('req/ip', limit: 100, period: 1.minutes) do |req|
|
||||
req.ip
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
class AddMissingIndexes < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
add_index :edge_nodes, [:kind, :value]
|
||||
add_index :farm_events, :end_time
|
||||
add_index :global_configs, :key
|
||||
add_index :logs, :created_at
|
||||
add_index :logs, :type
|
||||
add_index :peripherals, :mode
|
||||
add_index :token_issuances, [:jti, :device_id]
|
||||
add_index :token_issuances, :exp
|
||||
add_index :users, :confirmation_token
|
||||
end
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
class AddMoreMissingIndexes < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
add_index :logs, :verbosity
|
||||
change_column :logs, :verbosity, :integer, default: 1
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
class AddXySwapToWebAppConfigs < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
add_column :web_app_configs,
|
||||
:xy_swap,
|
||||
:boolean,
|
||||
default: false
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class CreateSequenceUsageReports < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
create_view :sequence_usage_reports
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class RetroactivelySetDefaultVerbosity < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
Log.where(verbosity: nil).update_all(verbosity: 1)
|
||||
end
|
||||
end
|
29
db/schema.rb
29
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20180411175813) do
|
||||
ActiveRecord::Schema.define(version: 20180413145332) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -58,6 +58,7 @@ ActiveRecord::Schema.define(version: 20180411175813) do
|
|||
t.bigint "primary_node_id", null: false
|
||||
t.string "kind", limit: 50
|
||||
t.string "value", limit: 300
|
||||
t.index ["kind", "value"], name: "index_edge_nodes_on_kind_and_value"
|
||||
t.index ["primary_node_id"], name: "index_edge_nodes_on_primary_node_id"
|
||||
t.index ["sequence_id"], name: "index_edge_nodes_on_sequence_id"
|
||||
end
|
||||
|
@ -71,6 +72,7 @@ ActiveRecord::Schema.define(version: 20180411175813) do
|
|||
t.string "executable_type", limit: 280
|
||||
t.integer "executable_id"
|
||||
t.index ["device_id"], name: "index_farm_events_on_device_id"
|
||||
t.index ["end_time"], name: "index_farm_events_on_end_time"
|
||||
t.index ["executable_type", "executable_id"], name: "index_farm_events_on_executable_type_and_executable_id"
|
||||
end
|
||||
|
||||
|
@ -204,6 +206,7 @@ ActiveRecord::Schema.define(version: 20180411175813) do
|
|||
t.text "value"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["key"], name: "index_global_configs_on_key"
|
||||
end
|
||||
|
||||
create_table "images", id: :serial, force: :cascade do |t|
|
||||
|
@ -259,11 +262,14 @@ ActiveRecord::Schema.define(version: 20180411175813) do
|
|||
t.string "type", limit: 10, default: "info"
|
||||
t.integer "major_version"
|
||||
t.integer "minor_version"
|
||||
t.integer "verbosity"
|
||||
t.integer "verbosity", default: 1
|
||||
t.integer "x"
|
||||
t.integer "y"
|
||||
t.integer "z"
|
||||
t.index ["created_at"], name: "index_logs_on_created_at"
|
||||
t.index ["device_id"], name: "index_logs_on_device_id"
|
||||
t.index ["type"], name: "index_logs_on_type"
|
||||
t.index ["verbosity"], name: "index_logs_on_verbosity"
|
||||
end
|
||||
|
||||
create_table "peripherals", id: :serial, force: :cascade do |t|
|
||||
|
@ -274,6 +280,7 @@ ActiveRecord::Schema.define(version: 20180411175813) do
|
|||
t.datetime "updated_at", null: false
|
||||
t.integer "mode", default: 0
|
||||
t.index ["device_id"], name: "index_peripherals_on_device_id"
|
||||
t.index ["mode"], name: "index_peripherals_on_mode"
|
||||
end
|
||||
|
||||
create_table "pin_bindings", force: :cascade do |t|
|
||||
|
@ -388,6 +395,8 @@ ActiveRecord::Schema.define(version: 20180411175813) do
|
|||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["device_id"], name: "index_token_issuances_on_device_id"
|
||||
t.index ["exp"], name: "index_token_issuances_on_exp"
|
||||
t.index ["jti", "device_id"], name: "index_token_issuances_on_jti_and_device_id"
|
||||
end
|
||||
|
||||
create_table "tools", id: :serial, force: :cascade do |t|
|
||||
|
@ -416,6 +425,7 @@ ActiveRecord::Schema.define(version: 20180411175813) do
|
|||
t.datetime "confirmation_sent_at"
|
||||
t.string "unconfirmed_email"
|
||||
t.index ["agreed_to_terms_at"], name: "index_users_on_agreed_to_terms_at"
|
||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token"
|
||||
t.index ["device_id"], name: "index_users_on_device_id"
|
||||
t.index ["email"], name: "index_users_on_email", unique: true
|
||||
end
|
||||
|
@ -458,6 +468,7 @@ ActiveRecord::Schema.define(version: 20180411175813) do
|
|||
t.string "photo_filter_begin"
|
||||
t.string "photo_filter_end"
|
||||
t.boolean "discard_unsaved", default: false
|
||||
t.boolean "xy_swap", default: false
|
||||
t.index ["device_id"], name: "index_web_app_configs_on_device_id"
|
||||
end
|
||||
|
||||
|
@ -513,4 +524,18 @@ ActiveRecord::Schema.define(version: 20180411175813) do
|
|||
WHERE ((edge_nodes.kind)::text = 'tool_id'::text);
|
||||
SQL
|
||||
|
||||
create_view "sequence_usage_reports", sql_definition: <<-SQL
|
||||
SELECT sequences.id AS sequence_id,
|
||||
( SELECT count(*) AS count
|
||||
FROM edge_nodes
|
||||
WHERE (((edge_nodes.kind)::text = 'sequence_id'::text) AND ((edge_nodes.value)::integer = sequences.id))) AS edge_node_count,
|
||||
( SELECT count(*) AS count
|
||||
FROM farm_events
|
||||
WHERE ((farm_events.executable_id = sequences.id) AND ((farm_events.executable_type)::text = 'Sequence'::text))) AS farm_event_count,
|
||||
( SELECT count(*) AS count
|
||||
FROM regimen_items
|
||||
WHERE (regimen_items.sequence_id = sequences.id)) AS regimen_items_count
|
||||
FROM sequences;
|
||||
SQL
|
||||
|
||||
end
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
SELECT
|
||||
sequences.id as sequence_id,
|
||||
(SELECT COUNT(*)
|
||||
FROM edge_nodes
|
||||
WHERE edge_nodes.kind = 'sequence_id'
|
||||
AND (edge_nodes.value)::int = sequences.id) AS edge_node_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM farm_events
|
||||
WHERE farm_events.executable_id = sequences.id
|
||||
AND farm_events.executable_type = 'Sequence') AS farm_event_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM regimen_items
|
||||
WHERE regimen_items.sequence_id = sequences.id) AS regimen_items_count
|
||||
FROM
|
||||
sequences;
|
|
@ -35,7 +35,6 @@ class LogService
|
|||
if save?(log)
|
||||
device = Device.find(device_id)
|
||||
db_log = Logs::Create.run!(log, device: device)
|
||||
db_log.save!
|
||||
maybe_clear_logs(device)
|
||||
LogDispatch.deliver(device, db_log)
|
||||
end
|
||||
|
|
|
@ -8,6 +8,7 @@ describe Api::GlobalConfigController do
|
|||
value: "INITIAL_" + SecureRandom.hex)
|
||||
|
||||
it 'shows configs' do
|
||||
GlobalConfig.reload_
|
||||
get :show
|
||||
expect(json[:PING]).to eq(GlobalConfig.find_by(key: "PING").value)
|
||||
end
|
||||
|
|
|
@ -1,121 +0,0 @@
|
|||
[{
|
||||
"meta": {
|
||||
"z": 0,
|
||||
"y": 0,
|
||||
"x": 0,
|
||||
"type": "warn"
|
||||
},
|
||||
"message": "FIXME failed to install first party farmwares: %RuntimeError{message: \"Could not sync Elixir.Farmbot.Farmware.Installer.Repository.Farmbot is already synced up!\"}",
|
||||
"created_at": 1495820256,
|
||||
"channels": []
|
||||
}, {
|
||||
"meta": {
|
||||
"z": 0,
|
||||
"y": 0,
|
||||
"x": 0,
|
||||
"type": "info"
|
||||
},
|
||||
"message": "FIXME is installing first party Farmwares.",
|
||||
"created_at": 1495820255,
|
||||
"channels": []
|
||||
}, {
|
||||
"meta": {
|
||||
"z": 0,
|
||||
"y": 0,
|
||||
"x": 0,
|
||||
"type": "info"
|
||||
},
|
||||
"message": "FIXME ntp: :ok",
|
||||
"created_at": 1495820255,
|
||||
"channels": []
|
||||
}, {
|
||||
"meta": {
|
||||
"z": 0,
|
||||
"y": 0,
|
||||
"x": 0,
|
||||
"type": "warn"
|
||||
},
|
||||
"message": "No serial handler yet, waiting...",
|
||||
"created_at": 23,
|
||||
"channels": []
|
||||
}, {
|
||||
"meta": {
|
||||
"z": 0,
|
||||
"y": 0,
|
||||
"x": 0,
|
||||
"type": "info"
|
||||
},
|
||||
"message": "FIXME trying to set time (try 0)",
|
||||
"created_at": 22,
|
||||
"channels": []
|
||||
}, {
|
||||
"meta": {
|
||||
"z": 0,
|
||||
"y": 0,
|
||||
"x": 0,
|
||||
"type": "info"
|
||||
},
|
||||
"message": "FIXME is getting time from NTP.",
|
||||
"created_at": 22,
|
||||
"channels": []
|
||||
}, {
|
||||
"meta": {
|
||||
"z": 0,
|
||||
"y": 0,
|
||||
"x": 0,
|
||||
"type": "info"
|
||||
},
|
||||
"message": "FIXME starting ntp client.",
|
||||
"created_at": 22,
|
||||
"channels": []
|
||||
}, {
|
||||
"meta": {
|
||||
"z": 0,
|
||||
"y": 0,
|
||||
"x": 0,
|
||||
"type": "success"
|
||||
},
|
||||
"message": "FIXME connection test complete",
|
||||
"created_at": 22,
|
||||
"channels": []
|
||||
}, {
|
||||
"meta": {
|
||||
"z": 0,
|
||||
"y": 0,
|
||||
"x": 0,
|
||||
"type": "busy"
|
||||
},
|
||||
"message": "FIXME doing connection test...",
|
||||
"created_at": 21,
|
||||
"channels": []
|
||||
}, {
|
||||
"meta": {
|
||||
"z": 0,
|
||||
"y": 0,
|
||||
"x": 0,
|
||||
"type": "warn"
|
||||
},
|
||||
"message": "No serial handler yet, waiting...",
|
||||
"created_at": 18,
|
||||
"channels": []
|
||||
}, {
|
||||
"meta": {
|
||||
"z": 0,
|
||||
"y": 0,
|
||||
"x": 0,
|
||||
"type": "info"
|
||||
},
|
||||
"message": "FIXME is waiting for linux and network and what not.",
|
||||
"created_at": 16,
|
||||
"channels": []
|
||||
}, {
|
||||
"meta": {
|
||||
"z": 0,
|
||||
"y": 0,
|
||||
"x": 0,
|
||||
"type": "info"
|
||||
},
|
||||
"message": "FIXME Configurator init!",
|
||||
"created_at": 15,
|
||||
"channels": []
|
||||
}]
|
|
@ -1,5 +1,4 @@
|
|||
require 'spec_helper'
|
||||
JSON_EXAMPLE = File.read("spec/controllers/api/logs/connor_fixture.json")
|
||||
|
||||
describe Api::LogsController do
|
||||
include Devise::Test::ControllerHelpers
|
||||
|
@ -81,76 +80,13 @@ describe Api::LogsController do
|
|||
expect(Log.count).to eq(0)
|
||||
end
|
||||
|
||||
it 'creates many logs (with an Array)' do
|
||||
sign_in user
|
||||
before_count = Log.count
|
||||
run_jobs_now do
|
||||
post :create,
|
||||
body: [
|
||||
{ meta: { x: 1, y: 2, z: 3, type: "info" },
|
||||
channels: ["toast"],
|
||||
message: "one" },
|
||||
{ meta: { x: 1, y: 2, z: 3, type: "info" },
|
||||
channels: ["toast"],
|
||||
message: "two" },
|
||||
{ meta: { x: 1, y: 2, z: 3, type: "info" },
|
||||
channels: ["toast"],
|
||||
message: "three" },
|
||||
].to_json,
|
||||
params: {format: :json}
|
||||
end
|
||||
expect(response.status).to eq(200)
|
||||
expect(before_count + 3).to eq(Log.count)
|
||||
end
|
||||
|
||||
it 'does not bother saving `fun` or `debug` logs' do
|
||||
sign_in user
|
||||
Log.destroy_all
|
||||
LogDispatch.destroy_all
|
||||
before_count = Log.count
|
||||
dispatch_before = LogDispatch.count
|
||||
run_jobs_now do
|
||||
post :create,
|
||||
body: [
|
||||
{ meta: { x: 1, y: 2, z: 3, type: "info" },
|
||||
channels: ["toast"],
|
||||
message: "one" },
|
||||
{ meta: { x: 1, y: 2, z: 3, type: "fun" }, # Ignored
|
||||
channels: [],
|
||||
message: "two" },
|
||||
{ meta: { x: 1, y: 2, z: 3, type: "debug" }, # Ignored
|
||||
channels: [],
|
||||
message: "two" },
|
||||
{ meta: { x: 1, y: 2, z: 3, type: "info" },
|
||||
channels: ["email"],
|
||||
message: "three" },
|
||||
].to_json,
|
||||
params: {format: :json}
|
||||
expect(response.status).to eq(200)
|
||||
expect(Log.count).to eq(before_count + 2)
|
||||
expect(LogDispatch.count).to eq(dispatch_before + 1)
|
||||
end
|
||||
end
|
||||
|
||||
it 'Runs compaction when the logs pile up' do
|
||||
payl = []
|
||||
100.times do
|
||||
payl.push({ meta: { x: 1,
|
||||
y: 2,
|
||||
z: 3,
|
||||
type: "info"
|
||||
},
|
||||
channels: ["toast"],
|
||||
message: "one" })
|
||||
end
|
||||
LogDispatch.destroy_all
|
||||
Log.destroy_all
|
||||
100.times { Log.create!(device: user.device) }
|
||||
sign_in user
|
||||
user.device.update_attributes!(max_log_count: 15)
|
||||
LogDispatch.destroy_all
|
||||
Log.destroy_all
|
||||
before_count = Log.count
|
||||
run_jobs_now do
|
||||
post :create, body: payl.to_json, params: {format: :json}
|
||||
end
|
||||
get :index, params: {format: :json}
|
||||
expect(response.status).to eq(200)
|
||||
expect(json.length).to eq(user.device.max_log_count)
|
||||
end
|
||||
|
@ -198,23 +134,10 @@ describe Api::LogsController do
|
|||
expect(last_email.to).to include(user.email)
|
||||
end
|
||||
end
|
||||
|
||||
it "handles bug that Connor reported" do
|
||||
sign_in user
|
||||
empty_mail_bag
|
||||
Log.destroy_all
|
||||
LogDispatch.destroy_all
|
||||
run_jobs_now do
|
||||
post :create,
|
||||
body: JSON_EXAMPLE,
|
||||
params: {format: :json}
|
||||
expect(last_email).to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#search" do
|
||||
examples = [
|
||||
EXAMPLES = [
|
||||
[1, "success"],
|
||||
[1, "busy"],
|
||||
[1, "warn"],
|
||||
|
@ -244,7 +167,7 @@ describe Api::LogsController do
|
|||
sign_in user
|
||||
Log.destroy_all
|
||||
conf = user.device.web_app_config
|
||||
examples.map do |(verbosity, type)|
|
||||
EXAMPLES.map do |(verbosity, type)|
|
||||
FactoryBot.create(:log, device: user.device,
|
||||
verbosity: verbosity,
|
||||
type: type)
|
||||
|
@ -258,14 +181,18 @@ describe Api::LogsController do
|
|||
debug_log: 3)
|
||||
get :search
|
||||
expect(response.status).to eq(200)
|
||||
expect(json.length).to eq(examples.length)
|
||||
expect(json.length).to eq(EXAMPLES.length)
|
||||
end
|
||||
|
||||
it 'sends emails'
|
||||
|
||||
it 'sends fatal_emails'
|
||||
|
||||
it 'filters NO logs based on log filtering settings in `WebAppConfig` ' do
|
||||
sign_in user
|
||||
Log.destroy_all
|
||||
conf = user.device.web_app_config
|
||||
examples.map do |(verbosity, type)|
|
||||
EXAMPLES.map do |(verbosity, type)|
|
||||
FactoryBot.create(:log, device: user.device,
|
||||
verbosity: verbosity,
|
||||
type: type)
|
||||
|
|
|
@ -136,7 +136,7 @@ describe Api::PointsController do
|
|||
|
||||
it "marks device as seen when they download points" do
|
||||
old_last_saw_api = user.device.last_saw_api
|
||||
ua = "FarmbotOS/5.0.2 (host) host ()"
|
||||
ua = "FarmbotOS/6.0.2 (host) host ()"
|
||||
allow(request).to receive(:user_agent).and_return(ua)
|
||||
request.env["HTTP_USER_AGENT"] = ua
|
||||
request.headers["Authorization"] = "bearer #{auth_token}"
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe(SequenceUsageReport) do
|
||||
it "is readonly" do
|
||||
expect(SequenceUsageReport.new.readonly?).to be(true)
|
||||
end
|
||||
end
|
|
@ -15,9 +15,9 @@ sudo docker run hello-world # Should run!
|
|||
command curl -sSL https://rvm.io/mpapis.asc | gpg --import -
|
||||
curl -sSL https://get.rvm.io | bash
|
||||
source /usr/local/rvm/scripts/rvm
|
||||
rvm install "ruby-2.5.0"
|
||||
rvm install "ruby-2.5.1"
|
||||
cd .
|
||||
rvm --default use 2.5.0
|
||||
rvm --default use 2.5.1
|
||||
# LOG OUT AND LOG BACK IN NOW.
|
||||
|
||||
# Image Magick
|
||||
|
|
|
@ -210,7 +210,8 @@ export function fakeWebAppConfig(): TaggedWebAppConfig {
|
|||
enable_browser_speak: false,
|
||||
photo_filter_begin: "2018-01-11T20:20:38.362Z",
|
||||
photo_filter_end: "2018-01-22T15:32:41.970Z",
|
||||
discard_unsaved: false
|
||||
discard_unsaved: false,
|
||||
xy_swap: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { MapTransformProps } from "../farm_designer/map/interfaces";
|
||||
|
||||
export const fakeMapTransformProps = (): MapTransformProps => {
|
||||
return {
|
||||
quadrant: 2,
|
||||
gridSize: { x: 3000, y: 1500 },
|
||||
xySwap: false,
|
||||
};
|
||||
};
|
|
@ -17,21 +17,22 @@ import { mount } from "enzyme";
|
|||
import { bot } from "../__test_support__/fake_state/bot";
|
||||
import { fakeUser } from "../__test_support__/fake_state/resources";
|
||||
|
||||
describe("<App />: Controls Pop-Up", () => {
|
||||
function fakeProps(): AppProps {
|
||||
return {
|
||||
timeOffset: 0, // Default to UTC
|
||||
dispatch: jest.fn(),
|
||||
loaded: [],
|
||||
logs: [],
|
||||
user: fakeUser(),
|
||||
bot: bot,
|
||||
consistent: true,
|
||||
axisInversion: { x: false, y: false, z: false },
|
||||
firmwareConfig: undefined,
|
||||
};
|
||||
}
|
||||
const fakeProps = (): AppProps => {
|
||||
return {
|
||||
timeOffset: 0, // Default to UTC
|
||||
dispatch: jest.fn(),
|
||||
loaded: [],
|
||||
logs: [],
|
||||
user: fakeUser(),
|
||||
bot: bot,
|
||||
consistent: true,
|
||||
axisInversion: { x: false, y: false, z: false },
|
||||
firmwareConfig: undefined,
|
||||
xySwap: false,
|
||||
};
|
||||
};
|
||||
|
||||
describe("<App />: Controls Pop-Up", () => {
|
||||
function controlsPopUp(page: string, exists: boolean) {
|
||||
it(`doesn't render controls pop-up on ${page} page`, () => {
|
||||
mockPath = "/app/" + page;
|
||||
|
@ -58,21 +59,6 @@ describe("<App />: Controls Pop-Up", () => {
|
|||
});
|
||||
|
||||
describe("<App />: Loading", () => {
|
||||
function fakeProps(): AppProps {
|
||||
const p: AppProps = {
|
||||
dispatch: jest.fn(),
|
||||
loaded: [],
|
||||
logs: [],
|
||||
user: fakeUser(),
|
||||
bot: bot,
|
||||
consistent: true,
|
||||
timeOffset: 0,
|
||||
axisInversion: { x: false, y: false, z: false },
|
||||
firmwareConfig: undefined,
|
||||
};
|
||||
return p;
|
||||
}
|
||||
|
||||
it("MUST_LOADs not loaded", () => {
|
||||
const wrapper = mount(<App {...fakeProps()} />);
|
||||
expect(wrapper.text()).toContain("Loading...");
|
||||
|
@ -94,20 +80,6 @@ describe("<App />: Loading", () => {
|
|||
});
|
||||
|
||||
describe("<App />: NavBar", () => {
|
||||
function fakeProps(): AppProps {
|
||||
return {
|
||||
dispatch: jest.fn(),
|
||||
loaded: [],
|
||||
logs: [],
|
||||
user: fakeUser(),
|
||||
bot: bot,
|
||||
consistent: true,
|
||||
timeOffset: 0,
|
||||
axisInversion: { x: false, y: false, z: false },
|
||||
firmwareConfig: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
it("displays links", () => {
|
||||
const wrapper = mount(<App {...fakeProps()} />);
|
||||
expect(wrapper.text())
|
||||
|
|
|
@ -10,17 +10,28 @@ import * as React from "react";
|
|||
import { ControlsPopup } from "../controls_popup";
|
||||
import { mount } from "enzyme";
|
||||
import { bot } from "../__test_support__/fake_state/bot";
|
||||
import { ControlsPopupProps } from "../controls/interfaces";
|
||||
|
||||
describe("<ControlsPopup />", () => {
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const wrapper = mount(<ControlsPopup
|
||||
dispatch={jest.fn()}
|
||||
axisInversion={{ x: true, y: false, z: false }}
|
||||
botPosition={{ x: undefined, y: undefined, z: undefined }}
|
||||
mcuParams={bot.hardware.mcu_params} />);
|
||||
const fakeProps = (): ControlsPopupProps => {
|
||||
return {
|
||||
dispatch: jest.fn(),
|
||||
axisInversion: { x: false, y: false, z: false },
|
||||
botPosition: { x: undefined, y: undefined, z: undefined },
|
||||
firmwareSettings: bot.hardware.mcu_params,
|
||||
xySwap: false,
|
||||
arduinoBusy: false,
|
||||
stepSize: 100,
|
||||
};
|
||||
};
|
||||
|
||||
const p = fakeProps();
|
||||
p.axisInversion.x = true;
|
||||
const wrapper = mount(<ControlsPopup {...p} />);
|
||||
|
||||
it("Has a false initial state", () => {
|
||||
expect(wrapper.state("isOpen")).toBeFalsy();
|
||||
|
@ -53,4 +64,17 @@ describe("<ControlsPopup />", () => {
|
|||
[0, 1, 2, 3].map((i) => wrapper.find("button").at(i).simulate("click"));
|
||||
expect(mockDevice.moveRelative).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("swaps axes", () => {
|
||||
const swappedProps = fakeProps();
|
||||
swappedProps.xySwap = true;
|
||||
const swapped = mount(<ControlsPopup {...swappedProps} />);
|
||||
swapped.setState({ isOpen: true });
|
||||
expect(swapped.state("isOpen")).toBeTruthy();
|
||||
const button = swapped.find("button").at(1);
|
||||
expect(button.props().title).toBe("move x axis (100)");
|
||||
button.simulate("click");
|
||||
expect(mockDevice.moveRelative)
|
||||
.toHaveBeenCalledWith({ speed: 100, x: 100, y: 0, z: 0 });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -23,6 +23,7 @@ import { Session } from "./session";
|
|||
import { BooleanSetting } from "./session_keys";
|
||||
import { getPathArray } from "./history";
|
||||
import { FirmwareConfig } from "./config_storage/firmware_configs";
|
||||
import { getWebAppConfigValue } from "./config_storage/actions";
|
||||
|
||||
/** Remove 300ms delay on touch devices - https://github.com/ftlabs/fastclick */
|
||||
const fastClick = require("fastclick");
|
||||
|
@ -40,6 +41,7 @@ export interface AppProps {
|
|||
consistent: boolean;
|
||||
timeOffset: number;
|
||||
axisInversion: Record<Xyz, boolean>;
|
||||
xySwap: boolean;
|
||||
firmwareConfig: FirmwareConfig | undefined;
|
||||
}
|
||||
|
||||
|
@ -62,6 +64,7 @@ function mapStateToProps(props: Everything): AppProps {
|
|||
y: !!Session.deprecatedGetBool(BooleanSetting.y_axis_inverted),
|
||||
z: !!Session.deprecatedGetBool(BooleanSetting.z_axis_inverted),
|
||||
},
|
||||
xySwap: !!getWebAppConfigValue(() => props)(BooleanSetting.xy_swap),
|
||||
firmwareConfig: validFwConfig(getFirmwareConfig(props.resources.index))
|
||||
};
|
||||
}
|
||||
|
@ -118,7 +121,10 @@ export class App extends React.Component<AppProps, {}> {
|
|||
dispatch={this.props.dispatch}
|
||||
axisInversion={this.props.axisInversion}
|
||||
botPosition={validBotLocationData(location_data).position}
|
||||
mcuParams={this.props.firmwareConfig || mcu_params} />}
|
||||
firmwareSettings={this.props.firmwareConfig || mcu_params}
|
||||
xySwap={this.props.xySwap}
|
||||
arduinoBusy={!!this.props.bot.hardware.informational_settings.busy}
|
||||
stepSize={this.props.bot.stepSize} />}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ export interface WebAppConfig {
|
|||
photo_filter_begin: string;
|
||||
photo_filter_end: string;
|
||||
discard_unsaved: boolean;
|
||||
xy_swap: boolean;
|
||||
}
|
||||
|
||||
export type NumberConfigKey = "id"
|
||||
|
@ -85,4 +86,5 @@ export type BooleanConfigKey = "confirm_step_deletion"
|
|||
|"show_first_party_farmware"
|
||||
|"enable_browser_speak"
|
||||
|"show_images"
|
||||
|"discard_unsaved";
|
||||
|"discard_unsaved"
|
||||
|"xy_swap";
|
||||
|
|
|
@ -38,6 +38,7 @@ describe("<Controls />", () => {
|
|||
botToMqttStatus: "up",
|
||||
firmwareSettings: bot.hardware.mcu_params,
|
||||
shouldDisplay: () => true,
|
||||
xySwap: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const mockDevice = {
|
||||
home: jest.fn(() => { return Promise.resolve(); }),
|
||||
takePhoto: jest.fn(() => { return Promise.resolve(); }),
|
||||
moveRelative: jest.fn(() => { return Promise.resolve(); }),
|
||||
};
|
||||
|
||||
jest.mock("../../device", () => ({
|
||||
|
@ -18,40 +19,64 @@ import { bot } from "../../__test_support__/fake_state/bot";
|
|||
describe("<JogButtons/>", function () {
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks();
|
||||
jogButtonProps.disabled = false;
|
||||
});
|
||||
const jogButtonProps: JogMovementControlsProps = {
|
||||
bot: bot,
|
||||
x_axis_inverted: false,
|
||||
y_axis_inverted: false,
|
||||
z_axis_inverted: false,
|
||||
disabled: false,
|
||||
firmwareSettings: bot.hardware.mcu_params
|
||||
|
||||
const jogButtonProps = (): JogMovementControlsProps => {
|
||||
return {
|
||||
stepSize: 100,
|
||||
botPosition: { x: undefined, y: undefined, z: undefined },
|
||||
axisInversion: { x: false, y: false, z: false },
|
||||
arduinoBusy: false,
|
||||
firmwareSettings: bot.hardware.mcu_params,
|
||||
xySwap: false,
|
||||
};
|
||||
};
|
||||
|
||||
it("calls home command", () => {
|
||||
const jogButtons = mount(<JogButtons {...jogButtonProps} />);
|
||||
const jogButtons = mount(<JogButtons {...jogButtonProps()} />);
|
||||
jogButtons.find("button").at(3).simulate("click");
|
||||
expect(mockDevice.home).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("is disabled", () => {
|
||||
jogButtonProps.disabled = true;
|
||||
const jogButtons = mount(<JogButtons {...jogButtonProps} />);
|
||||
const p = jogButtonProps();
|
||||
p.arduinoBusy = true;
|
||||
const jogButtons = mount(<JogButtons {...p} />);
|
||||
jogButtons.find("button").at(3).simulate("click");
|
||||
expect(mockDevice.home).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("call has correct args", () => {
|
||||
const jogButtons = mount(<JogButtons {...jogButtonProps} />);
|
||||
const jogButtons = mount(<JogButtons {...jogButtonProps()} />);
|
||||
jogButtons.find("button").at(3).simulate("click");
|
||||
expect(mockDevice.home)
|
||||
.toHaveBeenCalledWith({ axis: "all", speed: 100 });
|
||||
});
|
||||
|
||||
it("takes photo", () => {
|
||||
const jogButtons = mount(<JogButtons {...jogButtonProps} />);
|
||||
const jogButtons = mount(<JogButtons {...jogButtonProps()} />);
|
||||
jogButtons.find("button").at(0).simulate("click");
|
||||
expect(mockDevice.takePhoto).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("has unswapped xy jog buttons", () => {
|
||||
const jogButtons = mount(<JogButtons {...jogButtonProps()} />);
|
||||
const button = jogButtons.find("button").at(6);
|
||||
expect(button.props().title).toBe("move x axis (100)");
|
||||
button.simulate("click");
|
||||
expect(mockDevice.moveRelative)
|
||||
.toHaveBeenCalledWith({ speed: 100, x: 100, y: 0, z: 0 });
|
||||
});
|
||||
|
||||
it("has swapped xy jog buttons", () => {
|
||||
const p = jogButtonProps();
|
||||
(p.stepSize as number | undefined) = undefined;
|
||||
p.xySwap = true;
|
||||
const jogButtons = mount(<JogButtons {...p} />);
|
||||
const button = jogButtons.find("button").at(6);
|
||||
expect(button.props().title).toBe("move y axis (100)");
|
||||
button.simulate("click");
|
||||
expect(mockDevice.moveRelative)
|
||||
.toHaveBeenCalledWith({ speed: 100, x: 0, y: 100, z: 0 });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,12 +7,19 @@ jest.mock("../../session", () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock("../../config_storage/actions", () => {
|
||||
return {
|
||||
toggleWebAppBool: jest.fn()
|
||||
};
|
||||
});
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { Move } from "../move";
|
||||
import { bot } from "../../__test_support__/fake_state/bot";
|
||||
import { MoveProps } from "../interfaces";
|
||||
import { Session } from "../../session";
|
||||
import { toggleWebAppBool } from "../../config_storage/actions";
|
||||
|
||||
describe("<Move />", () => {
|
||||
beforeEach(function () {
|
||||
|
@ -24,7 +31,7 @@ describe("<Move />", () => {
|
|||
dispatch: jest.fn(),
|
||||
bot: bot,
|
||||
user: undefined,
|
||||
disabled: false,
|
||||
arduinoBusy: false,
|
||||
raw_encoders: false,
|
||||
scaled_encoders: false,
|
||||
x_axis_inverted: false,
|
||||
|
@ -32,6 +39,7 @@ describe("<Move />", () => {
|
|||
z_axis_inverted: false,
|
||||
botToMqttStatus: "up",
|
||||
firmwareSettings: bot.hardware.mcu_params,
|
||||
xySwap: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -62,8 +70,7 @@ describe("<Move />", () => {
|
|||
});
|
||||
|
||||
it("toggle: invert jog button", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<Move {...p} />);
|
||||
const wrapper = mount(<Move {...fakeProps()} />);
|
||||
// tslint:disable-next-line:no-any
|
||||
const instance = wrapper.instance() as any;
|
||||
instance.toggle("x")();
|
||||
|
@ -71,11 +78,18 @@ describe("<Move />", () => {
|
|||
});
|
||||
|
||||
it("toggle: encoder data display", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = mount(<Move {...p} />);
|
||||
const wrapper = mount(<Move {...fakeProps()} />);
|
||||
// tslint:disable-next-line:no-any
|
||||
const instance = wrapper.instance() as any;
|
||||
instance.toggle_encoder_data("raw_encoders")();
|
||||
expect(Session.invertBool).toHaveBeenCalledWith("raw_encoders");
|
||||
});
|
||||
|
||||
it("toggle: xy swap", () => {
|
||||
const wrapper = mount(<Move {...fakeProps()} />);
|
||||
// tslint:disable-next-line:no-any
|
||||
const instance = wrapper.instance() as any;
|
||||
instance.toggle_xy_swap();
|
||||
expect(toggleWebAppBool).toHaveBeenCalledWith("xy_swap");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,7 +24,7 @@ export class Controls extends React.Component<Props, {}> {
|
|||
bot: this.props.bot,
|
||||
user: this.props.user,
|
||||
dispatch: this.props.dispatch,
|
||||
disabled: arduinoBusy,
|
||||
arduinoBusy,
|
||||
raw_encoders: !!Session.deprecatedGetBool(BooleanSetting.raw_encoders),
|
||||
scaled_encoders: !!Session.deprecatedGetBool(BooleanSetting.scaled_encoders),
|
||||
x_axis_inverted: !!Session.deprecatedGetBool(BooleanSetting.x_axis_inverted),
|
||||
|
@ -32,6 +32,7 @@ export class Controls extends React.Component<Props, {}> {
|
|||
z_axis_inverted: !!Session.deprecatedGetBool(BooleanSetting.z_axis_inverted),
|
||||
botToMqttStatus: this.props.botToMqttStatus,
|
||||
firmwareSettings: this.props.firmwareSettings,
|
||||
xySwap: this.props.xySwap,
|
||||
};
|
||||
const showWebcamWidget = !Session.deprecatedGetBool(BooleanSetting.hide_webcam_widget);
|
||||
return <Page className="controls">
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { validBotLocationData } from "../util";
|
||||
import { JogMovementControlsProps } from "./interfaces";
|
||||
import { DirectionAxesProps } from "./interfaces";
|
||||
import { McuParams } from "farmbot";
|
||||
|
||||
const _ = (nr_steps: number | undefined, steps_mm: number | undefined) => {
|
||||
return (nr_steps || 0) / (steps_mm || 1);
|
||||
};
|
||||
|
||||
function calculateAxialLengths(props: JogMovementControlsProps) {
|
||||
function calculateAxialLengths(props: { firmwareSettings: McuParams }) {
|
||||
const fwParams = props.firmwareSettings;
|
||||
|
||||
return {
|
||||
|
@ -15,35 +15,33 @@ function calculateAxialLengths(props: JogMovementControlsProps) {
|
|||
};
|
||||
}
|
||||
|
||||
export function buildDirectionProps(props: JogMovementControlsProps) {
|
||||
const { firmwareSettings } = props;
|
||||
const { location_data } = props.bot.hardware;
|
||||
const botLocationData = validBotLocationData(location_data);
|
||||
export function buildDirectionProps(props: DirectionAxesProps) {
|
||||
const { firmwareSettings, botPosition } = props;
|
||||
const lengths = calculateAxialLengths(props);
|
||||
return {
|
||||
x: {
|
||||
isInverted: props.x_axis_inverted,
|
||||
isInverted: props.axisInversion.x,
|
||||
stopAtHome: !!firmwareSettings.movement_stop_at_home_x,
|
||||
stopAtMax: !!firmwareSettings.movement_stop_at_max_x,
|
||||
axisLength: lengths.x,
|
||||
negativeOnly: !!firmwareSettings.movement_home_up_x,
|
||||
position: botLocationData.position.x
|
||||
position: botPosition.x
|
||||
},
|
||||
y: {
|
||||
isInverted: props.y_axis_inverted,
|
||||
isInverted: props.axisInversion.y,
|
||||
stopAtHome: !!firmwareSettings.movement_stop_at_home_y,
|
||||
stopAtMax: !!firmwareSettings.movement_stop_at_max_y,
|
||||
axisLength: lengths.y,
|
||||
negativeOnly: !!firmwareSettings.movement_home_up_y,
|
||||
position: botLocationData.position.y
|
||||
position: botPosition.y
|
||||
},
|
||||
z: {
|
||||
isInverted: props.z_axis_inverted,
|
||||
isInverted: props.axisInversion.z,
|
||||
stopAtHome: !!firmwareSettings.movement_stop_at_home_z,
|
||||
stopAtMax: !!firmwareSettings.movement_stop_at_max_z,
|
||||
axisLength: lengths.z,
|
||||
negativeOnly: !!firmwareSettings.movement_home_up_z,
|
||||
position: botLocationData.position.z
|
||||
position: botPosition.z
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -18,13 +18,14 @@ export interface Props {
|
|||
botToMqttStatus: NetworkState;
|
||||
firmwareSettings: McuParams;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
xySwap: boolean;
|
||||
}
|
||||
|
||||
export interface MoveProps {
|
||||
dispatch: Function;
|
||||
bot: BotState;
|
||||
user: TaggedUser | undefined;
|
||||
disabled: boolean | undefined;
|
||||
arduinoBusy: boolean;
|
||||
raw_encoders: boolean;
|
||||
scaled_encoders: boolean;
|
||||
x_axis_inverted: boolean;
|
||||
|
@ -32,6 +33,7 @@ export interface MoveProps {
|
|||
z_axis_inverted: boolean;
|
||||
botToMqttStatus: NetworkState;
|
||||
firmwareSettings: McuParams;
|
||||
xySwap: boolean;
|
||||
}
|
||||
|
||||
export interface DirectionButtonProps {
|
||||
|
@ -93,13 +95,20 @@ export interface StepSizeSelectorProps {
|
|||
selector: (num: number) => void;
|
||||
}
|
||||
|
||||
export interface JogMovementControlsProps {
|
||||
x_axis_inverted: boolean;
|
||||
y_axis_inverted: boolean;
|
||||
z_axis_inverted: boolean;
|
||||
bot: BotState;
|
||||
export interface DirectionAxesProps {
|
||||
axisInversion: Record<Xyz, boolean>;
|
||||
botPosition: BotPosition;
|
||||
firmwareSettings: McuParams;
|
||||
disabled: boolean | undefined;
|
||||
}
|
||||
|
||||
export interface JogMovementControlsProps extends DirectionAxesProps {
|
||||
stepSize: number;
|
||||
arduinoBusy: boolean;
|
||||
xySwap: boolean;
|
||||
}
|
||||
|
||||
export interface ControlsPopupProps extends JogMovementControlsProps {
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
export interface ToggleButtonProps {
|
||||
|
|
|
@ -5,9 +5,15 @@ import { JogMovementControlsProps } from "./interfaces";
|
|||
import { getDevice } from "../device";
|
||||
import { buildDirectionProps } from "./direction_axes_props";
|
||||
|
||||
const DEFAULT_STEP_SIZE = 100;
|
||||
|
||||
export function JogButtons(props: JogMovementControlsProps) {
|
||||
const { stepSize, xySwap, arduinoBusy } = props;
|
||||
const directionAxesProps = buildDirectionProps(props);
|
||||
return <table className="jog-table" style={{ border: 0 }}>
|
||||
const rightLeft = xySwap ? "y" : "x";
|
||||
const upDown = xySwap ? "x" : "y";
|
||||
|
||||
return <table className="jog-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
|
@ -19,11 +25,11 @@ export function JogButtons(props: JogMovementControlsProps) {
|
|||
<td />
|
||||
<td>
|
||||
<DirectionButton
|
||||
axis="y"
|
||||
axis={upDown}
|
||||
direction="up"
|
||||
directionAxisProps={directionAxesProps.y}
|
||||
steps={props.bot.stepSize || 1000}
|
||||
disabled={props.disabled} />
|
||||
directionAxisProps={directionAxesProps[upDown]}
|
||||
steps={stepSize || DEFAULT_STEP_SIZE}
|
||||
disabled={arduinoBusy} />
|
||||
</td>
|
||||
<td />
|
||||
<td />
|
||||
|
@ -32,8 +38,8 @@ export function JogButtons(props: JogMovementControlsProps) {
|
|||
axis="z"
|
||||
direction="up"
|
||||
directionAxisProps={directionAxesProps.z}
|
||||
steps={props.bot.stepSize || 1000}
|
||||
disabled={props.disabled} />
|
||||
steps={stepSize || DEFAULT_STEP_SIZE}
|
||||
disabled={arduinoBusy} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -41,32 +47,32 @@ export function JogButtons(props: JogMovementControlsProps) {
|
|||
<button
|
||||
className="i fa fa-home arrow-button fb-button"
|
||||
onClick={() => homeAll(100)}
|
||||
disabled={props.disabled || false} />
|
||||
disabled={arduinoBusy || false} />
|
||||
</td>
|
||||
<td />
|
||||
<td>
|
||||
<DirectionButton
|
||||
axis="x"
|
||||
axis={rightLeft}
|
||||
direction="left"
|
||||
directionAxisProps={directionAxesProps.x}
|
||||
steps={props.bot.stepSize || 1000}
|
||||
disabled={props.disabled} />
|
||||
directionAxisProps={directionAxesProps[rightLeft]}
|
||||
steps={stepSize || DEFAULT_STEP_SIZE}
|
||||
disabled={arduinoBusy} />
|
||||
</td>
|
||||
<td>
|
||||
<DirectionButton
|
||||
axis="y"
|
||||
axis={upDown}
|
||||
direction="down"
|
||||
directionAxisProps={directionAxesProps.y}
|
||||
steps={props.bot.stepSize || 1000}
|
||||
disabled={props.disabled} />
|
||||
directionAxisProps={directionAxesProps[upDown]}
|
||||
steps={stepSize || DEFAULT_STEP_SIZE}
|
||||
disabled={arduinoBusy} />
|
||||
</td>
|
||||
<td>
|
||||
<DirectionButton
|
||||
axis="x"
|
||||
axis={rightLeft}
|
||||
direction="right"
|
||||
directionAxisProps={directionAxesProps.x}
|
||||
steps={props.bot.stepSize || 1000}
|
||||
disabled={props.disabled} />
|
||||
directionAxisProps={directionAxesProps[rightLeft]}
|
||||
steps={stepSize || DEFAULT_STEP_SIZE}
|
||||
disabled={arduinoBusy} />
|
||||
</td>
|
||||
<td />
|
||||
<td>
|
||||
|
@ -74,8 +80,8 @@ export function JogButtons(props: JogMovementControlsProps) {
|
|||
axis="z"
|
||||
direction="down"
|
||||
directionAxisProps={directionAxesProps.z}
|
||||
steps={props.bot.stepSize || 1000}
|
||||
disabled={props.disabled} />
|
||||
steps={stepSize || DEFAULT_STEP_SIZE}
|
||||
disabled={arduinoBusy} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
|
@ -15,6 +15,8 @@ import { AxisDisplayGroup } from "./axis_display_group";
|
|||
import { Session } from "../session";
|
||||
import { INVERSION_MAPPING, ENCODER_MAPPING } from "../devices/reducer";
|
||||
import { minFwVersionCheck, validBotLocationData } from "../util";
|
||||
import { toggleWebAppBool } from "../config_storage/actions";
|
||||
import { BooleanSetting } from "../session_keys";
|
||||
|
||||
export class Move extends React.Component<MoveProps, {}> {
|
||||
|
||||
|
@ -25,11 +27,14 @@ export class Move extends React.Component<MoveProps, {}> {
|
|||
toggle_encoder_data =
|
||||
(name: EncoderDisplay) => () => Session.invertBool(ENCODER_MAPPING[name]);
|
||||
|
||||
toggle_xy_swap = () =>
|
||||
this.props.dispatch(toggleWebAppBool(BooleanSetting.xy_swap));
|
||||
|
||||
render() {
|
||||
const { location_data, informational_settings } = this.props.bot.hardware;
|
||||
const { firmware_version } = informational_settings;
|
||||
const { x_axis_inverted, y_axis_inverted, z_axis_inverted,
|
||||
raw_encoders, scaled_encoders } = this.props;
|
||||
raw_encoders, scaled_encoders, xySwap } = this.props;
|
||||
|
||||
const btnColor = (flag: boolean) => { return flag ? "green" : "red"; };
|
||||
const xBtnColor = btnColor(x_axis_inverted);
|
||||
|
@ -37,6 +42,7 @@ export class Move extends React.Component<MoveProps, {}> {
|
|||
const zBtnColor = btnColor(z_axis_inverted);
|
||||
const rawBtnColor = btnColor(raw_encoders);
|
||||
const scaledBtnColor = btnColor(scaled_encoders);
|
||||
const xySwapBtnColor = btnColor(xySwap);
|
||||
|
||||
const locationData = validBotLocationData(location_data);
|
||||
const motor_coordinates = locationData.position;
|
||||
|
@ -54,10 +60,10 @@ export class Move extends React.Component<MoveProps, {}> {
|
|||
helpText={ToolTips.MOVE}>
|
||||
<Popover position={Position.BOTTOM_RIGHT}>
|
||||
<i className="fa fa-gear" />
|
||||
<div>
|
||||
<label>
|
||||
<div className="move-settings-menu">
|
||||
<p>
|
||||
{t("Invert Jog Buttons")}
|
||||
</label>
|
||||
</p>
|
||||
<fieldset>
|
||||
<label>
|
||||
{t("X Axis")}
|
||||
|
@ -82,9 +88,10 @@ export class Move extends React.Component<MoveProps, {}> {
|
|||
className={"fb-button fb-toggle-button " + zBtnColor}
|
||||
onClick={this.toggle("z")} />
|
||||
</fieldset>
|
||||
<label>
|
||||
|
||||
<p>
|
||||
{t("Display Encoder Data")}
|
||||
</label>
|
||||
</p>
|
||||
<fieldset>
|
||||
<label>
|
||||
{t("Scaled encoder position")}
|
||||
|
@ -101,6 +108,18 @@ export class Move extends React.Component<MoveProps, {}> {
|
|||
className={"fb-button fb-toggle-button " + rawBtnColor}
|
||||
onClick={this.toggle_encoder_data("raw_encoders")} />
|
||||
</fieldset>
|
||||
|
||||
<p>
|
||||
{t("Swap jog buttons")}
|
||||
</p>
|
||||
<fieldset>
|
||||
<label>
|
||||
{t("x and y axis")}
|
||||
</label>
|
||||
<button
|
||||
className={"fb-button fb-toggle-button " + xySwapBtnColor}
|
||||
onClick={this.toggle_xy_swap} />
|
||||
</fieldset>
|
||||
</div>
|
||||
</Popover>
|
||||
<EStopButton
|
||||
|
@ -120,12 +139,16 @@ export class Move extends React.Component<MoveProps, {}> {
|
|||
selector={num => this.props.dispatch(changeStepSize(num))}
|
||||
selected={this.props.bot.stepSize} />
|
||||
<JogButtons
|
||||
bot={this.props.bot}
|
||||
x_axis_inverted={x_axis_inverted}
|
||||
y_axis_inverted={y_axis_inverted}
|
||||
z_axis_inverted={z_axis_inverted}
|
||||
disabled={this.props.disabled}
|
||||
firmwareSettings={this.props.firmwareSettings} />
|
||||
stepSize={this.props.bot.stepSize}
|
||||
botPosition={locationData.position}
|
||||
axisInversion={{
|
||||
x: x_axis_inverted,
|
||||
y: y_axis_inverted,
|
||||
z: z_axis_inverted
|
||||
}}
|
||||
arduinoBusy={this.props.arduinoBusy}
|
||||
firmwareSettings={this.props.firmwareSettings}
|
||||
xySwap={this.props.xySwap} />
|
||||
<Row>
|
||||
<Col xs={3}>
|
||||
<label>{t("X AXIS")}</label>
|
||||
|
@ -151,7 +174,7 @@ export class Move extends React.Component<MoveProps, {}> {
|
|||
<AxisInputBoxGroup
|
||||
position={motor_coordinates}
|
||||
onCommit={input => moveAbs(input)}
|
||||
disabled={this.props.disabled} />
|
||||
disabled={this.props.arduinoBusy} />
|
||||
</MustBeOnline>
|
||||
</WidgetBody>
|
||||
</Widget>;
|
||||
|
|
|
@ -12,6 +12,8 @@ import * as _ from "lodash";
|
|||
import {
|
||||
validFwConfig, shouldDisplay, determineInstalledOsVersion
|
||||
} from "../util";
|
||||
import { BooleanSetting } from "../session_keys";
|
||||
import { getWebAppConfigValue } from "../config_storage/actions";
|
||||
|
||||
export function mapStateToProps(props: Everything): Props {
|
||||
const peripherals = _.uniq(selectAllPeripherals(props.resources.index));
|
||||
|
@ -23,6 +25,7 @@ export function mapStateToProps(props: Everything): Props {
|
|||
const { mcu_params } = props.bot.hardware;
|
||||
const installedOsVersion = determineInstalledOsVersion(
|
||||
props.bot, maybeGetDevice(props.resources.index));
|
||||
const getWebAppConfigVal = getWebAppConfigValue(() => props);
|
||||
|
||||
return {
|
||||
feeds: selectAllWebcamFeeds(resources.index),
|
||||
|
@ -34,5 +37,6 @@ export function mapStateToProps(props: Everything): Props {
|
|||
botToMqttStatus,
|
||||
firmwareSettings: fwConfig || mcu_params,
|
||||
shouldDisplay: shouldDisplay(installedOsVersion, props.bot.minOsFeatureData),
|
||||
xySwap: !!getWebAppConfigVal(BooleanSetting.xy_swap),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,55 +1,27 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { DirectionButton } from "./controls/direction_button";
|
||||
import { Xyz, BotPosition } from "./devices/interfaces";
|
||||
import { McuParams } from "farmbot";
|
||||
import { getDevice } from "./device";
|
||||
import { buildDirectionProps } from "./controls/direction_axes_props";
|
||||
import { ControlsPopupProps } from "./controls/interfaces";
|
||||
|
||||
interface State {
|
||||
isOpen: boolean;
|
||||
stepSize: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
dispatch: Function;
|
||||
axisInversion: Record<Xyz, boolean>;
|
||||
botPosition: BotPosition;
|
||||
mcuParams: McuParams;
|
||||
}
|
||||
|
||||
export class ControlsPopup extends React.Component<Props, Partial<State>> {
|
||||
|
||||
state: State = {
|
||||
isOpen: false,
|
||||
stepSize: 100
|
||||
};
|
||||
export class ControlsPopup
|
||||
extends React.Component<ControlsPopupProps, Partial<State>> {
|
||||
state: State = { isOpen: false };
|
||||
|
||||
private toggle = (property: keyof State) => () =>
|
||||
this.setState({ [property]: !this.state[property] });
|
||||
|
||||
public render() {
|
||||
const isOpen = this.state.isOpen ? "open" : "";
|
||||
const { mcuParams } = this.props;
|
||||
const directionAxesProps = {
|
||||
x: {
|
||||
isInverted: this.props.axisInversion.x,
|
||||
stopAtHome: !!mcuParams.movement_stop_at_home_x,
|
||||
stopAtMax: !!mcuParams.movement_stop_at_max_x,
|
||||
axisLength: (mcuParams.movement_axis_nr_steps_x || 0)
|
||||
/ (mcuParams.movement_step_per_mm_x || 1),
|
||||
negativeOnly: !!mcuParams.movement_home_up_x,
|
||||
position: this.props.botPosition.x
|
||||
},
|
||||
y: {
|
||||
isInverted: this.props.axisInversion.y,
|
||||
stopAtHome: !!mcuParams.movement_stop_at_home_y,
|
||||
stopAtMax: !!mcuParams.movement_stop_at_max_y,
|
||||
axisLength: (mcuParams.movement_axis_nr_steps_y || 0)
|
||||
/ (mcuParams.movement_step_per_mm_y || 1),
|
||||
negativeOnly: !!mcuParams.movement_home_up_y,
|
||||
position: this.props.botPosition.y
|
||||
}
|
||||
};
|
||||
const { stepSize, xySwap, arduinoBusy } = this.props;
|
||||
const directionAxesProps = buildDirectionProps(this.props);
|
||||
const rightLeft = xySwap ? "y" : "x";
|
||||
const upDown = xySwap ? "x" : "y";
|
||||
return <div
|
||||
className={"controls-popup " + isOpen}>
|
||||
<i className="fa fa-crosshairs"
|
||||
|
@ -57,29 +29,29 @@ export class ControlsPopup extends React.Component<Props, Partial<State>> {
|
|||
<div className="controls-popup-menu-outer">
|
||||
<div className="controls-popup-menu-inner">
|
||||
<DirectionButton
|
||||
axis={"x"}
|
||||
axis={rightLeft}
|
||||
direction="right"
|
||||
directionAxisProps={directionAxesProps.x}
|
||||
steps={this.state.stepSize}
|
||||
disabled={!isOpen} />
|
||||
directionAxisProps={directionAxesProps[rightLeft]}
|
||||
steps={stepSize}
|
||||
disabled={!isOpen || arduinoBusy} />
|
||||
<DirectionButton
|
||||
axis={"y"}
|
||||
axis={upDown}
|
||||
direction="up"
|
||||
directionAxisProps={directionAxesProps.y}
|
||||
steps={this.state.stepSize}
|
||||
disabled={!isOpen} />
|
||||
directionAxisProps={directionAxesProps[upDown]}
|
||||
steps={stepSize}
|
||||
disabled={!isOpen || arduinoBusy} />
|
||||
<DirectionButton
|
||||
axis={"y"}
|
||||
axis={upDown}
|
||||
direction="down"
|
||||
directionAxisProps={directionAxesProps.y}
|
||||
steps={this.state.stepSize}
|
||||
disabled={!isOpen} />
|
||||
directionAxisProps={directionAxesProps[upDown]}
|
||||
steps={stepSize}
|
||||
disabled={!isOpen || arduinoBusy} />
|
||||
<DirectionButton
|
||||
axis={"x"}
|
||||
axis={rightLeft}
|
||||
direction="left"
|
||||
directionAxisProps={directionAxesProps.x}
|
||||
steps={this.state.stepSize}
|
||||
disabled={!isOpen} />
|
||||
directionAxisProps={directionAxesProps[rightLeft]}
|
||||
steps={stepSize}
|
||||
disabled={!isOpen || arduinoBusy} />
|
||||
<button
|
||||
className="i fa fa-camera arrow-button fb-button brown"
|
||||
onClick={() => getDevice().takePhoto().catch(() => { })} />
|
||||
|
|
|
@ -319,7 +319,7 @@ fieldset {
|
|||
}
|
||||
}
|
||||
|
||||
.webcam-stream-unavailable text {
|
||||
.webcam-stream-unavailable p {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
@ -484,6 +484,18 @@ ul {
|
|||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.move-settings-menu {
|
||||
label {
|
||||
margin-top: 7px;
|
||||
}
|
||||
p {
|
||||
margin-top: 0.7rem;
|
||||
font-size: 1.4rem;
|
||||
color: $medium_gray;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.controls-popup,
|
||||
.controls-popup-menu-outer {
|
||||
position: fixed;
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
margin: auto;
|
||||
margin-top: 15px;
|
||||
width: auto;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.arrow-button {
|
||||
|
|
|
@ -3,6 +3,7 @@ import { BotExtents } from "../bot_extents";
|
|||
import { shallow } from "enzyme";
|
||||
import { bot } from "../../../__test_support__/fake_state/bot";
|
||||
import { BotExtentsProps } from "../interfaces";
|
||||
import { fakeMapTransformProps } from "../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<VirtualFarmBot/>", () => {
|
||||
function fakeProps(): BotExtentsProps {
|
||||
|
@ -10,9 +11,7 @@ describe("<VirtualFarmBot/>", () => {
|
|||
mcuParams.movement_stop_at_home_x = 1;
|
||||
mcuParams.movement_stop_at_home_y = 1;
|
||||
return {
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
stopAtHome: { x: true, y: true },
|
||||
botSize: {
|
||||
x: { value: 3000, isDefault: true },
|
||||
|
@ -23,7 +22,7 @@ describe("<VirtualFarmBot/>", () => {
|
|||
|
||||
it("renders home lines", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<BotExtents {...p } />);
|
||||
const wrapper = shallow(<BotExtents {...p} />);
|
||||
const homeLines = wrapper.find("#home-lines").find("line");
|
||||
expect(homeLines.at(0).props()).toEqual({ "x1": 2, "x2": 2, "y1": 2, "y2": 1500 });
|
||||
expect(homeLines.at(1).props()).toEqual({ "x1": 2, "x2": 3000, "y1": 2, "y2": 2 });
|
||||
|
@ -38,7 +37,7 @@ describe("<VirtualFarmBot/>", () => {
|
|||
x: { value: 100, isDefault: false },
|
||||
y: { value: 100, isDefault: false }
|
||||
};
|
||||
const wrapper = shallow(<BotExtents {...p } />);
|
||||
const wrapper = shallow(<BotExtents {...p} />);
|
||||
const homeLines = wrapper.find("#home-lines").find("line");
|
||||
expect(homeLines.at(0).props()).toEqual({ "x1": 2, "x2": 2, "y1": 2, "y2": 100 });
|
||||
expect(homeLines.at(1).props()).toEqual({ "x1": 2, "x2": 100, "y1": 2, "y2": 2 });
|
||||
|
@ -54,7 +53,7 @@ describe("<VirtualFarmBot/>", () => {
|
|||
x: { value: 3000, isDefault: true },
|
||||
y: { value: 100, isDefault: false }
|
||||
};
|
||||
const wrapper = shallow(<BotExtents {...p } />);
|
||||
const wrapper = shallow(<BotExtents {...p} />);
|
||||
const homeLines = wrapper.find("#home-lines").find("line");
|
||||
expect(homeLines.at(0).props()).toEqual({ "x1": 2, "x2": 3000, "y1": 2, "y2": 2 });
|
||||
expect(homeLines.at(1).html()).toBeFalsy();
|
||||
|
@ -71,7 +70,7 @@ describe("<VirtualFarmBot/>", () => {
|
|||
x: { value: 100, isDefault: false },
|
||||
y: { value: 100, isDefault: false }
|
||||
};
|
||||
const wrapper = shallow(<BotExtents {...p } />);
|
||||
const wrapper = shallow(<BotExtents {...p} />);
|
||||
const homeLines = wrapper.find("#home-lines").find("line");
|
||||
expect(homeLines.at(0).html()).toBeFalsy();
|
||||
expect(homeLines.at(1).html()).toBeFalsy();
|
||||
|
@ -87,7 +86,7 @@ describe("<VirtualFarmBot/>", () => {
|
|||
x: { value: 100, isDefault: false },
|
||||
y: { value: 100, isDefault: false }
|
||||
};
|
||||
const wrapper = shallow(<BotExtents {...p } />);
|
||||
const wrapper = shallow(<BotExtents {...p} />);
|
||||
const homeLines = wrapper.find("#home-lines").find("line");
|
||||
expect(homeLines.at(0).props()).toEqual({ "x1": 2998, "x2": 2998, "y1": 2, "y2": 100 });
|
||||
expect(homeLines.at(1).props()).toEqual({ "x1": 2998, "x2": 2900, "y1": 2, "y2": 2 });
|
||||
|
@ -96,11 +95,38 @@ describe("<VirtualFarmBot/>", () => {
|
|||
expect(maxLines.at(1).props()).toEqual({ "x1": 2998, "x2": 2900, "y1": 100, "y2": 100 });
|
||||
});
|
||||
|
||||
it("renders max line in correct location", () => {
|
||||
const p = fakeProps();
|
||||
p.stopAtHome.x = false;
|
||||
p.stopAtHome.y = false;
|
||||
p.botSize = {
|
||||
x: { value: 100, isDefault: false },
|
||||
y: { value: 100, isDefault: true }
|
||||
};
|
||||
const wrapper = shallow(<BotExtents {...p} />);
|
||||
const maxLines = wrapper.find("#max-lines").find("line");
|
||||
expect(maxLines.at(0).props()).toEqual({ "x1": 100, "x2": 100, "y1": 2, "y2": 100 });
|
||||
});
|
||||
|
||||
it("renders max line in correct location with swapped axes", () => {
|
||||
const p = fakeProps();
|
||||
p.stopAtHome.x = false;
|
||||
p.stopAtHome.y = false;
|
||||
p.mapTransformProps.xySwap = true;
|
||||
p.botSize = {
|
||||
x: { value: 100, isDefault: false },
|
||||
y: { value: 100, isDefault: true }
|
||||
};
|
||||
const wrapper = shallow(<BotExtents {...p} />);
|
||||
const maxLines = wrapper.find("#max-lines").find("line");
|
||||
expect(maxLines.at(0).props()).toEqual({ "x1": 2, "x2": 100, "y1": 100, "y2": 100 });
|
||||
});
|
||||
|
||||
it("renders no lines", () => {
|
||||
const p = fakeProps();
|
||||
p.stopAtHome.x = false;
|
||||
p.stopAtHome.y = false;
|
||||
const wrapper = shallow(<BotExtents {...p } />);
|
||||
const wrapper = shallow(<BotExtents {...p} />);
|
||||
const homeLines = wrapper.find("#home-lines").find("line");
|
||||
expect(homeLines.at(0).html()).toBeFalsy();
|
||||
expect(homeLines.at(1).html()).toBeFalsy();
|
||||
|
|
|
@ -4,13 +4,12 @@ import { shallow } from "enzyme";
|
|||
import { DragHelpersProps } from "../interfaces";
|
||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||
import { Color } from "../../../ui/index";
|
||||
import { fakeMapTransformProps } from "../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<DragHelpers/>", () => {
|
||||
function fakeProps(): DragHelpersProps {
|
||||
return {
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
plant: fakePlant(),
|
||||
dragging: false,
|
||||
zoomLvl: 1.8,
|
||||
|
@ -20,7 +19,7 @@ describe("<DragHelpers/>", () => {
|
|||
}
|
||||
|
||||
it("doesn't render drag helpers", () => {
|
||||
const wrapper = shallow(<DragHelpers {...fakeProps() } />);
|
||||
const wrapper = shallow(<DragHelpers {...fakeProps()} />);
|
||||
expect(wrapper.find("text").length).toEqual(0);
|
||||
expect(wrapper.find("rect").length).toBeLessThanOrEqual(1);
|
||||
expect(wrapper.find("use").length).toEqual(0);
|
||||
|
@ -29,7 +28,7 @@ describe("<DragHelpers/>", () => {
|
|||
it("renders drag helpers", () => {
|
||||
const p = fakeProps();
|
||||
p.dragging = true;
|
||||
const wrapper = shallow(<DragHelpers {...p } />);
|
||||
const wrapper = shallow(<DragHelpers {...p} />);
|
||||
expect(wrapper.find("#coordinates-tooltip").length).toEqual(1);
|
||||
expect(wrapper.find("#long-crosshair").length).toEqual(1);
|
||||
expect(wrapper.find("#short-crosshair").length).toEqual(1);
|
||||
|
@ -42,7 +41,7 @@ describe("<DragHelpers/>", () => {
|
|||
p.dragging = true;
|
||||
p.plant.body.x = 104;
|
||||
p.plant.body.y = 199;
|
||||
const wrapper = shallow(<DragHelpers {...p } />);
|
||||
const wrapper = shallow(<DragHelpers {...p} />);
|
||||
expect(wrapper.find("text").length).toEqual(1);
|
||||
expect(wrapper.find("text").text()).toEqual("100, 200");
|
||||
expect(wrapper.find("text").props().fontSize).toEqual("1.25rem");
|
||||
|
@ -53,7 +52,7 @@ describe("<DragHelpers/>", () => {
|
|||
const p = fakeProps();
|
||||
p.dragging = true;
|
||||
p.zoomLvl = 0.9;
|
||||
const wrapper = shallow(<DragHelpers {...p } />);
|
||||
const wrapper = shallow(<DragHelpers {...p} />);
|
||||
expect(wrapper.find("text").length).toEqual(1);
|
||||
expect(wrapper.find("text").text()).toEqual("100, 200");
|
||||
expect(wrapper.find("text").props().fontSize).toEqual("3rem");
|
||||
|
@ -64,7 +63,7 @@ describe("<DragHelpers/>", () => {
|
|||
const p = fakeProps();
|
||||
p.dragging = true;
|
||||
p.plant.body.id = 5;
|
||||
const wrapper = shallow(<DragHelpers {...p } />);
|
||||
const wrapper = shallow(<DragHelpers {...p} />);
|
||||
const crosshair = wrapper.find("#short-crosshair");
|
||||
expect(crosshair.length).toEqual(1);
|
||||
const segment = crosshair.find("#crosshair-segment-5");
|
||||
|
@ -83,7 +82,7 @@ describe("<DragHelpers/>", () => {
|
|||
const p = fakeProps();
|
||||
p.dragging = true;
|
||||
p.zoomLvl = 0.9;
|
||||
const wrapper = shallow(<DragHelpers {...p } />);
|
||||
const wrapper = shallow(<DragHelpers {...p} />);
|
||||
const crosshair = wrapper.find("#short-crosshair");
|
||||
expect(crosshair.length).toEqual(1);
|
||||
expect(crosshair.find("rect").first().props())
|
||||
|
@ -98,7 +97,7 @@ describe("<DragHelpers/>", () => {
|
|||
p.plant.body.x = 100;
|
||||
p.plant.body.y = 100;
|
||||
p.activeDragXY = { x: 100, y: 0, z: 0 };
|
||||
const wrapper = shallow(<DragHelpers {...p } />);
|
||||
const wrapper = shallow(<DragHelpers {...p} />);
|
||||
const indicators = wrapper.find("#alignment-indicator");
|
||||
expect(indicators.length).toEqual(1);
|
||||
const segment = indicators.find("#alignment-indicator-segment-5");
|
||||
|
@ -119,7 +118,7 @@ describe("<DragHelpers/>", () => {
|
|||
p.plant.body.x = 100;
|
||||
p.plant.body.y = 100;
|
||||
p.activeDragXY = { x: 0, y: 100, z: 0 };
|
||||
const wrapper = shallow(<DragHelpers {...p } />);
|
||||
const wrapper = shallow(<DragHelpers {...p} />);
|
||||
const indicator = wrapper.find("#alignment-indicator");
|
||||
const segments = indicator.find("use");
|
||||
expect(segments.length).toEqual(2);
|
||||
|
@ -136,7 +135,7 @@ describe("<DragHelpers/>", () => {
|
|||
p.plant.body.x = 100;
|
||||
p.plant.body.y = 100;
|
||||
p.activeDragXY = { x: 100, y: 100, z: 0 };
|
||||
const wrapper = shallow(<DragHelpers {...p } />);
|
||||
const wrapper = shallow(<DragHelpers {...p} />);
|
||||
const indicator = wrapper.find("#alignment-indicator");
|
||||
const masterSegment = indicator.find("#alignment-indicator-segment-6");
|
||||
const segmentProps = masterSegment.find("rect").props();
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import * as React from "react";
|
||||
import { DrawnPoint, DrawnPointProps } from "../drawn_point";
|
||||
import { mount } from "enzyme";
|
||||
import { fakeMapTransformProps } from "../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<DrawnPoint/>", () => {
|
||||
function fakeProps(): DrawnPointProps {
|
||||
return {
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
data: {
|
||||
cx: 10,
|
||||
cy: 20,
|
||||
|
@ -18,7 +17,7 @@ describe("<DrawnPoint/>", () => {
|
|||
}
|
||||
|
||||
it("renders point", () => {
|
||||
const wrapper = mount(<DrawnPoint {...fakeProps() } />);
|
||||
const wrapper = mount(<DrawnPoint {...fakeProps()} />);
|
||||
expect(wrapper.find("g").props().stroke).toEqual("red");
|
||||
expect(wrapper.find("circle").first().props()).toEqual({
|
||||
id: "point-radius", strokeDasharray: "4 5",
|
||||
|
|
|
@ -7,6 +7,12 @@ jest.mock("farmbot-toastr", () => ({
|
|||
error: mockError
|
||||
}));
|
||||
|
||||
jest.mock("../../actions", () => ({
|
||||
closePlantInfo: jest.fn(),
|
||||
movePlant: jest.fn(),
|
||||
unselectPlant: jest.fn(() => jest.fn()),
|
||||
}));
|
||||
|
||||
jest.mock("../../../api/crud", () => ({
|
||||
initSave: jest.fn(),
|
||||
edit: () => "edit resource",
|
||||
|
@ -26,6 +32,7 @@ import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
|||
import { Actions } from "../../../constants";
|
||||
import { initSave } from "../../../api/crud";
|
||||
import { setEggStatus, EggKeys } from "../easter_eggs/status";
|
||||
import { movePlant, unselectPlant } from "../../actions";
|
||||
|
||||
function fakeProps(): GardenMapProps {
|
||||
return {
|
||||
|
@ -172,23 +179,79 @@ describe("<GardenPlant/>", () => {
|
|||
|
||||
it("drags: editing", () => {
|
||||
mockPath = "/app/designer/plants/1/edit";
|
||||
Object.defineProperty(window, "getComputedStyle", {
|
||||
value: () => { return { zoom: 0.5 }; }, configurable: true
|
||||
});
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<GardenMap {...p} />);
|
||||
expect(wrapper.state()).toEqual({});
|
||||
wrapper.find("#drop-area-svg").simulate("mouseMove");
|
||||
expect(p.dispatch).not.toHaveBeenCalled();
|
||||
expect(wrapper.state()).toEqual({});
|
||||
wrapper.setState({ isDragging: true });
|
||||
wrapper.setState({ isDragging: true, pageX: 200, pageY: 300 });
|
||||
wrapper.find("#drop-area-svg").simulate("mouseMove", {
|
||||
pageX: 1, pageY: 2
|
||||
pageX: 400, pageY: 500
|
||||
});
|
||||
expect(wrapper.state()).toEqual({
|
||||
activeDragXY: { x: 100, y: 200, z: 0 },
|
||||
activeDragXY: { x: 500, y: 600, z: 0 },
|
||||
isDragging: true,
|
||||
pageX: 1,
|
||||
pageY: 2
|
||||
pageX: 400,
|
||||
pageY: 500
|
||||
});
|
||||
expect(p.dispatch).toHaveBeenCalledWith("edit resource");
|
||||
expect(movePlant).toHaveBeenCalledWith(expect.objectContaining({
|
||||
deltaX: 400, deltaY: 400
|
||||
}));
|
||||
});
|
||||
|
||||
it("drags: editing, zoom undefined", () => {
|
||||
mockPath = "/app/designer/plants/1/edit";
|
||||
Object.defineProperty(window, "getComputedStyle", {
|
||||
value: () => { return { zoom: undefined }; }, configurable: true
|
||||
});
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<GardenMap {...p} />);
|
||||
expect(wrapper.state()).toEqual({});
|
||||
wrapper.find("#drop-area-svg").simulate("mouseMove");
|
||||
expect(p.dispatch).not.toHaveBeenCalled();
|
||||
expect(wrapper.state()).toEqual({});
|
||||
wrapper.setState({ isDragging: true, pageX: 300, pageY: 400 });
|
||||
wrapper.find("#drop-area-svg").simulate("mouseMove", {
|
||||
pageX: 400, pageY: 500
|
||||
});
|
||||
expect(wrapper.state()).toEqual({
|
||||
activeDragXY: { x: 200, y: 300, z: 0 },
|
||||
isDragging: true,
|
||||
pageX: 400,
|
||||
pageY: 500
|
||||
});
|
||||
expect(movePlant).toHaveBeenCalledWith(expect.objectContaining({
|
||||
deltaX: 100, deltaY: 100
|
||||
}));
|
||||
});
|
||||
|
||||
it("drags: editing, X&Y swapped", () => {
|
||||
mockPath = "/app/designer/plants/1/edit";
|
||||
const p = fakeProps();
|
||||
p.getConfigValue = () => true;
|
||||
p.botOriginQuadrant = 1;
|
||||
const wrapper = shallow(<GardenMap {...p} />);
|
||||
expect(wrapper.state()).toEqual({});
|
||||
wrapper.find("#drop-area-svg").simulate("mouseMove");
|
||||
expect(p.dispatch).not.toHaveBeenCalled();
|
||||
expect(wrapper.state()).toEqual({});
|
||||
wrapper.setState({ isDragging: true, pageX: 300, pageY: 500 });
|
||||
wrapper.find("#drop-area-svg").simulate("mouseMove", {
|
||||
pageX: 400, pageY: 500
|
||||
});
|
||||
expect(wrapper.state()).toEqual({
|
||||
activeDragXY: { x: -100, y: 300, z: 0 },
|
||||
isDragging: true,
|
||||
pageX: 500,
|
||||
pageY: 400
|
||||
});
|
||||
expect(movePlant).toHaveBeenCalledWith(expect.objectContaining({
|
||||
deltaX: -200, deltaY: 100
|
||||
}));
|
||||
});
|
||||
|
||||
it("starts drag: selecting", async () => {
|
||||
|
@ -251,6 +314,23 @@ describe("<GardenPlant/>", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("selects location: zoom undefined", async () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<GardenMap {...p} />);
|
||||
Object.defineProperty(window, "getComputedStyle", {
|
||||
value: () => { return { zoom: undefined }; }, configurable: true
|
||||
});
|
||||
expect(wrapper.state()).toEqual({});
|
||||
mockPath = "/app/designer/plants/move_to";
|
||||
await wrapper.find("#drop-area-svg").simulate("click", {
|
||||
pageX: 1000, pageY: 2000, preventDefault: jest.fn()
|
||||
});
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
payload: { x: 580, y: 1790, z: 0 },
|
||||
type: Actions.CHOOSE_LOCATION
|
||||
});
|
||||
});
|
||||
|
||||
it("starts drawing point", async () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<GardenMap {...p} />);
|
||||
|
@ -292,4 +372,40 @@ describe("<GardenPlant/>", () => {
|
|||
const eggs = shallow(<GardenMap {...fakeProps()} />);
|
||||
expect(eggs.find("Bugs").length).toEqual(1);
|
||||
});
|
||||
|
||||
it(".drop-area: handles drag over", () => {
|
||||
mockPath = "/app/designer/plants/crop_search";
|
||||
const wrapper = shallow(<GardenMap {...fakeProps()} />);
|
||||
const e = {
|
||||
dataTransfer: { dropEffect: undefined },
|
||||
preventDefault: jest.fn()
|
||||
};
|
||||
wrapper.find(".drop-area").simulate("dragOver", e);
|
||||
expect(e.dataTransfer.dropEffect).toEqual("move");
|
||||
});
|
||||
|
||||
it(".drop-area: handles drag start", () => {
|
||||
mockPath = "/app/designer/plants";
|
||||
const wrapper = shallow(<GardenMap {...fakeProps()} />);
|
||||
const e = { preventDefault: jest.fn() };
|
||||
wrapper.find(".drop-area").simulate("dragStart", e);
|
||||
expect(e.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(".drop-area: handles drag enter", () => {
|
||||
mockPath = "/app/designer/plants/crop_search";
|
||||
const wrapper = shallow(<GardenMap {...fakeProps()} />);
|
||||
const e = { preventDefault: jest.fn() };
|
||||
wrapper.find(".drop-area").simulate("dragEnter", e);
|
||||
expect(e.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls unselectPlant on unmount", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<GardenMap {...p} />);
|
||||
// tslint:disable-next-line:no-any
|
||||
const instance = wrapper.instance() as any;
|
||||
instance.componentWillUnmount();
|
||||
expect(unselectPlant).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,13 +19,12 @@ import { GardenPlantProps } from "../interfaces";
|
|||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||
import { BooleanSetting } from "../../../session_keys";
|
||||
import { Actions } from "../../../constants";
|
||||
import { fakeMapTransformProps } from "../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<GardenPlant/>", () => {
|
||||
function fakeProps(): GardenPlantProps {
|
||||
return {
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
plant: fakePlant(),
|
||||
selected: false,
|
||||
grayscale: false,
|
||||
|
@ -39,7 +38,7 @@ describe("<GardenPlant/>", () => {
|
|||
|
||||
it("renders plant", () => {
|
||||
mockStorj[BooleanSetting.disable_animations] = true;
|
||||
const wrapper = shallow(<GardenPlant {...fakeProps() } />);
|
||||
const wrapper = shallow(<GardenPlant {...fakeProps()} />);
|
||||
expect(wrapper.find("image").length).toEqual(1);
|
||||
expect(wrapper.find("image").props().opacity).toEqual(1);
|
||||
expect(wrapper.find("text").length).toEqual(0);
|
||||
|
@ -50,14 +49,14 @@ describe("<GardenPlant/>", () => {
|
|||
|
||||
it("renders plant animations", () => {
|
||||
mockStorj[BooleanSetting.disable_animations] = false;
|
||||
const wrapper = shallow(<GardenPlant {...fakeProps() } />);
|
||||
const wrapper = shallow(<GardenPlant {...fakeProps()} />);
|
||||
expect(wrapper.find(".soil-cloud").length).toEqual(1);
|
||||
expect(wrapper.find(".animate").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("Calls the onClick callback", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<GardenPlant {...p } />);
|
||||
const wrapper = shallow(<GardenPlant {...p} />);
|
||||
wrapper.find("image").at(0).simulate("click");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.SELECT_PLANT,
|
||||
|
@ -67,7 +66,7 @@ describe("<GardenPlant/>", () => {
|
|||
|
||||
it("begins hover", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<GardenPlant {...p } />);
|
||||
const wrapper = shallow(<GardenPlant {...p} />);
|
||||
wrapper.find("image").at(0).simulate("mouseEnter");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.HOVER_PLANT_LIST_ITEM,
|
||||
|
@ -77,7 +76,7 @@ describe("<GardenPlant/>", () => {
|
|||
|
||||
it("ends hover", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<GardenPlant {...p } />);
|
||||
const wrapper = shallow(<GardenPlant {...p} />);
|
||||
wrapper.find("image").at(0).simulate("mouseLeave");
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
type: Actions.HOVER_PLANT_LIST_ITEM,
|
||||
|
@ -87,14 +86,14 @@ describe("<GardenPlant/>", () => {
|
|||
|
||||
it("has color", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<GardenPlant {...p } />);
|
||||
const wrapper = shallow(<GardenPlant {...p} />);
|
||||
expect(wrapper.find("image").props().filter).toEqual("");
|
||||
});
|
||||
|
||||
it("has no color", () => {
|
||||
const p = fakeProps();
|
||||
p.grayscale = true;
|
||||
const wrapper = shallow(<GardenPlant {...p } />);
|
||||
const wrapper = shallow(<GardenPlant {...p} />);
|
||||
expect(wrapper.find("image").props().filter).toEqual("url(#grayscale)");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,19 +3,18 @@ import { GardenPoint } from "../garden_point";
|
|||
import { shallow } from "enzyme";
|
||||
import { GardenPointProps } from "../interfaces";
|
||||
import { fakePoint } from "../../../__test_support__/fake_state/resources";
|
||||
import { fakeMapTransformProps } from "../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<GardenPoint/>", () => {
|
||||
function fakeProps(): GardenPointProps {
|
||||
return {
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
point: fakePoint()
|
||||
};
|
||||
}
|
||||
|
||||
it("renders point", () => {
|
||||
const wrapper = shallow(<GardenPoint {...fakeProps() } />);
|
||||
const wrapper = shallow(<GardenPoint {...fakeProps()} />);
|
||||
expect(wrapper.find("#point-radius").props().r).toEqual(100);
|
||||
expect(wrapper.find("#point-center").props().r).toEqual(2);
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as React from "react";
|
|||
import { Grid } from "../grid";
|
||||
import { shallow } from "enzyme";
|
||||
import { GridProps } from "../interfaces";
|
||||
import { fakeMapTransformProps } from "../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<Grid/>", () => {
|
||||
beforeEach(function () {
|
||||
|
@ -10,21 +11,33 @@ describe("<Grid/>", () => {
|
|||
|
||||
function fakeProps(): GridProps {
|
||||
return {
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
dispatch: jest.fn(),
|
||||
onClick: jest.fn()
|
||||
};
|
||||
}
|
||||
|
||||
it("renders grid", () => {
|
||||
const wrapper = shallow(<Grid {...fakeProps() } />);
|
||||
expect(wrapper.find("#major-grid").props().width).toEqual(3000);
|
||||
expect(wrapper.find("#minor-grid").props().width).toEqual(3000);
|
||||
const expectedGridShape = { width: 3000, height: 1500 };
|
||||
const wrapper = shallow(<Grid {...fakeProps()} />);
|
||||
expect(wrapper.find("#major-grid").props()).toEqual(
|
||||
expect.objectContaining(expectedGridShape));
|
||||
expect(wrapper.find("#minor-grid").props()).toEqual(
|
||||
expect.objectContaining(expectedGridShape));
|
||||
expect(wrapper.find("#axis-arrows").find("line").first().props())
|
||||
.toEqual({ x1: 0, x2: 25, y1: 0, y2: 0 });
|
||||
expect(wrapper.find("#axis-values").find("text").length).toEqual(43);
|
||||
});
|
||||
|
||||
it("renders grid: X&Y swapped", () => {
|
||||
const expectedGridShape = { width: 1500, height: 3000 };
|
||||
const p = fakeProps();
|
||||
p.mapTransformProps.xySwap = true;
|
||||
const wrapper = shallow(<Grid {...p} />);
|
||||
expect(wrapper.find("#major-grid").props()).toEqual(
|
||||
expect.objectContaining(expectedGridShape));
|
||||
expect(wrapper.find("#minor-grid").props()).toEqual(
|
||||
expect.objectContaining(expectedGridShape));
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -2,21 +2,31 @@ import * as React from "react";
|
|||
import { MapBackground } from "../map_background";
|
||||
import { shallow } from "enzyme";
|
||||
import { MapBackgroundProps } from "../interfaces";
|
||||
import { fakeMapTransformProps } from "../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<MapBackground/>", () => {
|
||||
function fakeProps(): MapBackgroundProps {
|
||||
return {
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
plantAreaOffset: { x: 100, y: 100 }
|
||||
};
|
||||
}
|
||||
|
||||
it("renders map background", () => {
|
||||
const wrapper = shallow(<MapBackground {...fakeProps() } />);
|
||||
expect(wrapper.find("#bed-interior").props().width).toEqual(3180);
|
||||
expect(wrapper.find("#bed-border").props().width).toEqual(3200);
|
||||
const wrapper = shallow(<MapBackground {...fakeProps()} />);
|
||||
expect(wrapper.find("#bed-interior").props()).toEqual(
|
||||
expect.objectContaining({ width: 3180, height: 1680 }));
|
||||
expect(wrapper.find("#bed-border").props()).toEqual(
|
||||
expect.objectContaining({ width: 3200, height: 1700 }));
|
||||
});
|
||||
|
||||
it("renders map background: X&Y swapped", () => {
|
||||
const p = fakeProps();
|
||||
p.mapTransformProps.xySwap = true;
|
||||
const wrapper = shallow(<MapBackground {...p} />);
|
||||
expect(wrapper.find("#bed-interior").props()).toEqual(
|
||||
expect.objectContaining({ width: 1680, height: 3180 }));
|
||||
expect(wrapper.find("#bed-border").props()).toEqual(
|
||||
expect.objectContaining({ width: 1700, height: 3200 }));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ import { MapImage, MapImageProps } from "../map_image";
|
|||
import { SpecialStatus } from "../../../resources/tagged_resources";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { trim } from "../../../util";
|
||||
import { fakeMapTransformProps } from "../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<MapImage />", () => {
|
||||
const fakeProps = (): MapImageProps => {
|
||||
|
@ -29,15 +30,12 @@ describe("<MapImage />", () => {
|
|||
scale: undefined,
|
||||
calibrationZ: undefined
|
||||
},
|
||||
mapTransformProps: {
|
||||
gridSize: { x: 0, y: 0 },
|
||||
quadrant: 1
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
};
|
||||
};
|
||||
|
||||
it("doesn't render image", () => {
|
||||
const wrapper = mount(<MapImage {...fakeProps() } />);
|
||||
const wrapper = mount(<MapImage {...fakeProps()} />);
|
||||
expect(wrapper.html()).toEqual("<image></image>");
|
||||
});
|
||||
|
||||
|
@ -49,8 +47,19 @@ describe("<MapImage />", () => {
|
|||
ty: number;
|
||||
}
|
||||
|
||||
interface ExtraTranslationData {
|
||||
rot: number;
|
||||
sx: number;
|
||||
sy: number;
|
||||
tx: number;
|
||||
ty: number;
|
||||
}
|
||||
|
||||
const renderedTest =
|
||||
(num: number, inputData: MapImageProps[], expectedData: ExpectedData) => {
|
||||
(num: number,
|
||||
inputData: MapImageProps[],
|
||||
expectedData: ExpectedData,
|
||||
extra?: ExtraTranslationData) => {
|
||||
it(`renders image: INPUT_SET_${num}`, () => {
|
||||
const wrapper = mount(<MapImage {...inputData[num]} />);
|
||||
expect(wrapper.find("image").props()).toEqual({
|
||||
|
@ -61,6 +70,8 @@ describe("<MapImage />", () => {
|
|||
height: expectedData.size.height,
|
||||
transform: trim(`scale(${expectedData.sx}, ${expectedData.sy})
|
||||
translate(${expectedData.tx}, ${expectedData.ty})`)
|
||||
+ (extra ? trim(` rotate(${extra.rot}) scale(${extra.sx}, ${extra.sy})
|
||||
translate(${extra.tx}, ${extra.ty})`) : "")
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -77,10 +88,9 @@ describe("<MapImage />", () => {
|
|||
scale: "0.8041",
|
||||
calibrationZ: "0"
|
||||
};
|
||||
INPUT_SET_1.mapTransformProps = {
|
||||
gridSize: { x: 5900, y: 2900 },
|
||||
quadrant: 3
|
||||
};
|
||||
INPUT_SET_1.mapTransformProps = fakeMapTransformProps();
|
||||
INPUT_SET_1.mapTransformProps.gridSize = { x: 5900, y: 2900 },
|
||||
INPUT_SET_1.mapTransformProps.quadrant = 3;
|
||||
INPUT_SET_1.sizeOverride = { width: 480, height: 640 };
|
||||
|
||||
const INPUT_SET_2 = cloneDeep(INPUT_SET_1);
|
||||
|
@ -106,10 +116,13 @@ describe("<MapImage />", () => {
|
|||
const INPUT_SET_7 = cloneDeep(INPUT_SET_6);
|
||||
INPUT_SET_7.cameraCalibrationData.origin = "BOTTOM_RIGHT";
|
||||
|
||||
const INPUT_SET_8 = cloneDeep(INPUT_SET_7);
|
||||
INPUT_SET_8.mapTransformProps.xySwap = true;
|
||||
|
||||
const DATA = [
|
||||
INPUT_SET_1,
|
||||
INPUT_SET_1, INPUT_SET_2, INPUT_SET_3, INPUT_SET_4, INPUT_SET_5,
|
||||
INPUT_SET_6, INPUT_SET_7
|
||||
INPUT_SET_6, INPUT_SET_7, INPUT_SET_8
|
||||
];
|
||||
|
||||
const expectedSize = { width: 385.968, height: 514.624 };
|
||||
|
@ -135,6 +148,9 @@ describe("<MapImage />", () => {
|
|||
renderedTest(7, DATA, {
|
||||
size: expectedSize, sx: 1, sy: 1, tx: 5436.016, ty: 2259.688
|
||||
});
|
||||
renderedTest(8, DATA, {
|
||||
size: expectedSize, sx: 1, sy: 1, tx: 2388.344, ty: 5307.36
|
||||
}, { rot: 90, sx: -1, sy: 1, tx: -514.624, ty: -514.624 });
|
||||
|
||||
it("doesn't render placeholder image", () => {
|
||||
const p = INPUT_SET_1;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { SelectionBox, SelectionBoxProps } from "../selection_box";
|
||||
import { shallow } from "enzyme";
|
||||
import { fakeMapTransformProps } from "../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<SelectionBox/>", () => {
|
||||
function fakeProps(): SelectionBoxProps {
|
||||
|
@ -11,14 +12,12 @@ describe("<SelectionBox/>", () => {
|
|||
x1: 240,
|
||||
y1: 130
|
||||
},
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
}
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
};
|
||||
}
|
||||
|
||||
it("renders selection box", () => {
|
||||
const wrapper = shallow(<SelectionBox {...fakeProps() } />);
|
||||
const wrapper = shallow(<SelectionBox {...fakeProps()} />);
|
||||
const boxProps = wrapper.find("rect").props();
|
||||
expect(boxProps.x).toEqual(40);
|
||||
expect(boxProps.y).toEqual(30);
|
||||
|
@ -29,14 +28,14 @@ describe("<SelectionBox/>", () => {
|
|||
it("doesn't render selection box: partially undefined", () => {
|
||||
const p = fakeProps();
|
||||
p.selectionBox.x1 = undefined;
|
||||
const wrapper = shallow(<SelectionBox {...p } />);
|
||||
const wrapper = shallow(<SelectionBox {...p} />);
|
||||
expect(wrapper.html()).toEqual("<g id=\"selection-box\"></g>");
|
||||
});
|
||||
|
||||
it("renders selection box: quadrant 4", () => {
|
||||
const p = fakeProps();
|
||||
p.mapTransformProps.quadrant = 4;
|
||||
const wrapper = shallow(<SelectionBox {...p } />);
|
||||
const wrapper = shallow(<SelectionBox {...p} />);
|
||||
const boxProps = wrapper.find("rect").props();
|
||||
expect(boxProps.x).toEqual(2760);
|
||||
expect(boxProps.y).toEqual(1370);
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { shallow } from "enzyme";
|
||||
import { SpreadOverlapHelperProps } from "../interfaces";
|
||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||
import { fakeMapTransformProps } from "../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<SpreadOverlapHelper/>", () => {
|
||||
function fakeProps(): SpreadOverlapHelperProps {
|
||||
|
@ -19,9 +20,7 @@ describe("<SpreadOverlapHelper/>", () => {
|
|||
plant.body.y = 100;
|
||||
plant.body.radius = 25;
|
||||
return {
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
plant,
|
||||
dragging: false,
|
||||
zoomLvl: 1,
|
||||
|
@ -36,7 +35,7 @@ describe("<SpreadOverlapHelper/>", () => {
|
|||
// Center distance: 900mm (inactive plant at x=100, y=100)
|
||||
// Overlap: -650mm (default spread = radius * 10 = 250mm)
|
||||
// Percentage overlap of inactive plant: 0%
|
||||
const wrapper = shallow(<SpreadOverlapHelper {...p } />);
|
||||
const wrapper = shallow(<SpreadOverlapHelper {...p} />);
|
||||
const indicator = wrapper.find(".overlap-circle").props();
|
||||
expect(indicator.fill).toEqual("none");
|
||||
});
|
||||
|
@ -47,7 +46,7 @@ describe("<SpreadOverlapHelper/>", () => {
|
|||
// Center distance: 240mm (inactive plant at x=100, y=100)
|
||||
// Overlap: 10mm (default spread = radius * 10 = 250mm)
|
||||
// Percentage overlap of inactive plant: 4%
|
||||
const wrapper = shallow(<SpreadOverlapHelper {...p } />);
|
||||
const wrapper = shallow(<SpreadOverlapHelper {...p} />);
|
||||
const indicator = wrapper.find(".overlap-circle").props();
|
||||
expect(indicator.fill).toEqual("rgba(41, 141, 0, 0.04)"); // "green"
|
||||
});
|
||||
|
@ -58,7 +57,7 @@ describe("<SpreadOverlapHelper/>", () => {
|
|||
// Center distance: 200mm (inactive plant at x=100, y=100)
|
||||
// Overlap: 50mm (default spread = radius * 10 = 250mm)
|
||||
// Percentage overlap of inactive plant: 20%
|
||||
const wrapper = shallow(<SpreadOverlapHelper {...p } />);
|
||||
const wrapper = shallow(<SpreadOverlapHelper {...p} />);
|
||||
const indicator = wrapper.find(".overlap-circle").props();
|
||||
expect(indicator.fill).toEqual("rgba(204, 255, 0, 0.2)"); // "yellow"
|
||||
});
|
||||
|
@ -69,7 +68,7 @@ describe("<SpreadOverlapHelper/>", () => {
|
|||
// Center distance: 150mm (inactive plant at x=100, y=100)
|
||||
// Overlap: 100mm (default spread = radius * 10 = 250mm)
|
||||
// Percentage overlap of inactive plant: 40%
|
||||
const wrapper = shallow(<SpreadOverlapHelper {...p } />);
|
||||
const wrapper = shallow(<SpreadOverlapHelper {...p} />);
|
||||
const indicator = wrapper.find(".overlap-circle").props();
|
||||
expect(indicator.fill).toEqual("rgba(255, 102, 0, 0.3)"); // "orange"
|
||||
});
|
||||
|
@ -80,7 +79,7 @@ describe("<SpreadOverlapHelper/>", () => {
|
|||
// Center distance: 125mm (inactive plant at x=100, y=100)
|
||||
// Overlap: 125mm (default spread = radius * 10 = 250mm)
|
||||
// Percentage overlap of inactive plant: 50%
|
||||
const wrapper = shallow(<SpreadOverlapHelper {...p } />);
|
||||
const wrapper = shallow(<SpreadOverlapHelper {...p} />);
|
||||
const indicator = wrapper.find(".overlap-circle").props();
|
||||
expect(indicator.fill).toEqual("rgba(255, 20, 0, 0.3)"); // "red"
|
||||
});
|
||||
|
@ -91,7 +90,7 @@ describe("<SpreadOverlapHelper/>", () => {
|
|||
// Center distance: 50mm (inactive plant at x=100, y=100)
|
||||
// Overlap: 200mm (default spread = radius * 10 = 250mm)
|
||||
// Percentage overlap of inactive plant: 80%
|
||||
const wrapper = shallow(<SpreadOverlapHelper {...p } />);
|
||||
const wrapper = shallow(<SpreadOverlapHelper {...p} />);
|
||||
const indicator = wrapper.find(".overlap-circle").props();
|
||||
expect(indicator.fill).toEqual("rgba(255, 0, 0, 0.3)"); // "red"
|
||||
});
|
||||
|
@ -102,7 +101,7 @@ describe("<SpreadOverlapHelper/>", () => {
|
|||
// Center distance: 0mm (inactive plant at x=100, y=100)
|
||||
// Overlap: 250mm (default spread = radius * 10 = 250mm)
|
||||
// Percentage overlap of inactive plant: 100%
|
||||
const wrapper = shallow(<SpreadOverlapHelper {...p } />);
|
||||
const wrapper = shallow(<SpreadOverlapHelper {...p} />);
|
||||
const indicator = wrapper.find(".overlap-circle").props();
|
||||
expect(indicator.fill).toEqual("rgba(255, 0, 0, 0.3)"); // "red"
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { TargetCoordinate, TargetCoordinateProps } from "../target_coordinate";
|
||||
import { shallow } from "enzyme";
|
||||
import { fakeMapTransformProps } from "../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<TargetCoordinate/>", () => {
|
||||
function fakeProps(): TargetCoordinateProps {
|
||||
|
@ -10,14 +11,12 @@ describe("<TargetCoordinate/>", () => {
|
|||
y: 200,
|
||||
z: 0
|
||||
},
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
}
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
};
|
||||
}
|
||||
|
||||
it("renders target", () => {
|
||||
const wrapper = shallow(<TargetCoordinate {...fakeProps() } />);
|
||||
const wrapper = shallow(<TargetCoordinate {...fakeProps()} />);
|
||||
const boxProps = wrapper.find("rect").props();
|
||||
expect(boxProps.x).toEqual(90);
|
||||
expect(boxProps.y).toEqual(198);
|
||||
|
|
|
@ -13,31 +13,49 @@ describe("<ToolbaySlot />", () => {
|
|||
x: 10,
|
||||
y: 20,
|
||||
pulloutDirection: 0,
|
||||
quadrant: 2
|
||||
quadrant: 2,
|
||||
xySwap: false,
|
||||
};
|
||||
};
|
||||
|
||||
const checkSlotDirection =
|
||||
(direction: number, quadrant: BotOriginQuadrant, expected: string) => {
|
||||
it(`renders slot, pullout: ${direction} quad: ${quadrant}`, () => {
|
||||
const p = fakeProps();
|
||||
p.pulloutDirection = direction;
|
||||
p.quadrant = quadrant;
|
||||
const wrapper = mount(<ToolbaySlot {...p } />);
|
||||
expect(wrapper.find("use").props().transform).toEqual(expected);
|
||||
});
|
||||
(direction: number,
|
||||
quadrant: BotOriginQuadrant,
|
||||
xySwap: boolean,
|
||||
expected: string) => {
|
||||
it(`renders slot, pullout: ${direction} quad: ${quadrant} yx: ${xySwap}`,
|
||||
() => {
|
||||
const p = fakeProps();
|
||||
p.pulloutDirection = direction;
|
||||
p.quadrant = quadrant;
|
||||
p.xySwap = xySwap;
|
||||
const wrapper = mount(<ToolbaySlot {...p} />);
|
||||
expect(wrapper.find("use").props().transform).toEqual(expected);
|
||||
});
|
||||
};
|
||||
checkSlotDirection(0, 2, "rotate(0, 10, 20)");
|
||||
checkSlotDirection(1, 1, "rotate(180, 10, 20)");
|
||||
checkSlotDirection(1, 2, "rotate(0, 10, 20)");
|
||||
checkSlotDirection(1, 3, "rotate(0, 10, 20)");
|
||||
checkSlotDirection(1, 4, "rotate(180, 10, 20)");
|
||||
checkSlotDirection(2, 3, "rotate(180, 10, 20)");
|
||||
checkSlotDirection(3, 1, "rotate(90, 10, 20)");
|
||||
checkSlotDirection(3, 2, "rotate(90, 10, 20)");
|
||||
checkSlotDirection(3, 3, "rotate(270, 10, 20)");
|
||||
checkSlotDirection(3, 4, "rotate(270, 10, 20)");
|
||||
checkSlotDirection(4, 3, "rotate(90, 10, 20)");
|
||||
checkSlotDirection(0, 2, false, "rotate(0, 10, 20)");
|
||||
checkSlotDirection(1, 1, false, "rotate(180, 10, 20)");
|
||||
checkSlotDirection(1, 2, false, "rotate(0, 10, 20)");
|
||||
checkSlotDirection(1, 3, false, "rotate(0, 10, 20)");
|
||||
checkSlotDirection(1, 4, false, "rotate(180, 10, 20)");
|
||||
checkSlotDirection(2, 3, false, "rotate(180, 10, 20)");
|
||||
checkSlotDirection(3, 1, false, "rotate(90, 10, 20)");
|
||||
checkSlotDirection(3, 2, false, "rotate(90, 10, 20)");
|
||||
checkSlotDirection(3, 3, false, "rotate(270, 10, 20)");
|
||||
checkSlotDirection(3, 4, false, "rotate(270, 10, 20)");
|
||||
checkSlotDirection(4, 3, false, "rotate(90, 10, 20)");
|
||||
|
||||
checkSlotDirection(0, 2, true, "rotate(180, 10, 20)");
|
||||
checkSlotDirection(1, 1, true, "rotate(90, 10, 20)");
|
||||
checkSlotDirection(1, 2, true, "rotate(90, 10, 20)");
|
||||
checkSlotDirection(1, 3, true, "rotate(270, 10, 20)");
|
||||
checkSlotDirection(1, 4, true, "rotate(270, 10, 20)");
|
||||
checkSlotDirection(2, 3, true, "rotate(90, 10, 20)");
|
||||
checkSlotDirection(3, 1, true, "rotate(180, 10, 20)");
|
||||
checkSlotDirection(3, 2, true, "rotate(0, 10, 20)");
|
||||
checkSlotDirection(3, 3, true, "rotate(0, 10, 20)");
|
||||
checkSlotDirection(3, 4, true, "rotate(180, 10, 20)");
|
||||
checkSlotDirection(4, 3, true, "rotate(180, 10, 20)");
|
||||
});
|
||||
|
||||
describe("<Tool/>", () => {
|
||||
|
@ -58,7 +76,7 @@ describe("<Tool/>", () => {
|
|||
};
|
||||
|
||||
it("renders standard tool styling", () => {
|
||||
const wrapper = mount(<Tool {...fakeProps() } />);
|
||||
const wrapper = mount(<Tool {...fakeProps()} />);
|
||||
const props = wrapper.find("circle").last().props();
|
||||
expect(props.r).toEqual(35);
|
||||
expect(props.cx).toEqual(10);
|
||||
|
@ -69,7 +87,7 @@ describe("<Tool/>", () => {
|
|||
it("tool hover", () => {
|
||||
const p = fakeProps();
|
||||
p.toolProps.hovered = true;
|
||||
const wrapper = mount(<Tool {...p } />);
|
||||
const wrapper = mount(<Tool {...p} />);
|
||||
const props = wrapper.find("circle").last().props();
|
||||
expect(props.fill).toEqual(Color.darkGray);
|
||||
});
|
||||
|
@ -77,7 +95,7 @@ describe("<Tool/>", () => {
|
|||
it("renders special tool styling: bin", () => {
|
||||
const p = fakeProps();
|
||||
p.tool = "seedBin";
|
||||
const wrapper = mount(<Tool {...p } />);
|
||||
const wrapper = mount(<Tool {...p} />);
|
||||
const elements = wrapper.find("#seed-bin").find("circle");
|
||||
expect(elements.length).toEqual(2);
|
||||
expect(elements.last().props().fill).toEqual("url(#SeedBinGradient)");
|
||||
|
@ -87,7 +105,7 @@ describe("<Tool/>", () => {
|
|||
const p = fakeProps();
|
||||
p.tool = "seedBin";
|
||||
p.toolProps.hovered = true;
|
||||
const wrapper = mount(<Tool {...p } />);
|
||||
const wrapper = mount(<Tool {...p} />);
|
||||
p.toolProps.hovered = true;
|
||||
expect(wrapper.find("#seed-bin").find("circle").length).toEqual(3);
|
||||
});
|
||||
|
@ -95,7 +113,7 @@ describe("<Tool/>", () => {
|
|||
it("renders special tool styling: tray", () => {
|
||||
const p = fakeProps();
|
||||
p.tool = "seedTray";
|
||||
const wrapper = mount(<Tool {...p } />);
|
||||
const wrapper = mount(<Tool {...p} />);
|
||||
const elements = wrapper.find("#seed-tray");
|
||||
expect(elements.find("circle").length).toEqual(2);
|
||||
expect(elements.find("rect").length).toEqual(1);
|
||||
|
@ -106,7 +124,7 @@ describe("<Tool/>", () => {
|
|||
const p = fakeProps();
|
||||
p.tool = "seedTray";
|
||||
p.toolProps.hovered = true;
|
||||
const wrapper = mount(<Tool {...p } />);
|
||||
const wrapper = mount(<Tool {...p} />);
|
||||
p.toolProps.hovered = true;
|
||||
expect(wrapper.find("#seed-tray").find("circle").length).toEqual(3);
|
||||
});
|
||||
|
|
|
@ -7,30 +7,46 @@ describe("textAnchorPosition()", () => {
|
|||
const MIDDLE_BOTTOM = { anchor: "middle", x: 0, y: -40 };
|
||||
|
||||
it("returns correct label position: positive x", () => {
|
||||
expect(textAnchorPosition(1, 1)).toEqual(END);
|
||||
expect(textAnchorPosition(1, 2)).toEqual(START);
|
||||
expect(textAnchorPosition(1, 3)).toEqual(START);
|
||||
expect(textAnchorPosition(1, 4)).toEqual(END);
|
||||
expect(textAnchorPosition(1, 1, false)).toEqual(END);
|
||||
expect(textAnchorPosition(1, 2, false)).toEqual(START);
|
||||
expect(textAnchorPosition(1, 3, false)).toEqual(START);
|
||||
expect(textAnchorPosition(1, 4, false)).toEqual(END);
|
||||
expect(textAnchorPosition(1, 1, true)).toEqual(MIDDLE_TOP);
|
||||
expect(textAnchorPosition(1, 2, true)).toEqual(MIDDLE_TOP);
|
||||
expect(textAnchorPosition(1, 3, true)).toEqual(MIDDLE_BOTTOM);
|
||||
expect(textAnchorPosition(1, 4, true)).toEqual(MIDDLE_BOTTOM);
|
||||
});
|
||||
|
||||
it("returns correct label position: negative x", () => {
|
||||
expect(textAnchorPosition(2, 1)).toEqual(START);
|
||||
expect(textAnchorPosition(2, 2)).toEqual(END);
|
||||
expect(textAnchorPosition(2, 3)).toEqual(END);
|
||||
expect(textAnchorPosition(2, 4)).toEqual(START);
|
||||
expect(textAnchorPosition(2, 1, false)).toEqual(START);
|
||||
expect(textAnchorPosition(2, 2, false)).toEqual(END);
|
||||
expect(textAnchorPosition(2, 3, false)).toEqual(END);
|
||||
expect(textAnchorPosition(2, 4, false)).toEqual(START);
|
||||
expect(textAnchorPosition(2, 1, true)).toEqual(MIDDLE_BOTTOM);
|
||||
expect(textAnchorPosition(2, 2, true)).toEqual(MIDDLE_BOTTOM);
|
||||
expect(textAnchorPosition(2, 3, true)).toEqual(MIDDLE_TOP);
|
||||
expect(textAnchorPosition(2, 4, true)).toEqual(MIDDLE_TOP);
|
||||
});
|
||||
|
||||
it("returns correct label position: positive y", () => {
|
||||
expect(textAnchorPosition(3, 1)).toEqual(MIDDLE_TOP);
|
||||
expect(textAnchorPosition(3, 2)).toEqual(MIDDLE_TOP);
|
||||
expect(textAnchorPosition(3, 3)).toEqual(MIDDLE_BOTTOM);
|
||||
expect(textAnchorPosition(3, 4)).toEqual(MIDDLE_BOTTOM);
|
||||
expect(textAnchorPosition(3, 1, false)).toEqual(MIDDLE_TOP);
|
||||
expect(textAnchorPosition(3, 2, false)).toEqual(MIDDLE_TOP);
|
||||
expect(textAnchorPosition(3, 3, false)).toEqual(MIDDLE_BOTTOM);
|
||||
expect(textAnchorPosition(3, 4, false)).toEqual(MIDDLE_BOTTOM);
|
||||
expect(textAnchorPosition(3, 1, true)).toEqual(END);
|
||||
expect(textAnchorPosition(3, 2, true)).toEqual(START);
|
||||
expect(textAnchorPosition(3, 3, true)).toEqual(START);
|
||||
expect(textAnchorPosition(3, 4, true)).toEqual(END);
|
||||
});
|
||||
|
||||
it("returns correct label position: negative y", () => {
|
||||
expect(textAnchorPosition(4, 1)).toEqual(MIDDLE_BOTTOM);
|
||||
expect(textAnchorPosition(4, 2)).toEqual(MIDDLE_BOTTOM);
|
||||
expect(textAnchorPosition(4, 3)).toEqual(MIDDLE_TOP);
|
||||
expect(textAnchorPosition(4, 4)).toEqual(MIDDLE_TOP);
|
||||
expect(textAnchorPosition(4, 1, false)).toEqual(MIDDLE_BOTTOM);
|
||||
expect(textAnchorPosition(4, 2, false)).toEqual(MIDDLE_BOTTOM);
|
||||
expect(textAnchorPosition(4, 3, false)).toEqual(MIDDLE_TOP);
|
||||
expect(textAnchorPosition(4, 4, false)).toEqual(MIDDLE_TOP);
|
||||
expect(textAnchorPosition(4, 1, true)).toEqual(START);
|
||||
expect(textAnchorPosition(4, 2, true)).toEqual(END);
|
||||
expect(textAnchorPosition(4, 3, true)).toEqual(END);
|
||||
expect(textAnchorPosition(4, 4, true)).toEqual(START);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,13 +2,12 @@ import * as React from "react";
|
|||
import { ToolSlotPoint, TSPProps } from "../tool_slot_point";
|
||||
import { mount } from "enzyme";
|
||||
import { fakeToolSlot, fakeTool } from "../../../__test_support__/fake_state/resources";
|
||||
import { fakeMapTransformProps } from "../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<ToolSlotPoint/>", () => {
|
||||
function fakeProps(): TSPProps {
|
||||
return {
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
slot: { toolSlot: fakeToolSlot(), tool: fakeTool() }
|
||||
};
|
||||
}
|
||||
|
@ -42,28 +41,28 @@ describe("<ToolSlotPoint/>", () => {
|
|||
it("displays 'no tool'", () => {
|
||||
const p = fakeProps();
|
||||
p.slot.tool = undefined;
|
||||
const wrapper = mount(<ToolSlotPoint {...p } />);
|
||||
const wrapper = mount(<ToolSlotPoint {...p} />);
|
||||
wrapper.setState({ hovered: true });
|
||||
expect(wrapper.find("text").text()).toEqual("no tool");
|
||||
expect(wrapper.find("text").props().dx).toEqual(40);
|
||||
});
|
||||
|
||||
it("doesn't display tool name", () => {
|
||||
const wrapper = mount(<ToolSlotPoint {...fakeProps() } />);
|
||||
const wrapper = mount(<ToolSlotPoint {...fakeProps()} />);
|
||||
expect(wrapper.find("text").props().visibility).toEqual("hidden");
|
||||
});
|
||||
|
||||
it("renders bin", () => {
|
||||
const p = fakeProps();
|
||||
if (p.slot.tool) { p.slot.tool.body.name = "seed bin"; }
|
||||
const wrapper = mount(<ToolSlotPoint {...p } />);
|
||||
const wrapper = mount(<ToolSlotPoint {...p} />);
|
||||
expect(wrapper.find("#SeedBinGradient").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("renders tray", () => {
|
||||
const p = fakeProps();
|
||||
if (p.slot.tool) { p.slot.tool.body.name = "seed tray"; }
|
||||
const wrapper = mount(<ToolSlotPoint {...p } />);
|
||||
const wrapper = mount(<ToolSlotPoint {...p} />);
|
||||
expect(wrapper.find("#SeedTrayPattern").length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { McuParams } from "farmbot";
|
||||
import { AxisNumberProperty, BotSize, MapTransformProps } from "../interfaces";
|
||||
import { StepsPerMmXY } from "../../../devices/interfaces";
|
||||
import { fakeMapTransformProps } from "../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("Utils", () => {
|
||||
it("rounds a number", () => {
|
||||
|
@ -18,29 +19,66 @@ describe("Utils", () => {
|
|||
});
|
||||
|
||||
describe("translateScreenToGarden()", () => {
|
||||
it("translates garden coords to screen coords: corner case", () => {
|
||||
const cornerCase = translateScreenToGarden({
|
||||
mapTransformProps: { quadrant: 2, gridSize: { x: 3000, y: 1500 } },
|
||||
it("translates screen coords to garden coords: zoomLvl = 1", () => {
|
||||
const result = translateScreenToGarden({
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
page: { x: 520, y: 212 },
|
||||
scroll: { left: 0, top: 0 },
|
||||
scroll: { left: 10, top: 20 },
|
||||
zoomLvl: 1,
|
||||
gridOffset: { x: 0, y: 0 },
|
||||
gridOffset: { x: 30, y: 40 },
|
||||
});
|
||||
expect(cornerCase.x).toEqual(200);
|
||||
expect(cornerCase.y).toEqual(100);
|
||||
expect(result).toEqual({ x: 180, y: 80 });
|
||||
});
|
||||
|
||||
it("translates garden coords to screen coords: edge case", () => {
|
||||
const edgeCase = translateScreenToGarden({
|
||||
mapTransformProps: { quadrant: 2, gridSize: { x: 3000, y: 1500 } },
|
||||
it("translates screen coords to garden coords: zoomLvl < 1", () => {
|
||||
const result = translateScreenToGarden({
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
page: { x: 1132, y: 382 },
|
||||
scroll: { left: 0, top: 0 },
|
||||
zoomLvl: 0.3,
|
||||
gridOffset: { x: 0, y: 0 },
|
||||
scroll: { left: 10, top: 20 },
|
||||
zoomLvl: 0.33,
|
||||
gridOffset: { x: 30, y: 40 },
|
||||
});
|
||||
expect(result).toEqual({ x: 2470, y: 840 });
|
||||
});
|
||||
|
||||
expect(Math.round(edgeCase.x)).toEqual(2710);
|
||||
expect(Math.round(edgeCase.y)).toEqual(910);
|
||||
it("translates screen coords to garden coords: zoomLvl > 1", () => {
|
||||
const result = translateScreenToGarden({
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
page: { x: 1132, y: 382 },
|
||||
scroll: { left: 10, top: 20 },
|
||||
zoomLvl: 1.5,
|
||||
gridOffset: { x: 30, y: 40 },
|
||||
});
|
||||
expect(result).toEqual({ x: 520, y: 150 });
|
||||
});
|
||||
|
||||
it("translates screen coords to garden coords: other case", () => {
|
||||
const fakeMTP = fakeMapTransformProps();
|
||||
fakeMTP.quadrant = 3;
|
||||
fakeMTP.gridSize = { x: 300, y: 150 };
|
||||
const result = translateScreenToGarden({
|
||||
mapTransformProps: fakeMTP,
|
||||
page: { x: 332, y: 132 },
|
||||
scroll: { left: 10, top: 20 },
|
||||
zoomLvl: 0.75,
|
||||
gridOffset: { x: 30, y: 40 },
|
||||
});
|
||||
expect(result).toEqual({ x: 0, y: 130 });
|
||||
});
|
||||
|
||||
it("translates screen coords to garden coords: swapped X&Y", () => {
|
||||
const fakeMTP = fakeMapTransformProps();
|
||||
fakeMTP.xySwap = true;
|
||||
fakeMTP.quadrant = 3;
|
||||
fakeMTP.gridSize = { x: 150, y: 300 };
|
||||
const result = translateScreenToGarden({
|
||||
mapTransformProps: fakeMTP,
|
||||
page: { x: 332, y: 132 },
|
||||
scroll: { left: 10, top: 20 },
|
||||
zoomLvl: 0.75,
|
||||
gridOffset: { x: 30, y: 40 },
|
||||
});
|
||||
expect(result).toEqual({ x: 130, y: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -147,65 +185,89 @@ describe("getbotSize()", () => {
|
|||
describe("getMapSize()", () => {
|
||||
it("calculates map size", () => {
|
||||
const mapSize = getMapSize(
|
||||
{ x: 2000, y: 1000 },
|
||||
fakeMapTransformProps(),
|
||||
{ x: 100, y: 50 });
|
||||
expect(mapSize).toEqual({ x: 2200, y: 1100 });
|
||||
expect(mapSize).toEqual({ h: 1600, w: 3200 });
|
||||
});
|
||||
|
||||
it("calculates map size: X&Y Swapped", () => {
|
||||
const fakeMPT = fakeMapTransformProps();
|
||||
fakeMPT.xySwap = true;
|
||||
const mapSize = getMapSize(
|
||||
fakeMPT,
|
||||
{ x: 100, y: 50 });
|
||||
expect(mapSize).toEqual({ h: 3200, w: 1600 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformXY", () => {
|
||||
const gridSize = { x: 2000, y: 1000 };
|
||||
const mapTransformProps = fakeMapTransformProps();
|
||||
mapTransformProps.gridSize = { x: 2000, y: 1000 };
|
||||
|
||||
type QXY = { qx: number, qy: number };
|
||||
|
||||
const transformCheck =
|
||||
(original: QXY, transformed: QXY, transformProps: MapTransformProps) => {
|
||||
transformProps.xySwap = false;
|
||||
expect(transformXY(original.qx, original.qy, transformProps))
|
||||
.toEqual(transformed);
|
||||
expect(transformXY(transformed.qx, transformed.qy, transformProps))
|
||||
.toEqual(original);
|
||||
transformProps.xySwap = true;
|
||||
const transformedYX = { qx: transformed.qy, qy: transformed.qx };
|
||||
expect(transformXY(original.qx, original.qy, transformProps))
|
||||
.toEqual(transformedYX);
|
||||
expect(transformXY(transformed.qx, transformed.qy, transformProps))
|
||||
.toEqual({ qx: original.qy, qy: original.qx });
|
||||
};
|
||||
|
||||
it("calculates transformed coordinate: quadrant 2", () => {
|
||||
const original = { qx: 100, qy: 200 };
|
||||
const transformed = { qx: 100, qy: 200 };
|
||||
const transformProps = { quadrant: 2, gridSize };
|
||||
transformCheck(original, transformed, transformProps);
|
||||
mapTransformProps.quadrant = 2;
|
||||
transformCheck(original, transformed, mapTransformProps);
|
||||
});
|
||||
|
||||
it("calculates transformed coordinate: quadrant 4", () => {
|
||||
const original = { qx: 100, qy: 200 };
|
||||
const transformed = { qx: 1900, qy: 800 };
|
||||
const transformProps = { quadrant: 4, gridSize };
|
||||
transformCheck(original, transformed, transformProps);
|
||||
mapTransformProps.quadrant = 4;
|
||||
transformCheck(original, transformed, mapTransformProps);
|
||||
});
|
||||
|
||||
it("calculates transformed coordinate: quadrant 4 (outside of grid)", () => {
|
||||
const original = { qx: 2200, qy: 1100 };
|
||||
const transformed = { qx: -200, qy: -100 };
|
||||
const transformProps = { quadrant: 4, gridSize };
|
||||
transformCheck(original, transformed, transformProps);
|
||||
mapTransformProps.quadrant = 4;
|
||||
transformCheck(original, transformed, mapTransformProps);
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformForQuadrant()", () => {
|
||||
const mapTransformProps = fakeMapTransformProps();
|
||||
mapTransformProps.gridSize = { x: 200, y: 100 };
|
||||
|
||||
it("calculates transform for quadrant 1", () => {
|
||||
expect(transformForQuadrant({ quadrant: 1, gridSize: { x: 200, y: 100 } }))
|
||||
mapTransformProps.quadrant = 1;
|
||||
expect(transformForQuadrant(mapTransformProps))
|
||||
.toEqual("scale(-1, 1) translate(-200, 0)");
|
||||
});
|
||||
|
||||
it("calculates transform for quadrant 2", () => {
|
||||
expect(transformForQuadrant({ quadrant: 2, gridSize: { x: 200, y: 100 } }))
|
||||
mapTransformProps.quadrant = 2;
|
||||
expect(transformForQuadrant(mapTransformProps))
|
||||
.toEqual("scale(1, 1) translate(0, 0)");
|
||||
});
|
||||
|
||||
it("calculates transform for quadrant 3", () => {
|
||||
expect(transformForQuadrant({ quadrant: 3, gridSize: { x: 200, y: 100 } }))
|
||||
mapTransformProps.quadrant = 3;
|
||||
expect(transformForQuadrant(mapTransformProps))
|
||||
.toEqual("scale(1, -1) translate(0, -100)");
|
||||
});
|
||||
|
||||
it("calculates transform for quadrant 4", () => {
|
||||
expect(transformForQuadrant({ quadrant: 4, gridSize: { x: 200, y: 100 } }))
|
||||
mapTransformProps.quadrant = 4;
|
||||
expect(transformForQuadrant(mapTransformProps))
|
||||
.toEqual("scale(-1, -1) translate(-200, -100)");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ import { BotExtentsProps } from "./interfaces";
|
|||
|
||||
export function BotExtents(props: BotExtentsProps) {
|
||||
const { stopAtHome, botSize, mapTransformProps } = props;
|
||||
const { xySwap } = mapTransformProps;
|
||||
const homeLength = transformXY(
|
||||
botSize.x.value, botSize.y.value, mapTransformProps);
|
||||
const homeZero = transformXY(2, 2, mapTransformProps);
|
||||
|
@ -27,12 +28,12 @@ export function BotExtents(props: BotExtentsProps) {
|
|||
}
|
||||
</g>
|
||||
<g id="max-lines">
|
||||
{!botSize.x.isDefault &&
|
||||
{(xySwap ? !botSize.y.isDefault : !botSize.x.isDefault) &&
|
||||
<line
|
||||
x1={homeLength.qx} y1={homeZero.qy}
|
||||
x2={homeLength.qx} y2={homeLength.qy} />
|
||||
}
|
||||
{!botSize.y.isDefault &&
|
||||
{(xySwap ? !botSize.x.isDefault : !botSize.y.isDefault) &&
|
||||
<line
|
||||
x1={homeZero.qx} y1={homeLength.qy}
|
||||
x2={homeLength.qx} y2={homeLength.qy} />
|
||||
|
|
|
@ -55,7 +55,7 @@ export function DragHelpers(props: DragHelpersProps) {
|
|||
const {
|
||||
dragging, plant, zoomLvl, activeDragXY, mapTransformProps, plantAreaOffset
|
||||
} = props;
|
||||
const mapSize = getMapSize(mapTransformProps.gridSize, plantAreaOffset);
|
||||
const mapSize = getMapSize(mapTransformProps, plantAreaOffset);
|
||||
const { radius, x, y } = plant.body;
|
||||
|
||||
const scale = 1 + Math.round(15 * (1.8 - zoomLvl)) / 10; // scale factor
|
||||
|
@ -71,8 +71,8 @@ export function DragHelpers(props: DragHelpersProps) {
|
|||
</text>}
|
||||
{dragging && // Active plant
|
||||
<g id="long-crosshair">
|
||||
<rect x={qx - 0.5} y={-plantAreaOffset.y} width={1} height={mapSize.y} />
|
||||
<rect x={-plantAreaOffset.x} y={qy - 0.5} width={mapSize.x} height={1} />
|
||||
<rect x={qx - 0.5} y={-plantAreaOffset.y} width={1} height={mapSize.h} />
|
||||
<rect x={-plantAreaOffset.x} y={qy - 0.5} width={mapSize.w} height={1} />
|
||||
</g>}
|
||||
{dragging && // Active plant
|
||||
<g id="short-crosshair">
|
||||
|
|
|
@ -3,15 +3,14 @@ import { shallow, mount } from "enzyme";
|
|||
import { Bugs, BugsProps, showBugResetButton, showBugs, resetBugs } from "../bugs";
|
||||
import { EggKeys, setEggStatus, getEggStatus } from "../status";
|
||||
import { range } from "lodash";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
|
||||
const expectAlive = (value: string) =>
|
||||
expect(getEggStatus(EggKeys.BUGS_ARE_STILL_ALIVE)).toEqual(value);
|
||||
|
||||
describe("<Bugs />", () => {
|
||||
const fakeProps = (): BugsProps => ({
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
botSize: {
|
||||
x: { value: 3000, isDefault: true },
|
||||
y: { value: 1500, isDefault: true }
|
||||
|
|
|
@ -36,9 +36,7 @@ import { isNumber } from "lodash";
|
|||
import { TargetCoordinate } from "./target_coordinate";
|
||||
import { DrawnPoint } from "./drawn_point";
|
||||
import { Bugs, showBugs } from "./easter_eggs/bugs";
|
||||
|
||||
const DRAG_ERROR = `ERROR - Couldn't get zoom level of garden map, check the
|
||||
handleDrop() or drag() method in garden_map.tsx`;
|
||||
import { BooleanSetting } from "../../session_keys";
|
||||
|
||||
export enum Mode {
|
||||
none = "none",
|
||||
|
@ -73,7 +71,8 @@ export class GardenMap extends
|
|||
get mapTransformProps(): MapTransformProps {
|
||||
return {
|
||||
quadrant: this.props.botOriginQuadrant,
|
||||
gridSize: this.props.gridSize
|
||||
gridSize: this.props.gridSize,
|
||||
xySwap: !!this.props.getConfigValue(BooleanSetting.xy_swap),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -113,7 +112,7 @@ export class GardenMap extends
|
|||
const map = document.querySelector(".farm-designer-map");
|
||||
const page = document.querySelector(".farm-designer");
|
||||
if (el && map && page) {
|
||||
const zoomLvl = parseFloat(window.getComputedStyle(map).zoom || DRAG_ERROR);
|
||||
const zoomLvl = parseFloat(window.getComputedStyle(map).zoom || "1");
|
||||
const params: ScreenToGardenParams = {
|
||||
page: { x: e.pageX, y: e.pageY },
|
||||
scroll: { left: page.scrollLeft, top: map.scrollTop * zoomLvl },
|
||||
|
@ -264,16 +263,19 @@ export class GardenMap extends
|
|||
const plant = this.getPlant();
|
||||
const map = document.querySelector(".farm-designer-map");
|
||||
const { gridSize } = this.props;
|
||||
const { quadrant, xySwap } = this.mapTransformProps;
|
||||
if (this.state.isDragging && plant && map) {
|
||||
const zoomLvl = parseFloat(window.getComputedStyle(map).zoom || DRAG_ERROR);
|
||||
const zoomLvl = parseFloat(window.getComputedStyle(map).zoom || "1");
|
||||
const { qx, qy } = transformXY(e.pageX, e.pageY, this.mapTransformProps);
|
||||
const deltaX = Math.round((qx - (this.state.pageX || qx)) / zoomLvl);
|
||||
const deltaY = Math.round((qy - (this.state.pageY || qy)) / zoomLvl);
|
||||
const dX = xySwap && (quadrant % 2 === 1) ? -deltaX : deltaX;
|
||||
const dY = xySwap && (quadrant % 2 === 1) ? -deltaY : deltaY;
|
||||
this.setState({
|
||||
pageX: qx, pageY: qy,
|
||||
activeDragXY: { x: plant.body.x + deltaX, y: plant.body.y + deltaY, z: 0 }
|
||||
activeDragXY: { x: plant.body.x + dX, y: plant.body.y + dY, z: 0 }
|
||||
});
|
||||
this.props.dispatch(movePlant({ deltaX, deltaY, plant, gridSize }));
|
||||
this.props.dispatch(movePlant({ deltaX: dX, deltaY: dY, plant, gridSize }));
|
||||
}
|
||||
break;
|
||||
case Mode.boxSelect:
|
||||
|
@ -313,13 +315,14 @@ export class GardenMap extends
|
|||
|
||||
render() {
|
||||
const { gridSize } = this.props;
|
||||
const mapSize = getMapSize(gridSize, this.props.gridOffset);
|
||||
const mapSize = getMapSize(this.mapTransformProps, this.props.gridOffset);
|
||||
const mapTransformProps = this.mapTransformProps;
|
||||
const { xySwap } = mapTransformProps;
|
||||
return <div
|
||||
className="drop-area"
|
||||
style={{
|
||||
height: mapSize.y + "px", maxHeight: mapSize.y + "px",
|
||||
width: mapSize.x + "px", maxWidth: mapSize.x + "px"
|
||||
height: mapSize.h + "px", maxHeight: mapSize.h + "px",
|
||||
width: mapSize.w + "px", maxWidth: mapSize.w + "px"
|
||||
}}
|
||||
onDrop={this.handleDrop}
|
||||
onDragEnter={this.handleDragEnter}
|
||||
|
@ -335,7 +338,8 @@ export class GardenMap extends
|
|||
<svg
|
||||
id="drop-area-svg"
|
||||
x={this.props.gridOffset.x} y={this.props.gridOffset.y}
|
||||
width={gridSize.x} height={gridSize.y}
|
||||
width={xySwap ? gridSize.y : gridSize.x}
|
||||
height={xySwap ? gridSize.x : gridSize.y}
|
||||
onMouseUp={this.endDrag}
|
||||
onMouseDown={this.startDrag}
|
||||
onMouseMove={this.drag}
|
||||
|
|
|
@ -6,7 +6,9 @@ import { Color } from "../../ui/index";
|
|||
|
||||
export function Grid(props: GridProps) {
|
||||
const { mapTransformProps } = props;
|
||||
const { gridSize } = mapTransformProps;
|
||||
const { gridSize, xySwap } = mapTransformProps;
|
||||
const gridSizeW = xySwap ? gridSize.y : gridSize.x;
|
||||
const gridSizeH = xySwap ? gridSize.x : gridSize.y;
|
||||
const origin = transformXY(0, 0, mapTransformProps);
|
||||
const arrowEnd = transformXY(25, 25, mapTransformProps);
|
||||
const xLabel = transformXY(15, -10, mapTransformProps);
|
||||
|
@ -34,10 +36,10 @@ export function Grid(props: GridProps) {
|
|||
|
||||
<g id="grid">
|
||||
<rect id="minor-grid"
|
||||
width={gridSize.x} height={gridSize.y} fill="url(#minor_grid)" />
|
||||
width={gridSizeW} height={gridSizeH} fill="url(#minor_grid)" />
|
||||
<rect id="major-grid" transform={transformForQuadrant(mapTransformProps)}
|
||||
width={gridSize.x} height={gridSize.y} fill="url(#major_grid)" />
|
||||
<rect id="border" width={gridSize.x} height={gridSize.y} fill="none"
|
||||
width={gridSizeW} height={gridSizeH} fill="url(#major_grid)" />
|
||||
<rect id="border" width={gridSizeW} height={gridSizeH} fill="none"
|
||||
stroke="rgba(0,0,0,0.3)" strokeWidth={2} />
|
||||
</g>
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ export interface GardenMapLegendProps {
|
|||
export type MapTransformProps = {
|
||||
quadrant: BotOriginQuadrant,
|
||||
gridSize: AxisNumberProperty
|
||||
xySwap: boolean;
|
||||
};
|
||||
|
||||
export interface GardenPlantProps {
|
||||
|
|
|
@ -2,15 +2,14 @@ import * as React from "react";
|
|||
import { DragHelperLayer, DragHelperLayerProps } from "../drag_helper_layer";
|
||||
import { shallow } from "enzyme";
|
||||
import { fakePlant } from "../../../../__test_support__/fake_state/resources";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<DragHelperLayer/>", () => {
|
||||
function fakeProps(): DragHelperLayerProps {
|
||||
return {
|
||||
currentPlant: fakePlant(),
|
||||
editing: true,
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
dragging: true,
|
||||
zoomLvl: 1.8,
|
||||
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
||||
|
@ -20,7 +19,7 @@ describe("<DragHelperLayer/>", () => {
|
|||
|
||||
it("shows drag helpers", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<DragHelperLayer {...p } />);
|
||||
const wrapper = shallow(<DragHelperLayer {...p} />);
|
||||
["drag-helpers",
|
||||
"coordinates-tooltip",
|
||||
"long-crosshair",
|
||||
|
@ -31,7 +30,7 @@ describe("<DragHelperLayer/>", () => {
|
|||
it("doesn't show drag helpers", () => {
|
||||
const p = fakeProps();
|
||||
p.editing = false;
|
||||
const wrapper = shallow(<DragHelperLayer {...p } />);
|
||||
const wrapper = shallow(<DragHelperLayer {...p} />);
|
||||
expect(wrapper.html()).toEqual("<g id=\"drag-helper-layer\"></g>");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as React from "react";
|
|||
import { FarmBotLayer } from "../farmbot_layer";
|
||||
import { shallow } from "enzyme";
|
||||
import { FarmBotLayerProps } from "../../interfaces";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<FarmBotLayer/>", () => {
|
||||
function fakeProps(): FarmBotLayerProps {
|
||||
|
@ -12,9 +13,7 @@ describe("<FarmBotLayer/>", () => {
|
|||
scaled_encoders: { x: undefined, y: undefined, z: undefined },
|
||||
raw_encoders: { x: undefined, y: undefined, z: undefined },
|
||||
},
|
||||
mapTransformProps: {
|
||||
quadrant: 1, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
stopAtHome: { x: true, y: true },
|
||||
botSize: {
|
||||
x: { value: 3000, isDefault: true },
|
||||
|
@ -28,7 +27,7 @@ describe("<FarmBotLayer/>", () => {
|
|||
|
||||
it("shows layer elements", () => {
|
||||
const p = fakeProps();
|
||||
const result = shallow(<FarmBotLayer {...p } />);
|
||||
const result = shallow(<FarmBotLayer {...p} />);
|
||||
const layer = result.find("#farmbot-layer");
|
||||
expect(layer.find("#virtual-farmbot")).toBeTruthy();
|
||||
expect(layer.find("#extents")).toBeTruthy();
|
||||
|
@ -37,7 +36,7 @@ describe("<FarmBotLayer/>", () => {
|
|||
it("toggles visibility off", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = false;
|
||||
const result = shallow(<FarmBotLayer {...p } />);
|
||||
const result = shallow(<FarmBotLayer {...p} />);
|
||||
expect(result.html()).toEqual("<g id=\"farmbot-layer\"></g>");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as React from "react";
|
|||
import { HoveredPlantLayer, HoveredPlantLayerProps } from "../hovered_plant_layer";
|
||||
import { shallow } from "enzyme";
|
||||
import { fakePlant } from "../../../../__test_support__/fake_state/resources";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<HoveredPlantLayer/>", () => {
|
||||
function fakeProps(): HoveredPlantLayerProps {
|
||||
|
@ -23,16 +24,14 @@ describe("<HoveredPlantLayer/>", () => {
|
|||
},
|
||||
hoveredPlant: fakePlant(),
|
||||
isEditing: false,
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
}
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
};
|
||||
}
|
||||
|
||||
it("shows hovered plant icon", () => {
|
||||
const p = fakeProps();
|
||||
p.designer.hoveredPlant.icon = "fake icon";
|
||||
const wrapper = shallow(<HoveredPlantLayer {...p } />);
|
||||
const wrapper = shallow(<HoveredPlantLayer {...p} />);
|
||||
const icon = wrapper.find("image").props();
|
||||
expect(icon.visibility).toBeTruthy();
|
||||
expect(icon.opacity).toEqual(1);
|
||||
|
@ -44,7 +43,7 @@ describe("<HoveredPlantLayer/>", () => {
|
|||
const p = fakeProps();
|
||||
p.designer.hoveredPlant.icon = "fake icon";
|
||||
p.currentPlant = fakePlant();
|
||||
const wrapper = shallow(<HoveredPlantLayer {...p } />);
|
||||
const wrapper = shallow(<HoveredPlantLayer {...p} />);
|
||||
expect(wrapper.find("#selected-plant-indicators").length).toEqual(1);
|
||||
expect(wrapper.find("Circle").length).toEqual(1);
|
||||
expect(wrapper.find("Circle").props().selected).toBeTruthy();
|
||||
|
@ -55,7 +54,7 @@ describe("<HoveredPlantLayer/>", () => {
|
|||
|
||||
it("doesn't show hovered plant icon", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<HoveredPlantLayer {...p } />);
|
||||
const wrapper = shallow(<HoveredPlantLayer {...p} />);
|
||||
expect(wrapper.html()).toEqual("<g id=\"hovered-plant-layer\"></g>");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as React from "react";
|
|||
import { ImageLayer, ImageLayerProps } from "../image_layer";
|
||||
import { shallow } from "enzyme";
|
||||
import { fakeImage, fakeWebAppConfig } from "../../../../__test_support__/fake_state/resources";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
|
||||
const mockConfig = fakeWebAppConfig();
|
||||
jest.mock("../../../../resources/selectors", () => {
|
||||
|
@ -19,9 +20,7 @@ describe("<ImageLayer/>", () => {
|
|||
return {
|
||||
visible: true,
|
||||
images: [image],
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
cameraCalibrationData: {
|
||||
offset: { x: "0", y: "0" },
|
||||
origin: "TOP_LEFT",
|
||||
|
|
|
@ -16,15 +16,14 @@ import { PlantLayer } from "../plant_layer";
|
|||
import { shallow } from "enzyme";
|
||||
import { fakePlant } from "../../../../__test_support__/fake_state/resources";
|
||||
import { PlantLayerProps } from "../../interfaces";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<PlantLayer/>", () => {
|
||||
function fakeProps(): PlantLayerProps {
|
||||
return {
|
||||
visible: true,
|
||||
plants: [fakePlant()],
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
currentPlant: undefined,
|
||||
dragging: false,
|
||||
editing: false,
|
||||
|
@ -38,7 +37,7 @@ describe("<PlantLayer/>", () => {
|
|||
|
||||
it("shows plants", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<PlantLayer {...p } />);
|
||||
const wrapper = shallow(<PlantLayer {...p} />);
|
||||
const layer = wrapper.find("#plant-layer");
|
||||
expect(layer.find(".plant-link-wrapper").length).toEqual(1);
|
||||
["soil-cloud",
|
||||
|
@ -55,21 +54,21 @@ describe("<PlantLayer/>", () => {
|
|||
it("toggles visibility off", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = false;
|
||||
const wrapper = shallow(<PlantLayer {...p } />);
|
||||
const wrapper = shallow(<PlantLayer {...p} />);
|
||||
expect(wrapper.html()).toEqual("<g id=\"plant-layer\"></g>");
|
||||
});
|
||||
|
||||
it("is in clickable mode", () => {
|
||||
mockPath = "/app/designer/plants";
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<PlantLayer {...p } />);
|
||||
const wrapper = shallow(<PlantLayer {...p} />);
|
||||
expect(wrapper.find("Link").props().style).toEqual({});
|
||||
});
|
||||
|
||||
it("is in non-clickable mode", () => {
|
||||
mockPath = "/app/designer/plants/select";
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<PlantLayer {...p } />);
|
||||
const wrapper = shallow(<PlantLayer {...p} />);
|
||||
expect(wrapper.find("Link").props().style)
|
||||
.toEqual({ pointerEvents: "none" });
|
||||
});
|
||||
|
|
|
@ -2,21 +2,20 @@ import * as React from "react";
|
|||
import { PointLayer, PointLayerProps } from "../point_layer";
|
||||
import { shallow } from "enzyme";
|
||||
import { fakePoint } from "../../../../__test_support__/fake_state/resources";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<PointLayer/>", () => {
|
||||
function fakeProps(): PointLayerProps {
|
||||
return {
|
||||
visible: true,
|
||||
points: [fakePoint()],
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
}
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
};
|
||||
}
|
||||
|
||||
it("shows points", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<PointLayer {...p } />);
|
||||
const wrapper = shallow(<PointLayer {...p} />);
|
||||
const layer = wrapper.find("#point-layer");
|
||||
expect(layer.find("GardenPoint").html()).toContain("r=\"100\"");
|
||||
});
|
||||
|
@ -24,7 +23,7 @@ describe("<PointLayer/>", () => {
|
|||
it("toggles visibility off", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = false;
|
||||
const wrapper = shallow(<PointLayer {...p } />);
|
||||
const wrapper = shallow(<PointLayer {...p} />);
|
||||
const layer = wrapper.find("#point-layer");
|
||||
expect(layer.find("GardenPoint").length).toEqual(0);
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as React from "react";
|
|||
import { SpreadLayer, SpreadLayerProps } from "../spread_layer";
|
||||
import { shallow } from "enzyme";
|
||||
import { fakePlant } from "../../../../__test_support__/fake_state/resources";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<SpreadLayer/>", () => {
|
||||
function fakeProps(): SpreadLayerProps {
|
||||
|
@ -9,9 +10,7 @@ describe("<SpreadLayer/>", () => {
|
|||
visible: true,
|
||||
plants: [fakePlant()],
|
||||
currentPlant: undefined,
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
dragging: false,
|
||||
zoomLvl: 1.8,
|
||||
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
||||
|
@ -22,7 +21,7 @@ describe("<SpreadLayer/>", () => {
|
|||
|
||||
it("shows spread", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<SpreadLayer {...p } />);
|
||||
const wrapper = shallow(<SpreadLayer {...p} />);
|
||||
const layer = wrapper.find("#spread-layer");
|
||||
expect(layer.find("SpreadCircle").html()).toContain("r=\"125\"");
|
||||
});
|
||||
|
@ -30,7 +29,7 @@ describe("<SpreadLayer/>", () => {
|
|||
it("toggles visibility off", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = false;
|
||||
const wrapper = shallow(<SpreadLayer {...p } />);
|
||||
const wrapper = shallow(<SpreadLayer {...p} />);
|
||||
const layer = wrapper.find("#spread-layer");
|
||||
expect(layer.find("SpreadCircle").length).toEqual(0);
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ jest.mock("../../../../history", () => ({
|
|||
|
||||
import * as React from "react";
|
||||
import { ToolSlotLayer, ToolSlotLayerProps } from "../tool_slot_layer";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
import { fakeResource } from "../../../../__test_support__/fake_resource";
|
||||
import { ToolSlotPointer } from "../../../../interfaces";
|
||||
import { shallow } from "enzyme";
|
||||
|
@ -34,28 +35,26 @@ describe("<ToolSlotLayer/>", () => {
|
|||
return {
|
||||
visible: false,
|
||||
slots: [{ toolSlot, tool: undefined }],
|
||||
mapTransformProps: {
|
||||
quadrant: 1, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
dispatch: jest.fn()
|
||||
};
|
||||
}
|
||||
it("toggles visibility off", () => {
|
||||
const result = shallow(<ToolSlotLayer {...fakeProps() } />);
|
||||
const result = shallow(<ToolSlotLayer {...fakeProps()} />);
|
||||
expect(result.find("ToolSlotPoint").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("toggles visibility on", () => {
|
||||
const p = fakeProps();
|
||||
p.visible = true;
|
||||
const result = shallow(<ToolSlotLayer {...p } />);
|
||||
const result = shallow(<ToolSlotLayer {...p} />);
|
||||
expect(result.find("ToolSlotPoint").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("navigates to tools page", async () => {
|
||||
mockPath = "/app/designer/plants";
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<ToolSlotLayer {...p } />);
|
||||
const wrapper = shallow(<ToolSlotLayer {...p} />);
|
||||
const tools = wrapper.find("g").first();
|
||||
await tools.simulate("click");
|
||||
expect(mockHistory).toHaveBeenCalledWith("/app/tools");
|
||||
|
@ -64,7 +63,7 @@ describe("<ToolSlotLayer/>", () => {
|
|||
it("doesn't navigate to tools page", async () => {
|
||||
mockPath = "/app/designer/plants/1";
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<ToolSlotLayer {...p } />);
|
||||
const wrapper = shallow(<ToolSlotLayer {...p} />);
|
||||
const tools = wrapper.find("g").first();
|
||||
await tools.simulate("click");
|
||||
expect(mockHistory).not.toHaveBeenCalled();
|
||||
|
@ -74,7 +73,7 @@ describe("<ToolSlotLayer/>", () => {
|
|||
it("is in non-clickable mode", () => {
|
||||
mockPath = "/app/designer/plants/select";
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<ToolSlotLayer {...p } />);
|
||||
const wrapper = shallow(<ToolSlotLayer {...p} />);
|
||||
expect(wrapper.find("g").props().style)
|
||||
.toEqual({ pointerEvents: "none" });
|
||||
});
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import * as React from "react";
|
||||
import { MapBackgroundProps } from "./interfaces";
|
||||
import { Color } from "../../ui/index";
|
||||
import { getMapSize } from "./util";
|
||||
|
||||
export function MapBackground(props: MapBackgroundProps) {
|
||||
const { mapTransformProps, plantAreaOffset } = props;
|
||||
const { gridSize } = mapTransformProps;
|
||||
const { gridSize, xySwap } = mapTransformProps;
|
||||
const gridSizeW = xySwap ? gridSize.y : gridSize.x;
|
||||
const gridSizeH = xySwap ? gridSize.x : gridSize.y;
|
||||
const boardWidth = 20;
|
||||
const mapWidth = gridSize.x + plantAreaOffset.x * 2;
|
||||
const mapHeight = gridSize.y + plantAreaOffset.y * 2;
|
||||
const mapSize = getMapSize(mapTransformProps, plantAreaOffset);
|
||||
return <g id="map-background">
|
||||
<defs>
|
||||
<pattern id="diagonalHatch"
|
||||
|
@ -18,16 +20,16 @@ export function MapBackground(props: MapBackgroundProps) {
|
|||
</defs>
|
||||
|
||||
<rect id="bed-border"
|
||||
x={0} y={0} width={mapWidth} height={mapHeight}
|
||||
x={0} y={0} width={mapSize.w} height={mapSize.h}
|
||||
fill={Color.soilBackground} />
|
||||
<rect id="bed-interior" x={boardWidth / 2} y={boardWidth / 2}
|
||||
width={mapWidth - boardWidth} height={mapHeight - boardWidth}
|
||||
width={mapSize.w - boardWidth} height={mapSize.h - boardWidth}
|
||||
stroke="rgba(120,63,4,0.25)" strokeWidth={boardWidth}
|
||||
fill={Color.soilBackground} />
|
||||
<rect id="no-access-perimeter" x={boardWidth} y={boardWidth}
|
||||
width={mapWidth - boardWidth * 2} height={mapHeight - boardWidth * 2}
|
||||
width={mapSize.w - boardWidth * 2} height={mapSize.h - boardWidth * 2}
|
||||
fill="url(#diagonalHatch)" />
|
||||
<rect id="grid-fill" x={plantAreaOffset.x} y={plantAreaOffset.y}
|
||||
width={gridSize.x} height={gridSize.y} fill={Color.gridSoil} />
|
||||
width={gridSizeW} height={gridSizeH} fill={Color.gridSoil} />
|
||||
</g>;
|
||||
}
|
||||
|
|
|
@ -81,11 +81,12 @@ interface TransformProps {
|
|||
qCoords: { qx: number, qy: number };
|
||||
size: { x: number, y: number };
|
||||
imageOrigin: string;
|
||||
xySwap: boolean;
|
||||
}
|
||||
|
||||
/* Image transform string. Flip and place image at the correct map location. */
|
||||
const transform = (props: TransformProps): string => {
|
||||
const { quadrant, qCoords, size, imageOrigin } = props;
|
||||
const { quadrant, qCoords, size, imageOrigin, xySwap } = props;
|
||||
const { qx, qy } = qCoords;
|
||||
const orginAdjusted = originAdjustment(imageOrigin);
|
||||
const quadrantAdjusted = quadrantAdjustment(quadrant);
|
||||
|
@ -101,7 +102,11 @@ const transform = (props: TransformProps): string => {
|
|||
x: round(flip.x * qx + toZero.x, PRECISION),
|
||||
y: round(flip.y * qy + toZero.y, PRECISION)
|
||||
};
|
||||
return `scale(${flip.x}, ${flip.y}) translate(${translate.x}, ${translate.y})`;
|
||||
const xySwapTransform = xySwap
|
||||
? ` rotate(90) scale(${-1}, ${1}) translate(${-size.y}, ${-size.y})`
|
||||
: "";
|
||||
return `scale(${flip.x}, ${flip.y}) translate(${translate.x}, ${translate.y})`
|
||||
+ xySwapTransform;
|
||||
};
|
||||
|
||||
export interface MapImageProps {
|
||||
|
@ -123,7 +128,7 @@ export function MapImage(props: MapImageProps) {
|
|||
const imageOffsetX = parse(offset.x);
|
||||
const imageOffsetY = parse(offset.y);
|
||||
const imageOrigin = origin ? origin.split("\"").join("") : undefined;
|
||||
const { quadrant } = props.mapTransformProps;
|
||||
const { quadrant, xySwap } = props.mapTransformProps;
|
||||
|
||||
/* Check if the image exists. */
|
||||
if (image) {
|
||||
|
@ -144,7 +149,7 @@ export function MapImage(props: MapImageProps) {
|
|||
y: y + imageOffsetY - size.y / 2
|
||||
};
|
||||
const qCoords = transformXY(o.x, o.y, props.mapTransformProps);
|
||||
const transformProps = { quadrant, qCoords, size, imageOrigin };
|
||||
const transformProps = { quadrant, qCoords, size, imageOrigin, xySwap };
|
||||
return <image
|
||||
xlinkHref={imageUrl}
|
||||
height={size.y} width={size.x} x={0} y={0}
|
||||
|
|
|
@ -22,13 +22,16 @@ export interface ToolSlotGraphicProps {
|
|||
y: number;
|
||||
pulloutDirection: ToolPulloutDirection;
|
||||
quadrant: BotOriginQuadrant;
|
||||
xySwap: boolean;
|
||||
}
|
||||
|
||||
const toolbaySlotAngle = (
|
||||
pulloutDirection: ToolPulloutDirection,
|
||||
quadrant: BotOriginQuadrant) => {
|
||||
quadrant: BotOriginQuadrant,
|
||||
xySwap: boolean) => {
|
||||
const rawAngle = () => {
|
||||
switch (pulloutDirection) {
|
||||
const direction = pulloutDirection + (xySwap ? 2 : 0);
|
||||
switch (direction > 4 ? direction % 4 : direction) {
|
||||
case ToolPulloutDirection.POSITIVE_X: return 0;
|
||||
case ToolPulloutDirection.NEGATIVE_X: return 180;
|
||||
case ToolPulloutDirection.NEGATIVE_Y: return 90;
|
||||
|
@ -56,8 +59,8 @@ export enum ToolNames {
|
|||
}
|
||||
|
||||
export const ToolbaySlot = (props: ToolSlotGraphicProps) => {
|
||||
const { id, x, y, pulloutDirection, quadrant } = props;
|
||||
const angle = toolbaySlotAngle(pulloutDirection, quadrant);
|
||||
const { id, x, y, pulloutDirection, quadrant, xySwap } = props;
|
||||
const angle = toolbaySlotAngle(pulloutDirection, quadrant, xySwap);
|
||||
return <g id={"toolbay-slot"}>
|
||||
<defs>
|
||||
<g id={"toolbay-slot-" + id}
|
||||
|
|
|
@ -12,9 +12,11 @@ enum Anchor {
|
|||
|
||||
export const textAnchorPosition = (
|
||||
pulloutDirection: ToolPulloutDirection,
|
||||
quadrant: BotOriginQuadrant): { x: number, y: number, anchor: string } => {
|
||||
quadrant: BotOriginQuadrant,
|
||||
xySwap: boolean): { x: number, y: number, anchor: string } => {
|
||||
const rawAnchor = () => {
|
||||
switch (pulloutDirection) {
|
||||
const direction = pulloutDirection + (xySwap ? 2 : 0);
|
||||
switch (direction > 4 ? direction % 4 : direction) {
|
||||
case ToolPulloutDirection.POSITIVE_X: return Anchor.start;
|
||||
case ToolPulloutDirection.NEGATIVE_X: return Anchor.end;
|
||||
case ToolPulloutDirection.NEGATIVE_Y: return Anchor.middleTop;
|
||||
|
@ -48,11 +50,12 @@ interface ToolLabelProps {
|
|||
y: number;
|
||||
pulloutDirection: ToolPulloutDirection;
|
||||
quadrant: BotOriginQuadrant;
|
||||
xySwap: boolean;
|
||||
}
|
||||
|
||||
export const ToolLabel = (props: ToolLabelProps) => {
|
||||
const { toolName, hovered, x, y, pulloutDirection, quadrant } = props;
|
||||
const labelAnchor = textAnchorPosition(pulloutDirection, quadrant);
|
||||
const { toolName, hovered, x, y, pulloutDirection, quadrant, xySwap } = props;
|
||||
const labelAnchor = textAnchorPosition(pulloutDirection, quadrant, xySwap);
|
||||
|
||||
return <text textAnchor={labelAnchor.anchor}
|
||||
visibility={hovered ? "visible" : "hidden"}
|
||||
|
|
|
@ -36,7 +36,7 @@ export class ToolSlotPoint extends
|
|||
render() {
|
||||
const { id, x, y, pullout_direction } = this.slot.toolSlot.body;
|
||||
const { mapTransformProps } = this.props;
|
||||
const { quadrant } = mapTransformProps;
|
||||
const { quadrant, xySwap } = mapTransformProps;
|
||||
const { qx, qy } = transformXY(x, y, this.props.mapTransformProps);
|
||||
const toolName = this.slot.tool ? this.slot.tool.body.name : "no tool";
|
||||
const toolProps = {
|
||||
|
@ -52,7 +52,8 @@ export class ToolSlotPoint extends
|
|||
x={qx}
|
||||
y={qy}
|
||||
pulloutDirection={pullout_direction}
|
||||
quadrant={quadrant} />}
|
||||
quadrant={quadrant}
|
||||
xySwap={xySwap} />}
|
||||
|
||||
{(this.slot.tool || !pullout_direction) &&
|
||||
<Tool
|
||||
|
@ -65,7 +66,8 @@ export class ToolSlotPoint extends
|
|||
x={qx}
|
||||
y={qy}
|
||||
pulloutDirection={pullout_direction}
|
||||
quadrant={quadrant} />
|
||||
quadrant={quadrant}
|
||||
xySwap={xySwap} />
|
||||
</g>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ export function round(num: number) {
|
|||
* zoom level
|
||||
* quadrant
|
||||
* grid size
|
||||
* XY swap (coming soon)
|
||||
* XY swap
|
||||
*
|
||||
*/
|
||||
|
||||
|
@ -69,18 +69,22 @@ export function translateScreenToGarden(
|
|||
params: ScreenToGardenParams
|
||||
): XYCoordinate {
|
||||
const { page, scroll, zoomLvl, mapTransformProps, gridOffset } = params;
|
||||
const { xySwap } = mapTransformProps;
|
||||
const screenXY = page;
|
||||
const mapXY = ["x", "y"].reduce<XYCoordinate>(
|
||||
(result: XYCoordinate, axis: "x" | "y") => {
|
||||
const unscrolled = screenXY[axis] - scroll[leftOrTop[axis]];
|
||||
const unscrolled = screenXY[axis] + scroll[leftOrTop[axis]];
|
||||
const map = unscrolled - mapPadding[leftOrTop[axis]];
|
||||
const grid = map - gridOffset[axis] * zoomLvl;
|
||||
const unscaled = round(grid / zoomLvl);
|
||||
result[axis] = unscaled;
|
||||
return result;
|
||||
}, { x: 0, y: 0 });
|
||||
const gardenXY = quadTransform({ coordinate: mapXY, mapTransformProps });
|
||||
return gardenXY;
|
||||
const coordinate = xySwap ? { x: mapXY.y, y: mapXY.x } : mapXY;
|
||||
const gardenXY = transformXY(coordinate.x, coordinate.y, mapTransformProps);
|
||||
return xySwap
|
||||
? { x: gardenXY.qy, y: gardenXY.qx }
|
||||
: { x: gardenXY.qx, y: gardenXY.qy };
|
||||
}
|
||||
|
||||
/* BotOriginQuadrant diagram
|
||||
|
@ -112,7 +116,7 @@ interface QuadTransformParams {
|
|||
/** Quadrant coordinate transformation */
|
||||
function quadTransform(params: QuadTransformParams): XYCoordinate {
|
||||
const { coordinate, mapTransformProps } = params;
|
||||
const { quadrant, gridSize } = mapTransformProps;
|
||||
const { gridSize, quadrant } = mapTransformProps;
|
||||
if (isBotOriginQuadrant(quadrant)) {
|
||||
return ["x", "y"].reduce<XYCoordinate>(
|
||||
(result: XYCoordinate, axis: "x" | "y") => {
|
||||
|
@ -146,9 +150,24 @@ function quadTransform(params: QuadTransformParams): XYCoordinate {
|
|||
export function transformXY(
|
||||
x: number,
|
||||
y: number,
|
||||
mapTransformProps: MapTransformProps
|
||||
rawMapTransformProps: MapTransformProps
|
||||
): { qx: number, qy: number } {
|
||||
const transformed = quadTransform({ coordinate: { x, y }, mapTransformProps });
|
||||
const { quadrant, gridSize, xySwap } = rawMapTransformProps;
|
||||
const coordinate = {
|
||||
x: xySwap ? y : x,
|
||||
y: xySwap ? x : y,
|
||||
};
|
||||
const transformed = quadTransform({
|
||||
coordinate,
|
||||
mapTransformProps: {
|
||||
quadrant,
|
||||
gridSize: {
|
||||
x: xySwap ? gridSize.y : gridSize.x,
|
||||
y: xySwap ? gridSize.x : gridSize.y,
|
||||
},
|
||||
xySwap
|
||||
}
|
||||
});
|
||||
return {
|
||||
qx: transformed.x,
|
||||
qy: transformed.y
|
||||
|
@ -184,13 +203,18 @@ export function getBotSize(
|
|||
|
||||
/** Calculate map dimensions */
|
||||
export function getMapSize(
|
||||
gridSize: AxisNumberProperty,
|
||||
mapTransformProps: MapTransformProps,
|
||||
gridOffset: AxisNumberProperty
|
||||
): AxisNumberProperty {
|
||||
return {
|
||||
): { w: number, h: number } {
|
||||
const { gridSize, xySwap } = mapTransformProps;
|
||||
const mapSize = {
|
||||
x: gridSize.x + gridOffset.x * 2,
|
||||
y: gridSize.y + gridOffset.y * 2
|
||||
};
|
||||
return {
|
||||
w: xySwap ? mapSize.y : mapSize.x,
|
||||
h: xySwap ? mapSize.x : mapSize.y
|
||||
};
|
||||
}
|
||||
|
||||
/** Transform object based on selected map quadrant and grid size. */
|
||||
|
|
|
@ -3,36 +3,37 @@ import { shallow } from "enzyme";
|
|||
import { BotOriginQuadrant } from "../../../interfaces";
|
||||
import { BotFigure, BotFigureProps } from "../bot_figure";
|
||||
import { Color } from "../../../../ui/index";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<BotFigure/>", () => {
|
||||
function fakeProps(): BotFigureProps {
|
||||
return {
|
||||
name: "",
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
mapTransformProps: {
|
||||
quadrant: 1, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
plantAreaOffset: { x: 100, y: 100 }
|
||||
};
|
||||
}
|
||||
|
||||
function checkPositionForQuadrant(
|
||||
quadrant: BotOriginQuadrant,
|
||||
xySwap: boolean,
|
||||
expected: { x: number, y: number },
|
||||
name: string,
|
||||
opacity: number) {
|
||||
it(`shows ${name} in correct location for quadrant ${quadrant}`, () => {
|
||||
const p = fakeProps();
|
||||
p.mapTransformProps.quadrant = quadrant;
|
||||
p.mapTransformProps.xySwap = xySwap;
|
||||
p.name = name;
|
||||
const result = shallow(<BotFigure {...p} />);
|
||||
|
||||
const expectedGantryProps = expect.objectContaining({
|
||||
id: "gantry",
|
||||
x: expected.x - 10,
|
||||
y: -100,
|
||||
width: 20,
|
||||
height: 1700,
|
||||
x: xySwap ? -100 : expected.x - 10,
|
||||
y: xySwap ? expected.x - 10 : -100,
|
||||
width: xySwap ? 1700 : 20,
|
||||
height: xySwap ? 20 : 1700,
|
||||
fill: Color.darkGray,
|
||||
fillOpacity: opacity
|
||||
});
|
||||
|
@ -41,8 +42,8 @@ describe("<BotFigure/>", () => {
|
|||
|
||||
const expectedUTMProps = expect.objectContaining({
|
||||
id: "UTM",
|
||||
cx: expected.x,
|
||||
cy: expected.y,
|
||||
cx: xySwap ? expected.y : expected.x,
|
||||
cy: xySwap ? expected.x : expected.y,
|
||||
r: 35,
|
||||
fill: Color.darkGray,
|
||||
fillOpacity: opacity
|
||||
|
@ -52,11 +53,15 @@ describe("<BotFigure/>", () => {
|
|||
});
|
||||
}
|
||||
|
||||
checkPositionForQuadrant(1, { x: 3000, y: 0 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(2, { x: 0, y: 0 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(3, { x: 0, y: 1500 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(4, { x: 3000, y: 1500 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(2, { x: 0, y: 0 }, "encoders", 0.25);
|
||||
checkPositionForQuadrant(1, false, { x: 3000, y: 0 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(2, false, { x: 0, y: 0 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(3, false, { x: 0, y: 1500 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(4, false, { x: 3000, y: 1500 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(1, true, { x: 0, y: 1500 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(2, true, { x: 0, y: 0 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(3, true, { x: 3000, y: 0 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(4, true, { x: 3000, y: 1500 }, "motors", 0.75);
|
||||
checkPositionForQuadrant(2, false, { x: 0, y: 0 }, "encoders", 0.25);
|
||||
|
||||
it("changes location", () => {
|
||||
const p = fakeProps();
|
||||
|
|
|
@ -16,15 +16,14 @@ import * as React from "react";
|
|||
import { shallow } from "enzyme";
|
||||
import { BotPeripheralsProps, BotPeripherals } from "../bot_peripherals";
|
||||
import { BooleanSetting } from "../../../../session_keys";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<BotPeripherals/>", () => {
|
||||
function fakeProps(): BotPeripheralsProps {
|
||||
return {
|
||||
peripherals: [{ label: "", value: false }],
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
plantAreaOffset: { x: 100, y: 100 }
|
||||
};
|
||||
}
|
||||
|
@ -34,7 +33,7 @@ describe("<BotPeripherals/>", () => {
|
|||
const p = fakeProps();
|
||||
p.peripherals[0].label = name;
|
||||
p.peripherals[0].value = false;
|
||||
const wrapper = shallow(<BotPeripherals {...p } />);
|
||||
const wrapper = shallow(<BotPeripherals {...p} />);
|
||||
expect(wrapper.find(`#${name}`).length).toEqual(0);
|
||||
});
|
||||
}
|
||||
|
@ -42,11 +41,11 @@ describe("<BotPeripherals/>", () => {
|
|||
function animationToggle(
|
||||
props: BotPeripheralsProps, enabled: number, disabled: number) {
|
||||
mockStorj[BooleanSetting.disable_animations] = false;
|
||||
const wrapperEnabled = shallow(<BotPeripherals {...props } />);
|
||||
const wrapperEnabled = shallow(<BotPeripherals {...props} />);
|
||||
expect(wrapperEnabled.find("use").length).toEqual(enabled);
|
||||
|
||||
mockStorj[BooleanSetting.disable_animations] = true;
|
||||
const wrapperDisabled = shallow(<BotPeripherals {...props } />);
|
||||
const wrapperDisabled = shallow(<BotPeripherals {...props} />);
|
||||
expect(wrapperDisabled.find("use").length).toEqual(disabled);
|
||||
}
|
||||
|
||||
|
@ -54,19 +53,48 @@ describe("<BotPeripherals/>", () => {
|
|||
const p = fakeProps();
|
||||
p.peripherals[0].label = "lights";
|
||||
p.peripherals[0].value = true;
|
||||
const wrapper = shallow(<BotPeripherals {...p } />);
|
||||
const wrapper = shallow(<BotPeripherals {...p} />);
|
||||
expect(wrapper.find("#lights").length).toEqual(1);
|
||||
expect(wrapper.find("rect").last().props()).toEqual({
|
||||
fill: "url(#LightingGradient)",
|
||||
height: 1700, width: 400, x: 0, y: -100
|
||||
});
|
||||
expect(wrapper.find("use").first().props()).toEqual({
|
||||
xlinkHref: "#light-half",
|
||||
transform: "rotate(0, 0, 750)"
|
||||
});
|
||||
expect(wrapper.find("use").last().props()).toEqual({
|
||||
xlinkHref: "#light-half",
|
||||
transform: "rotate(180, 0, 750)"
|
||||
});
|
||||
});
|
||||
|
||||
it("displays light: X&Y swapped", () => {
|
||||
const p = fakeProps();
|
||||
p.peripherals[0].label = "lights";
|
||||
p.peripherals[0].value = true;
|
||||
p.mapTransformProps.xySwap = true;
|
||||
const wrapper = shallow(<BotPeripherals {...p} />);
|
||||
expect(wrapper.find("#lights").length).toEqual(1);
|
||||
expect(wrapper.find("rect").last().props()).toEqual({
|
||||
fill: "url(#LightingGradient)",
|
||||
height: 1700, width: 400, x: -100, y: 0
|
||||
});
|
||||
expect(wrapper.find("use").first().props()).toEqual({
|
||||
xlinkHref: "#light-half",
|
||||
transform: "rotate(90, 750, 850)"
|
||||
});
|
||||
expect(wrapper.find("use").last().props()).toEqual({
|
||||
xlinkHref: "#light-half",
|
||||
transform: "rotate(270, -100, 0)"
|
||||
});
|
||||
});
|
||||
|
||||
it("displays water", () => {
|
||||
const p = fakeProps();
|
||||
p.peripherals[0].label = "water valve";
|
||||
p.peripherals[0].value = true;
|
||||
const wrapper = shallow(<BotPeripherals {...p } />);
|
||||
const wrapper = shallow(<BotPeripherals {...p} />);
|
||||
expect(wrapper.find("#water").length).toEqual(1);
|
||||
expect(wrapper.find("circle").last().props()).toEqual({
|
||||
cx: 0, cy: 0, fill: "rgb(11, 83, 148)", fillOpacity: 0.2, r: 55
|
||||
|
@ -78,7 +106,7 @@ describe("<BotPeripherals/>", () => {
|
|||
const p = fakeProps();
|
||||
p.peripherals[0].label = "vacuum pump";
|
||||
p.peripherals[0].value = true;
|
||||
const wrapper = shallow(<BotPeripherals {...p } />);
|
||||
const wrapper = shallow(<BotPeripherals {...p} />);
|
||||
expect(wrapper.find("#vacuum").length).toEqual(1);
|
||||
expect(wrapper.find("circle").last().props()).toEqual({
|
||||
fill: "url(#WaveGradient)", cx: 0, cy: 0, r: 100
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { BotTrail, BotTrailProps, VirtualTrail } from "../bot_trail";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<BotTrail/>", () => {
|
||||
function fakeProps(): BotTrailProps {
|
||||
|
@ -12,9 +13,7 @@ describe("<BotTrail/>", () => {
|
|||
{ coord: { x: 4, y: 4 }, water: 20 }]);
|
||||
return {
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
mapTransformProps: {
|
||||
quadrant: 2, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
peripherals: []
|
||||
};
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import { VirtualFarmBot } from "../index";
|
|||
import { shallow } from "enzyme";
|
||||
import { VirtualFarmBotProps } from "../../interfaces";
|
||||
import { Dictionary } from "farmbot";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<VirtualFarmBot/>", () => {
|
||||
function fakeProps(): VirtualFarmBotProps {
|
||||
|
@ -25,9 +26,7 @@ describe("<VirtualFarmBot/>", () => {
|
|||
scaled_encoders: { x: undefined, y: undefined, z: undefined },
|
||||
raw_encoders: { x: undefined, y: undefined, z: undefined },
|
||||
},
|
||||
mapTransformProps: {
|
||||
quadrant: 1, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
plantAreaOffset: { x: 100, y: 100 },
|
||||
peripherals: [],
|
||||
eStopStatus: false
|
||||
|
@ -35,7 +34,7 @@ describe("<VirtualFarmBot/>", () => {
|
|||
}
|
||||
|
||||
it("shows bot position", () => {
|
||||
const wrapper = shallow(<VirtualFarmBot {...fakeProps() } />);
|
||||
const wrapper = shallow(<VirtualFarmBot {...fakeProps()} />);
|
||||
const figures = wrapper.find("BotFigure");
|
||||
expect(figures.length).toEqual(1);
|
||||
expect(figures.last().props().name).toEqual("motor-position");
|
||||
|
@ -43,13 +42,13 @@ describe("<VirtualFarmBot/>", () => {
|
|||
|
||||
it("shows trail", () => {
|
||||
mockStorj["display_trail"] = true;
|
||||
const wrapper = shallow(<VirtualFarmBot {...fakeProps() } />);
|
||||
const wrapper = shallow(<VirtualFarmBot {...fakeProps()} />);
|
||||
expect(wrapper.find("BotTrail").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("shows encoder position", () => {
|
||||
mockStorj["encoder_figure"] = true;
|
||||
const wrapper = shallow(<VirtualFarmBot {...fakeProps() } />);
|
||||
const wrapper = shallow(<VirtualFarmBot {...fakeProps()} />);
|
||||
const figures = wrapper.find("BotFigure");
|
||||
expect(figures.length).toEqual(2);
|
||||
expect(figures.last().props().name).toEqual("encoder-position");
|
||||
|
|
|
@ -3,14 +3,13 @@ import { shallow } from "enzyme";
|
|||
import {
|
||||
NegativePositionLabel, NegativePositionLabelProps
|
||||
} from "../negative_position_labels";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<NegativePositionLabel />", () => {
|
||||
const fakeProps = (): NegativePositionLabelProps => {
|
||||
return {
|
||||
position: { x: 1234, y: undefined, z: undefined },
|
||||
mapTransformProps: {
|
||||
quadrant: 1, gridSize: { x: 3000, y: 1500 }
|
||||
},
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
plantAreaOffset: { x: 100, y: 100 }
|
||||
};
|
||||
};
|
||||
|
|
|
@ -26,18 +26,18 @@ export class BotFigure extends
|
|||
render() {
|
||||
const { name, position, plantAreaOffset, eStopStatus, mapTransformProps
|
||||
} = this.props;
|
||||
const mapSize = getMapSize(mapTransformProps.gridSize, plantAreaOffset);
|
||||
const { xySwap } = mapTransformProps;
|
||||
const mapSize = getMapSize(mapTransformProps, plantAreaOffset);
|
||||
const positionQ = transformXY(
|
||||
(position.x || 0), (position.y || 0), mapTransformProps);
|
||||
const color = eStopStatus ? Color.virtualRed : Color.darkGray;
|
||||
const opacity = name.includes("encoder") ? 0.25 : 0.75;
|
||||
return <g id={name}>
|
||||
<rect id="gantry"
|
||||
x={positionQ.qx - 10}
|
||||
y={-plantAreaOffset.y
|
||||
}
|
||||
width={20}
|
||||
height={mapSize.y}
|
||||
x={xySwap ? -plantAreaOffset.x : positionQ.qx - 10}
|
||||
y={xySwap ? positionQ.qy - 10 : -plantAreaOffset.y}
|
||||
width={xySwap ? mapSize.w : 20}
|
||||
height={xySwap ? 20 : mapSize.h}
|
||||
fillOpacity={opacity}
|
||||
fill={color} />
|
||||
<circle id="UTM"
|
||||
|
|
|
@ -5,6 +5,7 @@ import { BotPosition } from "../../../devices/interfaces";
|
|||
import * as _ from "lodash";
|
||||
import { Session } from "../../../session";
|
||||
import { BooleanSetting } from "../../../session_keys";
|
||||
import { trim } from "../../../util";
|
||||
|
||||
export interface BotPeripheralsProps {
|
||||
position: BotPosition;
|
||||
|
@ -14,8 +15,8 @@ export interface BotPeripheralsProps {
|
|||
}
|
||||
|
||||
function lightsFigure(
|
||||
props: { i: number, x: number, y: number, height: number }) {
|
||||
const { i, x, y, height } = props;
|
||||
props: { i: number, x: number, y: number, height: number, xySwap: boolean }) {
|
||||
const { i, x, y, height, xySwap } = props;
|
||||
const mapHeightMid = height / 2 + y;
|
||||
return <g id="lights" key={`peripheral_${i}`}>
|
||||
<defs>
|
||||
|
@ -34,9 +35,13 @@ function lightsFigure(
|
|||
</defs>
|
||||
|
||||
<use xlinkHref="#light-half"
|
||||
transform={`rotate(0, ${x}, ${mapHeightMid})`} />
|
||||
transform={trim(`rotate(${xySwap ? 90 : 0},
|
||||
${xySwap ? height / 2 + x : x},
|
||||
${mapHeightMid})`)} />
|
||||
<use xlinkHref="#light-half"
|
||||
transform={`rotate(180, ${x}, ${mapHeightMid})`} />
|
||||
transform={trim(`rotate(${xySwap ? 270 : 180},
|
||||
${x},
|
||||
${xySwap ? y : mapHeightMid})`)} />
|
||||
</g>;
|
||||
}
|
||||
|
||||
|
@ -117,7 +122,8 @@ function vacuumFigure(
|
|||
|
||||
export function BotPeripherals(props: BotPeripheralsProps) {
|
||||
const { peripherals, position, plantAreaOffset, mapTransformProps } = props;
|
||||
const mapSize = getMapSize(mapTransformProps.gridSize, plantAreaOffset);
|
||||
const { xySwap } = mapTransformProps;
|
||||
const mapSize = getMapSize(mapTransformProps, plantAreaOffset);
|
||||
const positionQ = transformXY(
|
||||
(position.x || 0), (position.y || 0), mapTransformProps);
|
||||
|
||||
|
@ -126,9 +132,10 @@ export function BotPeripherals(props: BotPeripheralsProps) {
|
|||
if (x.label.toLowerCase().includes("light") && x.value) {
|
||||
return lightsFigure({
|
||||
i,
|
||||
x: positionQ.qx,
|
||||
y: -plantAreaOffset.y,
|
||||
height: mapSize.y
|
||||
x: xySwap ? -plantAreaOffset.y : positionQ.qx,
|
||||
y: xySwap ? positionQ.qy : -plantAreaOffset.y,
|
||||
height: xySwap ? mapSize.w : mapSize.h,
|
||||
xySwap,
|
||||
});
|
||||
} else if (x.label.toLowerCase().includes("water") && x.value) {
|
||||
return waterFigure({
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue