-
Notifications
You must be signed in to change notification settings - Fork 599
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rename awful.popup
hide_on_right_click
property to hide_on_click
#3665
base: master
Are you sure you want to change the base?
Conversation
Codecov Report
@@ Coverage Diff @@
## master #3665 +/- ##
==========================================
+ Coverage 90.91% 90.94% +0.02%
==========================================
Files 896 897 +1
Lines 56633 56689 +56
==========================================
+ Hits 51489 51553 +64
+ Misses 5144 5136 -8
Flags with carried forward coverage won't be shown. Click here to find out more.
|
spec/awful/popup_spec.lua
Outdated
} | ||
_G.awesome = gtable.join(_G.awesome, { connect_signal = function() end }) | ||
_G.screen = { | ||
connect_signal = function() end, | ||
set_index_miss_handler = function() end, | ||
set_newindex_miss_handler = function() end, | ||
} | ||
_G.drawin = setmetatable( | ||
gtable.join(gobject { enable_properties = true, enable_auto_signals = true }, { | ||
set_index_miss_handler = function() end, | ||
set_newindex_miss_handler = function() end, | ||
}), | ||
{ | ||
__call = function() | ||
return { | ||
connect_signal = function() end, | ||
} | ||
end, | ||
} | ||
) | ||
package.loaded["wibox.drawable"] = setmetatable({}, { | ||
__call = function() | ||
return { | ||
_inform_visible = function() end, | ||
connect_signal = function() end, | ||
set_bg = function() end, | ||
set_fg = function() end, | ||
draw = function() end, | ||
set_widget = function() end, | ||
get_widget = function() end, | ||
} | ||
end, | ||
}) | ||
package.loaded["gears.debug"] = | ||
{ deprecate = function() end, deprecate_class = function() end } | ||
|
||
local popup = require "awful.popup" | ||
|
||
describe("awful.popup", function() | ||
describe("hide_on_click", function() | ||
it( | ||
"Constructor without the param should not connect the hide_fct", | ||
function() | ||
local p = popup { widget = utils.widget_stub() } | ||
assert.is_nil(p._signals["button::press"]) | ||
end | ||
) | ||
|
||
it("Constructor parameter should connect the hide_fct", function() | ||
local p = | ||
popup { widget = utils.widget_stub(), hide_on_click = true } | ||
assert.is_true( | ||
p._signals["button::press"].strong[p._private.hide_fct] | ||
) | ||
end) | ||
|
||
it("Setter set_hide_click should connect/disconnect the the hide_fct", function() | ||
local p = popup { widget = utils.widget_stub() } | ||
assert.is_nil(p._signals["button::press"]) | ||
|
||
p:set_hide_on_click(true) | ||
assert.is_true( | ||
p._signals["button::press"].strong[p._private.hide_fct] | ||
) | ||
|
||
p:set_hide_on_click(false) | ||
assert.is_nil( | ||
p._signals["button::press"].strong[p._private.hide_fct] | ||
) | ||
end) | ||
|
||
it( | ||
"Legacy constructor parameter hide_on_right_click should still work", | ||
function() | ||
local p = popup { | ||
widget = utils.widget_stub(), | ||
hide_on_right_click = true, | ||
} | ||
assert.is_true( | ||
p._signals["button::press"].strong[p._private.hide_fct] | ||
) | ||
end | ||
) | ||
|
||
it("Legacy setter set_hide_on_right_click should still work", function() | ||
local p = popup { widget = utils.widget_stub() } | ||
assert.is_nil(p._signals["button::press"]) | ||
|
||
p:set_hide_on_right_click(true) | ||
assert.is_true( | ||
p._signals["button::press"].strong[p._private.hide_fct] | ||
) | ||
|
||
p:set_hide_on_right_click(false) | ||
assert.is_nil( | ||
p._signals["button::press"].strong[p._private.hide_fct] | ||
) | ||
end) | ||
end) | ||
end) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unit tests shouldn't test private properties and implementation details. They shouldn't know or care about those things.
Another red flag is the fact that you had to noop-stub half the widget system to get this to work. That will never be an accurate test of what's going on in the non-stubbed library.
The integration test in tests
is the correct way to test functionality like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could make a "global" monkey-patch of gears.object
and it's CAPI variant to check if a signal is connected. Testing private APIs is bad, but testing at a message passing level is a very good thing. we have plenty of broken signals right now and very little tooling to detect them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't.
If the thing to test for is "if I do A, is signal X emitted?", that's fine. We don't need any monkey-patching for that, just a spy.new()
passed to connect_signal
and an assert.spy().was_called
.
But if the thing to test is "if I do A, does thing X happen?", then the internal signal is an implementation detail that shouldn't be tested. What should be tested is "did X happen?". And that test should be agnostic of whether a signal, function or any other system was called for that to happen, now or in any future revision of the implementation.
The public contract for hide_on_click
should not include "there is a private function, but you can still access it, and that function is then assigned to another private table based on the specific signal we use internally". There shouldn't be a test breaking because someone decided to make the function actually local
, renamed it, or because the "when button is clicked" logic changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, yeah. I didn't feel so good about these unit tests. I'm happy to see some feedback on it. Thank you both of you @sclu1034 and @Elv13.
First, to describe my goal with these unit tests, I want to make sure that setting the parameter, or calling the setter, would actually trigger the appropriate signal connection. So that's a "if I do A, does thing X happen?" as described by @sclu1034. The idea was to make sure the parameter is actually taken into account. (it was mostly in the regard of the recent PR about the with_shell
parameter blatantly ignored by the implementation.)
Another red flag is the fact that you had to noop-stub half the widget system to get this to work.
I have to disagree with this one. In a unit test suite, you only load the module that the test targets. In this case, awful.popup
. However, for the module to work, we need to have some tools from Awesome that just can't be here, like the CAPI. It just doesn't exist when we require("awful.popup")
with busted.
Maybe we could make a "global" monkey-patch of gears.object and it's CAPI variant to check if a signal is connected.
We don't need any monkey-patching for that, just a spy.new() passed to connect_signal and an assert.spy().was_called.
Yup, we could have definition with spies in the preload.lua
busted helper for this kind of objects.
The public contract for hide_on_click should not include "there is a private function, but you can still access it, and that function is then assigned to another private table based on the specific signal we use internally". There shouldn't be a test breaking because someone decided to make the function actually local, renamed it, or because the "when button is clicked" logic changed.
This sounds right, but it doesn't give a solution for the "I want to make sure that setting the parameter, or calling the setter, would actually trigger the appropriate signal connection" problem. Hm... This is actually an obvious example of "testing implementation details".
Maybe we could also make hide_on_click either a boolean or a key/value table of button id as keys and boolean as values? However, that's not required to merge this PR. It's just "umm, that would be nice!".
Making it a key/value table of button id as keys and boolean as values is basically a simplification (would it even be easier?) of the buttons
property. Sooo 🤷
Making hide_on_click
a boolean property can actually be a good idea. We can make it behave as a property by only adding a getter that does the self._signals["button::press"].strong[self._private.hide_fct] ~=nil
test. So the unit test would have to check that after setting the (this is actually a bad idea)hide_on_click
value, the getter returns the appropriate boolean value.
I guess this way, we can reconcile both parties.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I guess we could also just test that setting hide_on_click on the constructor will actually call the "set_hide_on_click" method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this. I think we will need a full backward compatibility property for this (@deprecatedproperty
).
Maybe we could also make hide_on_click
either a boolean or a key/value table of button id as keys and boolean as values? However, that's not required to merge this PR. It's just "umm, that would be nice!".
As for the tests, I agree with @sclu1034 the private API part for the signals. However I am fine with all the shims. It tests what it aims to test. I really don't have a strict line in the sand for testing. I usually do the doc example because it has a bigger impact compared to the time they take to write. But it's a bad fit for this fix. The sequence is currently ill-suited for widgets, even if it's technically possible to abuse it for them. All other template cannot reflect the "click" action.
spec/awful/popup_spec.lua
Outdated
} | ||
_G.awesome = gtable.join(_G.awesome, { connect_signal = function() end }) | ||
_G.screen = { | ||
connect_signal = function() end, | ||
set_index_miss_handler = function() end, | ||
set_newindex_miss_handler = function() end, | ||
} | ||
_G.drawin = setmetatable( | ||
gtable.join(gobject { enable_properties = true, enable_auto_signals = true }, { | ||
set_index_miss_handler = function() end, | ||
set_newindex_miss_handler = function() end, | ||
}), | ||
{ | ||
__call = function() | ||
return { | ||
connect_signal = function() end, | ||
} | ||
end, | ||
} | ||
) | ||
package.loaded["wibox.drawable"] = setmetatable({}, { | ||
__call = function() | ||
return { | ||
_inform_visible = function() end, | ||
connect_signal = function() end, | ||
set_bg = function() end, | ||
set_fg = function() end, | ||
draw = function() end, | ||
set_widget = function() end, | ||
get_widget = function() end, | ||
} | ||
end, | ||
}) | ||
package.loaded["gears.debug"] = | ||
{ deprecate = function() end, deprecate_class = function() end } | ||
|
||
local popup = require "awful.popup" | ||
|
||
describe("awful.popup", function() | ||
describe("hide_on_click", function() | ||
it( | ||
"Constructor without the param should not connect the hide_fct", | ||
function() | ||
local p = popup { widget = utils.widget_stub() } | ||
assert.is_nil(p._signals["button::press"]) | ||
end | ||
) | ||
|
||
it("Constructor parameter should connect the hide_fct", function() | ||
local p = | ||
popup { widget = utils.widget_stub(), hide_on_click = true } | ||
assert.is_true( | ||
p._signals["button::press"].strong[p._private.hide_fct] | ||
) | ||
end) | ||
|
||
it("Setter set_hide_click should connect/disconnect the the hide_fct", function() | ||
local p = popup { widget = utils.widget_stub() } | ||
assert.is_nil(p._signals["button::press"]) | ||
|
||
p:set_hide_on_click(true) | ||
assert.is_true( | ||
p._signals["button::press"].strong[p._private.hide_fct] | ||
) | ||
|
||
p:set_hide_on_click(false) | ||
assert.is_nil( | ||
p._signals["button::press"].strong[p._private.hide_fct] | ||
) | ||
end) | ||
|
||
it( | ||
"Legacy constructor parameter hide_on_right_click should still work", | ||
function() | ||
local p = popup { | ||
widget = utils.widget_stub(), | ||
hide_on_right_click = true, | ||
} | ||
assert.is_true( | ||
p._signals["button::press"].strong[p._private.hide_fct] | ||
) | ||
end | ||
) | ||
|
||
it("Legacy setter set_hide_on_right_click should still work", function() | ||
local p = popup { widget = utils.widget_stub() } | ||
assert.is_nil(p._signals["button::press"]) | ||
|
||
p:set_hide_on_right_click(true) | ||
assert.is_true( | ||
p._signals["button::press"].strong[p._private.hide_fct] | ||
) | ||
|
||
p:set_hide_on_right_click(false) | ||
assert.is_nil( | ||
p._signals["button::press"].strong[p._private.hide_fct] | ||
) | ||
end) | ||
end) | ||
end) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could make a "global" monkey-patch of gears.object
and it's CAPI variant to check if a signal is connected. Testing private APIs is bad, but testing at a message passing level is a very good thing. we have plenty of broken signals right now and very little tooling to detect them.
144331b
to
687e5f8
Compare
I took an hour or two this morning to work a little more on this unit testing thing... I ended up with something like : it(
"Call set_hide_on_click with `true` should connect the hide_fct",
function()
local p = popup { widget = utils.widget_stub() }
local spy_connect = spy.on(p, "connect_signal")
local spy_disconnect = spy.on(p, "disconnect_signal")
p:set_hide_on_click(true)
assert.spy(spy_connect).was_called()
assert.spy(spy_disconnect).was_not_called()
end
) It seems cleaner since we now spy on the connect/disconnect signals. But I couldn't find a way to make the spies to work with the constructor, so I think the test is incomplete. I decided to drop the unit-test for now. Since I couldn't find something that make me happy with it. I'll probably put some more time on it latter~~~. On another note, I have added commit 687e5f8. It implements the appropriate getter, so |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The version with spy.on(p, "connect_signal")
would still be testing an implementation detail. Not as bad as the previous one, as no private values are accessed, but still not something a unit test should do.
lib/awful/popup.lua
Outdated
local button_press_signals = self._signals["button::press"] | ||
return button_press_signals and button_press_signals.strong[self._private.hide_fct] or false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This depends on the internal implementation of gears.object
, which something like awful.popup
should know nothing about.
The proper way to do this would be to provide something like is_connected(signal_name, fn)
in gears.object
.
687e5f8
to
ef46d3f
Compare
lib/gears/object.lua
Outdated
-- @tparam function func The callback to check if connected. | ||
-- @treturn boolean Whether the signal is connected. | ||
-- @method is_weak_signal_connected | ||
function object:is_weak_signal_connected(name, func) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could we merge both strong and weak variants into one? I don't really see an use case where it would make a difference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
The implementation of the `hide_fct`actually doesn't check for the button pressed. And it sounds more user-friendly that it works this way. So, it is reasonable to rename the constructor parameter to `hide_on_click`, and also apply this same naming update to the setter. Note that the changes here should preserve full backward compatibility with deprecation warning.
This adds integration test to validate the behavior of `hide_on_click`.
0af67d6
to
111ce05
Compare
111ce05
to
8ad78c7
Compare
|
||
--- Check if the callback is connected to the signal. | ||
-- | ||
-- This function check both kind of signal connection (strong and weak). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-- This function check both kind of signal connection (strong and weak). | |
-- This function checks both kinds of signal connection (strong and weak). |
local sig = find_signal(self, name) | ||
return not not (sig and (sig.strong[func] or sig.weak[func])) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
find_signal
creates the entry when it doesn't exist, so you don't need to check if sig
exists.
@@ -453,8 +465,8 @@ end | |||
-- @tparam string|table args.preferred_positions | |||
-- @tparam string|table args.preferred_anchors | |||
-- @tparam table|number args.offset The X and Y offset compared to the parent object | |||
-- @tparam boolean args.hide_on_right_click Whether or not to hide the popup on | |||
-- right clicks. | |||
-- @tparam boolean args.hide_on_click Whether or not to hide the popup on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about turning it from boolean into int, so user could choose the mouse button?
I stepped on this lately, the implementation of the hide_fct doesn't match the name since there is no check on what button is actually pressed.
It sounds more reasonable to me to rename the
hide_on_right_click
property tohide_on_click
, because I feel the current behavior is more natural.It was either: break the current behavior, or change the property name. Note that the latter can also preserve the API backwards compatibility by keeping the old property with an additional deprecation message.
I have also write some tests to make sure everything is working the as expected now.