diff --git a/src/include/network/services/routing.rb b/src/include/network/services/routing.rb index a9271459f..8e6d3c81d 100644 --- a/src/include/network/services/routing.rb +++ b/src/include/network/services/routing.rb @@ -28,10 +28,14 @@ # # # Routing configuration dialogs + +require "y2network/dialogs/route" + module Yast module NetworkServicesRoutingInclude include Yast::I18n include Yast::UIShortcuts + include Yast::Logger def initialize_network_services_routing(include_target) Yast.import "UI" @@ -93,158 +97,6 @@ def initialize_network_services_routing(include_target) } end - # Validates user's input obtained from Netmask field - # - # It currently allows to use netmask for IPv4 (e.g. 255.0.0.0) or - # prefix length. If prefix length is used it has to start with '/'. - # For IPv6 network, only prefix length is allowed. - # - # @param [String] netmask or / - def valid_netmask?(netmask) - return false if netmask.nil? || netmask.empty? - return true if Netmask.Check4(netmask) - - if netmask.start_with?("/") - return true if netmask[1..-1].to_i.between?(1, 128) - end - - false - end - - # Route edit dialog - # @param [Fixnum] id id of the edited route - # @param [Yast::Term] entry edited entry - # @param [Array] devs available devices - # @return route or nil, if canceled - def RoutingEditDialog(id, entry, devs) - entry = deep_copy(entry) - devs = deep_copy(devs) - - UI.OpenDialog( - Opt(:decorated), - MinWidth(60, - VBox( - HSpacing(1), - VBox( - HBox( - HWeight(70, - InputField( - Id(:destination), - Opt(:hstretch), - _("&Destination"), - Ops.get_string(entry, 1, "") - )), - HSpacing(1), - HWeight(30, - InputField( - Id(:genmask), - Opt(:hstretch), - _("&Netmask"), - Ops.get_string(entry, 3, "-") - )) - ), - HBox( - HWeight(70, - InputField( - Id(:gateway), - Opt(:hstretch), - _("&Gateway"), - Ops.get_string(entry, 2, "-") - )), - HSpacing(1), - HWeight(30, - ComboBox( - Id(:device), - Opt(:editable, :hstretch), - _("De&vice"), - devs - )) - ), - # ComboBox label - InputField( - Id(:options), - Opt(:hstretch), - Label.Options, - Ops.get_string(entry, 5, "") - ) - ), - HSpacing(1), - HBox( - PushButton(Id(:ok), Opt(:default), Label.OKButton), - PushButton(Id(:cancel), Label.CancelButton) - ) - )) - ) - - # Allow declaring route without iface (for gateway) #93996 - # if empty, use '-' which stands for any - if Ops.get_string(entry, 4, "") != "" - UI.ChangeWidget(Id(:device), :Value, Ops.get_string(entry, 4, "")) - else - UI.ChangeWidget(Id(:device), :Items, devs) - end - UI.ChangeWidget( - Id(:destination), - :ValidChars, - Ops.add(Ops.add(IP.ValidChars, "default"), "/-") - ) - UI.ChangeWidget(Id(:gateway), :ValidChars, Ops.add(IP.ValidChars, "-")) - # user can enter IPv4 netmask or prefix length (has to start with '/') - UI.ChangeWidget(Id(:genmask), :ValidChars, Netmask.ValidChars + "-/") - - if entry == term(:empty) - UI.SetFocus(Id(:destination)) - else - UI.SetFocus(Id(:gateway)) - end - - ret = nil - route = nil - - loop do - ret = UI.UserInput - break if ret != :ok - - route = Item(Id(id)) - val = Convert.to_string(UI.QueryWidget(Id(:destination), :Value)) - slash = Builtins.search(val, "/") - noprefix = slash.nil? ? val : Builtins.substring(val, 0, slash) - if val != "default" && !IP.Check(noprefix) - # Popup::Error text - Popup.Error(_("Destination is invalid.")) - UI.SetFocus(Id(:destination)) - next - end - route = Builtins.add(route, val) - val = Convert.to_string(UI.QueryWidget(Id(:gateway), :Value)) - if val != "-" && !IP.Check(val) - # Popup::Error text - Popup.Error(_("Gateway IP address is invalid.")) - UI.SetFocus(Id(:gateway)) - next - end - route = Builtins.add(route, val) - val = Convert.to_string(UI.QueryWidget(Id(:genmask), :Value)) - if val != "-" && !valid_netmask?(val) - # Popup::Error text - Popup.Error(_("Subnetmask is invalid.")) - UI.SetFocus(Id(:genmask)) - next - end - route = Builtins.add(route, val) - val = Convert.to_string(UI.QueryWidget(Id(:device), :Value)) - route = Builtins.add(route, val) - val = Convert.to_string(UI.QueryWidget(Id(:options), :Value)) - route = Builtins.add(route, val) - break - end - - UI.CloseDialog - return nil if ret != :ok - Builtins.y2debug("route=%1", route) - deep_copy(route) - end - def initRouting(_key) route_conf = deep_copy(Routing.Routes) @@ -330,14 +182,12 @@ def handleRouting(_key, event) Ops.get_string(event, "EventReason", "") == "ValueChanged" case Ops.get_symbol(event, "ID", :nil) when :add - item = RoutingEditDialog( - Builtins.size(@r_items), - term(:empty), - devs - ) + term = Item(Id(@r_items.size)) + term = Y2Network::Dialogs::Route.run(term, devs) + log.info "route dialog term #{term.inspect}" - if !item.nil? - @r_items = Builtins.add(@r_items, item) + if !term.nil? + @r_items << term UI.ChangeWidget(Id(:table), :Items, @r_items) end when :delete @@ -357,7 +207,7 @@ def handleRouting(_key, event) end devs = Builtins.sort(devs) - item = RoutingEditDialog(cur, item, devs) + item = Y2Network::Dialogs::Route.run(item, devs) if !item.nil? @r_items = Builtins.maplist(@r_items) do |e| next deep_copy(item) if cur == Ops.get_integer(e, [0, 0], -1) diff --git a/src/lib/y2network/dialogs/route.rb b/src/lib/y2network/dialogs/route.rb new file mode 100644 index 000000000..d32e3295e --- /dev/null +++ b/src/lib/y2network/dialogs/route.rb @@ -0,0 +1,79 @@ +require "ipaddr" + +require "cwm/popup" +require "y2network/route" +require "y2network/widgets/destination" +require "y2network/widgets/devices" +require "y2network/widgets/gateway" +require "y2network/widgets/route_options" + +Yast.import "Label" + +module Y2Network + module Dialogs + # Dialog to create or edit route. + class Route < CWM::Popup + # @param route is now term from table, but it should be object for single route + def initialize(route, available_devices) + log.info "route dialog with route: #{route.inspect} " \ + "and devices #{available_devices.inspect}" + params = route.params + @route_id = params[0] + # TODO: netmask + @route = ::Y2Network::Route.new( + to: (params[1] || "-") == "-" ? :default : IPAddr.new(params[1]), + interface: (params[4].nil? || params[4].empty?) ? :any : params[4], + gateway: (params[2] || "-") == "-" ? nil : IPAddr.new(params[2]), + options: params[5] || "" + ) + + @available_devices = available_devices + end + + def run + res = super + log.info "route dialog result #{res.inspect}" + return nil if res != :ok + + Yast::Term.new( + :item, + @route_id, + @route.to == :default ? "-" : @route.to.to_s, + @route.gateway.nil? ? "-" : @route.gateway.to_s, + "-", # TODO: netmask + @route.interface == :any ? "" : @route.interface, + @route.options + ) + end + + def contents + MinWidth( + 60, + VBox( + HBox( + HWeight(100, Widgets::Destination.new(@route)) + ), + HBox( + HWeight(70, Widgets::Gateway.new(@route)), + HSpacing(1), + HWeight(30, Widgets::Devices.new(@route, @available_devices)) + ), + Widgets::RouteOptions.new(@route) + ) + ) + end + end + + def next_button + Yast::Label.OKButton + end + + def back_button + Yast::Label.CancelButton + end + + def abort_button + "" + end + end +end diff --git a/src/lib/y2network/route.rb b/src/lib/y2network/route.rb index 3ea1c7676..4d719c7ee 100644 --- a/src/lib/y2network/route.rb +++ b/src/lib/y2network/route.rb @@ -20,15 +20,15 @@ module Y2Network # This class represents a network route class Route # @return [IPAddr,:default] Destination; :default if it is the default route - attr_reader :to + attr_accessor :to # @return [Interface,:any] Interface to associate the route to; :any if no interface is given - attr_reader :interface + attr_accessor :interface # @return [IPAddr,nil] Source IP address ('src' in ip route) - attr_reader :source + attr_accessor :source # @return [IPAddr,nil] Gateway IP address ('via' in ip route) - attr_reader :gateway + attr_accessor :gateway # @return [String] Additional options - attr_reader :options + attr_accessor :options # @param to [IPAddr,:default] Destination # @param interface [Interface,:any] Interface to associate the root to diff --git a/src/lib/y2network/widgets/destination.rb b/src/lib/y2network/widgets/destination.rb new file mode 100644 index 000000000..6ee09315b --- /dev/null +++ b/src/lib/y2network/widgets/destination.rb @@ -0,0 +1,61 @@ +Yast.import "IP" +Yast.import "Popup" + +require "ipaddr" + +require "cwm/common_widgets" + +module Y2Network + module Widgets + class Destination < CWM::InputField + # @param route [Y2Network::Route] route to modify by widget + def initialize(route) + textdomain "network" + + @route = route + end + + def label + _("&Destination") + end + + def help + # TODO: original also does not have help, so write new one + "" + end + + def opt + [:hstretch] + end + + def init + Yast::UI.ChangeWidget(Id(widget_id), :ValidChars, Yast::IP.ValidChars + "-/") + val = @route.to + self.value = val == :default ? "-" : (val.to_s + "/" + val.prefix.to_s) + end + + def validate + return true if valid_destination? + + Yast::Popup.Error(_("Destination is invalid.")) + focus + false + end + + def store + @route.to = value == "-" ? :default : IPAddr.new(value) + end + + private + + # Validates user's input obtained from destination field + def valid_destination? + destination = value + return true if destination == "-" + + ip = destination[/^[^\/]+/] + Yast::IP.Check(ip) + end + end + end +end diff --git a/src/lib/y2network/widgets/devices.rb b/src/lib/y2network/widgets/devices.rb new file mode 100644 index 000000000..8f3608ae2 --- /dev/null +++ b/src/lib/y2network/widgets/devices.rb @@ -0,0 +1,43 @@ +require "cwm/common_widgets" + +module Y2Network + module Widgets + class Devices < CWM::ComboBox + # @param route route object to get and store gateway value + def initialize(route, available_devices) + textdomain "network" + + @devices = available_devices + @route = route + end + + def label + _("De&vice") + end + + def help + # TODO: original also does not have help + "" + end + + def items + # TODO: maybe some translated names? + @devices.map { |d| [d, d] } + end + + def opt + [:hstretch, :editable] + end + + def init + interface = @route.interface + self.value = interface == :any ? "" : interface + end + + def store + interface = value + @route.interface = interface.empty? ? :any : interface + end + end + end +end diff --git a/src/lib/y2network/widgets/gateway.rb b/src/lib/y2network/widgets/gateway.rb new file mode 100644 index 000000000..0ed731fa4 --- /dev/null +++ b/src/lib/y2network/widgets/gateway.rb @@ -0,0 +1,54 @@ +Yast.import "IP" +Yast.import "Popup" +Yast.import "UI" + +require "ipaddr" + +require "cwm/common_widgets" + +module Y2Network + module Widgets + class Gateway < CWM::InputField + # @param route route object to get and store gateway value + def initialize(route) + textdomain "network" + + @route = route + end + + def label + _("&Gateway") + end + + def help + # TODO: original also does not have help + "" + end + + def opt + [:hstretch] + end + + def init + Yast::UI.ChangeWidget(Id(widget_id), :ValidChars, Yast::IP.ValidChars + "-") + + self.value = @route.gateway.nil? ? "-" : @route.gateway.to_s + end + + def validate + return true if value == "-" + + return true if Yast::IP.Check(value) + + Yast::Popup.Error(_("Gateway IP address is invalid.")) + focus + false + end + + def store + gw = value + @route.gateway = gw == "-" ? nil : IPAddr.new(gw) + end + end + end +end diff --git a/src/lib/y2network/widgets/route_options.rb b/src/lib/y2network/widgets/route_options.rb new file mode 100644 index 000000000..e8bfe9d09 --- /dev/null +++ b/src/lib/y2network/widgets/route_options.rb @@ -0,0 +1,38 @@ +Yast.import "Netmask" +Yast.import "Label" + +require "cwm/common_widgets" + +module Y2Network + module Widgets + class RouteOptions < CWM::InputField + # @param route route object to get and store options + def initialize(route) + textdomain "network" + + @route = route + end + + def label + Yast::Label.Options + end + + def help + # TODO: original also does not have help + "" + end + + def opt + [:hstretch] + end + + def init + self.value = @route.options + end + + def store + @route.options = value + end + end + end +end diff --git a/test/lib/y2network/proposal_settings_test.rb b/test/lib/y2network/proposal_settings_test.rb index 5e0984f8e..93d2cddbf 100755 --- a/test/lib/y2network/proposal_settings_test.rb +++ b/test/lib/y2network/proposal_settings_test.rb @@ -17,6 +17,8 @@ def stub_features(features) Yast.import "ProductFeatures" Yast::ProductFeatures.Import(features) + # stub restore as during Stage normal it restores features + allow(Yast::ProductFeatures).to receive(:Restore) end describe ".instance" do @@ -121,7 +123,7 @@ def stub_features(features) it "initializes the default network backend from the product control file" do expect(subject.default_backend).to eql(:network_manager) - stub_features("network" => { "network_manager" => "" }) + stub_features("network" => { "network_manager" => "", "network_manager_is_default" => false }) expect(subject.default_backend).to eql(:wicked) end end diff --git a/test/lib/y2remote/remote_test.rb b/test/lib/y2remote/remote_test.rb index 6e6153032..231d6012f 100644 --- a/test/lib/y2remote/remote_test.rb +++ b/test/lib/y2remote/remote_test.rb @@ -300,6 +300,7 @@ allow(subject).to receive(:modes).and_return(modes) allow(Yast2::Systemd::Target).to receive(:set_default) allow(display_manager).to receive(:restart) + allow(Y2Remote::Modes).to receive(:restart_modes) end context "when vnc is enabled" do diff --git a/test/routing_helpers_test.rb b/test/routing_helpers_test.rb index 7fa39e828..76950ebe9 100755 --- a/test/routing_helpers_test.rb +++ b/test/routing_helpers_test.rb @@ -42,40 +42,4 @@ def initialize expect(routing.convert_route_conf(obsolete_route_conf)).to eql correct_route_conf end end - - describe "#valid_netmask?" do - it "accepts IPv4 netmask" do - expect(routing.valid_netmask?("255.0.0.0")).to be true - end - - it "accepts IPv4 prefix" do - expect(routing.valid_netmask?("/24")).to be true - end - - it "accepts IPv6 prefix" do - expect(routing.valid_netmask?("/128")).to be true - end - - it "declines IPv6 netmask" do - expect(routing.valid_netmask?("fe00::")).to be false - end - - it "declines nil or empty input" do - expect(routing.valid_netmask?("")).to be false - expect(routing.valid_netmask?(nil)).to be false - end - - it "declines malformed prefix" do - expect(routing.valid_netmask?("/255/")).to be false - expect(routing.valid_netmask?("/255")).to be false - expect(routing.valid_netmask?("/-255")).to be false - expect(routing.valid_netmask?("/0")).to be false - end - - it "declines malformed IPv4 netmask" do - expect(routing.valid_netmask?("/255.0.0.0")).to be false - expect(routing.valid_netmask?("255.0.255.0")).to be false - expect(routing.valid_netmask?("0.0.0.0")).to be false - end - end end diff --git a/test/y2network/dialogs/route_test.rb b/test/y2network/dialogs/route_test.rb new file mode 100644 index 000000000..d4c0efb9d --- /dev/null +++ b/test/y2network/dialogs/route_test.rb @@ -0,0 +1,30 @@ +# Copyright (c) [2019] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../test_helper" +require "cwm/rspec" + +require "y2network/dialogs/route" + +describe Y2Network::Dialogs::Route do + let(:route) { Yast::Term.new(:empty) } + subject { described_class.new(route, ["eth0", "lo"]) } + + include_examples "CWM::Dialog" +end diff --git a/test/y2network/widgets/destination_test.rb b/test/y2network/widgets/destination_test.rb new file mode 100644 index 000000000..838f67b43 --- /dev/null +++ b/test/y2network/widgets/destination_test.rb @@ -0,0 +1,109 @@ +# Copyright (c) [2019] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../test_helper" +require "cwm/rspec" + +require "y2network/route" +require "y2network/widgets/destination" + +describe Y2Network::Widgets::Destination do + let(:widget_value) { "-" } + let(:route) { Y2Network::Route.new } + subject { described_class.new(route) } + + before do + allow(subject).to receive(:value).and_return(widget_value) + allow(Yast::UI).to receive(:ChangeWidget) + end + + include_examples "CWM::InputField" + + describe "#validate" do + context "when invalid ip is used" do + let(:widget_value) { "666.0.0.1" } + + it "returns false" do + expect(subject.validate).to eq false + end + + it "focus widget" do + expect(subject).to receive(:focus) + + subject.validate + end + + it "shows popup" do + expect(Yast::Popup).to receive(:Error) + + subject.validate + end + end + end + + describe "#init" do + it "sets valid characters for widget" do + expect(Yast::UI).to receive(:ChangeWidget).with(anything, :ValidChars, anything) + + subject.init + end + + context "route.to is :default" do + let(:route) { Y2Network::Route.new(to: :default) } + + it "sets values to '-'" do + expect(subject).to receive(:value=).with("-") + + subject.init + end + end + + context "route.to is IPAddr" do + let(:route) { Y2Network::Route.new(to: IPAddr.new("127.0.0.1/24")) } + it "sets value including prefix" do + expect(subject).to receive(:value=).with("127.0.0.0/24") + + subject.init + end + end + end + + describe "#store" do + context "value is '-'" do + let(:widget_value) { "-" } + + it "stores :default to route" do + subject.store + + expect(route.to).to eq :default + end + end + + context "value is ip address" do + let(:widget_value) { "127.0.0.1/24" } + + it "stores IPAddr to route" do + subject.store + + expect(route.to).to eq IPAddr.new("127.0.0.0/24") + end + end + + end +end diff --git a/test/y2network/widgets/devices_test.rb b/test/y2network/widgets/devices_test.rb new file mode 100644 index 000000000..f45b6e12e --- /dev/null +++ b/test/y2network/widgets/devices_test.rb @@ -0,0 +1,30 @@ +# Copyright (c) [2019] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../test_helper" +require "y2network/route" +require "y2network/widgets/devices" + +require "cwm/rspec" + +describe Y2Network::Widgets::Devices do + subject { described_class.new(Y2Network::Route.new, ["eth0", "lo"]) } + + include_examples "CWM::ComboBox" +end diff --git a/test/y2network/widgets/gateway_test.rb b/test/y2network/widgets/gateway_test.rb new file mode 100644 index 000000000..26423dc41 --- /dev/null +++ b/test/y2network/widgets/gateway_test.rb @@ -0,0 +1,109 @@ +# Copyright (c) [2019] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../test_helper" +require "cwm/rspec" + +require "y2network/route" +require "y2network/widgets/gateway" + +describe Y2Network::Widgets::Gateway do + let(:widget_value) { "-" } + let(:route) { Y2Network::Route.new } + subject { described_class.new(route) } + + before do + allow(subject).to receive(:value).and_return(widget_value) + allow(Yast::UI).to receive(:ChangeWidget) + end + + include_examples "CWM::InputField" + + describe "#validate" do + context "when invalid ip is used" do + let(:widget_value) { "666.0.0.1" } + + it "returns false" do + expect(subject.validate).to eq false + end + + it "focus widget" do + expect(subject).to receive(:focus) + + subject.validate + end + + it "shows popup" do + expect(Yast::Popup).to receive(:Error) + + subject.validate + end + end + end + + describe "#init" do + it "sets valid characters for widget" do + expect(Yast::UI).to receive(:ChangeWidget).with(anything, :ValidChars, anything) + + subject.init + end + + context "route.gateway is nil" do + let(:route) { Y2Network::Route.new(gateway: nil) } + + it "sets values to '-'" do + expect(subject).to receive(:value=).with("-") + + subject.init + end + end + + context "route.gateway is IPAddr" do + let(:route) { Y2Network::Route.new(gateway: IPAddr.new("127.0.0.1")) } + it "sets value including prefix" do + expect(subject).to receive(:value=).with("127.0.0.1") + + subject.init + end + end + end + + describe "#store" do + context "value is '-'" do + let(:widget_value) { "-" } + + it "stores nil to route" do + subject.store + + expect(route.gateway).to eq nil + end + end + + context "value is ip address" do + let(:widget_value) { "127.0.0.1" } + + it "stores IPAddr to route" do + subject.store + + expect(route.gateway).to eq IPAddr.new("127.0.0.1") + end + end + + end +end diff --git a/test/y2network/widgets/route_options_test.rb b/test/y2network/widgets/route_options_test.rb new file mode 100644 index 000000000..bcc289558 --- /dev/null +++ b/test/y2network/widgets/route_options_test.rb @@ -0,0 +1,31 @@ +# Copyright (c) [2019] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../test_helper" +require "cwm/rspec" + +require "y2network/route" +require "y2network/widgets/route_options" + +describe Y2Network::Widgets::RouteOptions do + let(:route) { Y2Network::Route.new } + subject { described_class.new(route) } + + include_examples "CWM::InputField" +end