-- This test suite tries to prevent the legacy notification popups from -- regressing as the new notification API is improving. local spawn = require("awful.spawn") local naughty = require("naughty" ) local gdebug = require("gears.debug") local cairo = require("lgi" ).cairo local beautiful = require("beautiful") local Gio = require("lgi" ).Gio local GLib = require("lgi" ).GLib -- This module test deprecated APIs require("gears.debug").deprecate = function() end local dbus_connection = assert(Gio.bus_get_sync(Gio.BusType.SESSION)) local function send_notify(app, id, icon, summary, body, actions, hints, timeout, callback) local parameters = GLib.Variant("(susssasa{sv}i)", { app, id, icon, summary, body, actions, hints, timeout }) local reply_type = GLib.VariantType.new("(u)") local function invoke_callback(conn, result) local new_id = conn:call_finish(result).value[1] if callback then callback(new_id) end end dbus_connection:call("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "Notify", parameters, reply_type, Gio.DBusCallFlags.NO_AUTO_START, -1, nil, invoke_callback) end local steps = {} local has_cmd_notify = false -- Use `notify-send` instead of the shimmed version to better test the dbus -- to notification code. local function check_cmd() local path = os.getenv("PATH") local pos = 1 while path:find(":", pos) do local np = path:find(":", pos) local p = path:sub(pos, np-1).."/" pos = np+1 local f = io.open(p.."notify-send") if f then f:close() has_cmd_notify = true end if has_cmd_notify then return end end end check_cmd() if not has_cmd_notify then require("gears.debug").print_warning("Did not find notify-send, skipping some tests") end local active, destroyed, reasons, counter = {}, {}, {}, 0 local default_width, default_height = 0, 0 local function added_callback(n) table.insert(active, n) counter = counter + 1 end naughty.connect_signal("added", added_callback) local function destroyed_callback(n, reason) local found = false for k, n2 in ipairs(active) do if n2 == n then found = true table.remove(active, k) end end assert(found) if reason then reasons[reason] = reasons[reason] and reasons[reason] + 1 or 1 end table.insert(destroyed, n) end naughty.connect_signal("destroyed", destroyed_callback) table.insert(steps, function() if not has_cmd_notify then return true end spawn{ 'notify-send', 'title', 'message', '-t', '25000' } return true end) table.insert(steps, function() if not has_cmd_notify then return true end if #active ~= 1 then return end local n = active[1] assert(n.box) local offset = 2*n.box.border_width default_width = n.box.width+offset default_height = n.box.height + offset + naughty.config.spacing assert(default_width > 0) assert(default_height > 0) -- Make sure the expiration timer is started assert(n.timer) assert(n.timer.started) assert(n.is_expired == false) n:destroy() assert(#active == 0) return true end) -- Test pausing incoming notifications. table.insert(steps, function() assert(not naughty.suspended) naughty.suspended = true -- There is some magic behind this, check it works assert(naughty.suspended) spawn{ 'notify-send', 'title', 'message', '-t', '25000' } return true end) -- Test resuming incoming notifications. table.insert(steps, function(count) if count ~= 4 then return end assert(#active == 0) assert(#naughty.notifications.suspended == 1) assert(naughty.notifications.suspended[1]:get_suspended()) naughty.resume() assert(not naughty.suspended) assert(#naughty.notifications.suspended == 0) assert(#active == 1) active[1]:destroy() assert(#active == 0) spawn{ 'notify-send', 'title', 'message', '-t', '1' } return true end) -- Test automatic expiration. table.insert(steps, function() if counter ~= 3 then return end return true end) table.insert(steps, function() if #active > 0 then return end -- It expired after one milliseconds, so it should be gone as soon as -- it is registered. assert(#active == 0) assert(not naughty.expiration_paused) naughty.expiration_paused = true -- There is some magic behind this, make sure it works assert(naughty.expiration_paused) spawn{ 'notify-send', 'title', 'message', '-t', '1' } return true end) -- Test disabling automatic expiration. table.insert(steps, function() if counter ~= 4 then return end -- It should not expire by itself, so that should always be true assert(#active == 1) return true end) -- Wait long enough to avoid races. table.insert(steps, function(count) if count ~= 4 then return end assert(#active == 1) assert(active[1].is_expired) naughty.expiration_paused = false assert(not naughty.expiration_paused) return true end) -- Make sure enabling expiration process the expired queue. table.insert(steps, function() -- Right now this doesn't require a step for itself, but this could change -- so better not "document" the instantaneous clearing of the queue. if #active > 0 then return end spawn{ 'notify-send', 'low', 'message', '-t', '25000', '-u', 'low' } spawn{ 'notify-send', 'normal', 'message', '-t', '25000', '-u', 'normal' } spawn{ 'notify-send', 'critical', 'message', '-t', '25000', '-u', 'critical' } return true end) -- Test the urgency level and default preset. table.insert(steps, function() if counter ~= 7 then return end while #active > 0 do active[1]:destroy() end return true end) -- Test what happens when the screen has the maximum number of notification it -- can display at one. table.insert(steps, function() local wa = mouse.screen.workarea local max_notif = math.floor(wa.height/default_height) -- Everything should fit, otherwise the math is wrong in -- `neughty.layout.legacy` and its a regression. for i=1, max_notif do spawn{ 'notify-send', 'notif '..i, 'message', '-t', '25000', '-u', 'low' } end return true end) -- Test vertical overlapping local function test_overlap() local wa = mouse.screen.workarea for _, n1 in ipairs(active) do assert(n1.box) assert(n1.id) -- Check for overlapping the workarea assert(n1.box.y+default_height < wa.y+wa.height) assert(n1.box.y >= wa.y) -- Check for overlapping each other for _, n2 in ipairs(active) do assert(n2.box) if n1 ~= n2 then local geo1, geo2 = n1.box:geometry(), n2.box:geometry() assert(geo1.height == geo2.height) assert(geo1.height + 2*n1.box.border_width + naughty.config.spacing == default_height) if n1.position == n2.position then assert( geo1.y >= geo2.y+default_height or geo2.y >= geo1.y+default_height ) end end end end end -- Check the lack of overlapping and the presence of the expected content. table.insert(steps, function() local wa = mouse.screen.workarea local max_notif = math.floor(wa.height/default_height) if counter ~= 7 + max_notif then return end assert(#active == max_notif) test_overlap() -- Now add even more! for i=1, 5 do spawn{ 'notify-send', 'notif '..i, 'message', '-t', '25000', '-u', 'low' } end return true end) -- Test the code to hide the older notifications when there is too many for the -- screen. table.insert(steps, function() local wa = mouse.screen.workarea local max_notif = math.floor(wa.height/default_height) if counter ~= 7 + max_notif + 5 then return end -- The other should have been hidden assert(#active == max_notif) assert(reasons[naughty.notification_closed_reason.too_many_on_screen] == 5) test_overlap() while #active > 0 do active[1]:destroy() end return true end) local positions = { "top_left" , "top_middle" , "top_right" , "bottom_left" , "bottom_middle" , "bottom_right" , } -- Test each corners. table.insert(steps, function() for _, pos in ipairs(positions) do for i=1, 3 do -- Skip dbus for this one. naughty.notification { position = pos, title = "At "..pos.." "..i, message = "some message", timeout = 25000, } end end return true end) table.insert(steps, function() if #active ~= #positions*3 then return end test_overlap() while #active > 0 do active[1]:destroy() end return true end) local big_icon = cairo.ImageSurface(cairo.Format.ARGB32, 256, 256) local cr = cairo.Context(big_icon) local small_icon = cairo.ImageSurface(cairo.Format.ARGB32, 32 , 32 ) local cr2 = cairo.Context(small_icon) local wierd_ratio1 = cairo.ImageSurface(cairo.Format.ARGB32, 256, 128) local cr3 = cairo.Context(wierd_ratio1) local wierd_ratio2 = cairo.ImageSurface(cairo.Format.ARGB32, 128, 256) local cr4 = cairo.Context(wierd_ratio2) -- Checkboard shirt pattern icon! for i=1, 5 do for j=1, 5 do cr:set_source_rgb( i%2 == 1 and 1 or 0, j%2 == 1 and 1 or 0, i%2 == 0 and 0 or 1 ) cr:rectangle( (i-1)*48, (j-1)*48, 48, 48 ) cr:fill() cr2:set_source_rgb( i%2 == 1 and 1 or 0, j%2 == 1 and 1 or 0, i%2 == 0 and 0 or 1 ) cr2:rectangle( (i-1)*6, (j-1)*6, 6, 6 ) cr2:fill() cr3:set_source_rgb( i%2 == 1 and 1 or 0, j%2 == 1 and 1 or 0, i%2 == 0 and 0 or 1 ) cr3:rectangle( (i-1)*48, (j-1)*24, 48, 24 ) cr3:fill() cr4:set_source_rgb( i%2 == 1 and 1 or 0, j%2 == 1 and 1 or 0, i%2 == 0 and 0 or 1 ) cr4:rectangle( (i-1)*24, (j-1)*48, 24, 48 ) cr4:fill() end end -- Test the icon size constraints. table.insert(steps, function() beautiful.notification_icon_size = 64 -- Icons that are too large (they should be downscaled) local n1 = naughty.notification { icon = big_icon, title = "Has a nice icon!", message = "big", timeout = 25000, } assert(n1.id) assert(n1.iconbox) assert(n1.iconbox._private.image:get_width () == beautiful.notification_icon_size) assert(n1.iconbox._private.image:get_height() == beautiful.notification_icon_size) assert(n1.iconbox._private.image:get_width () == n1.size_info.icon_w) assert(n1.iconbox._private.image:get_height() == n1.size_info.icon_h) assert(n1.size_info.icon_scale_factor == 1/4) -- Icons that are too small (they should not be upscaled) local n2 = naughty.notification { icon = small_icon, title = "Has a nice icon!", message = "small", timeout = 25000, } assert(n2.id) assert(n2.iconbox) assert(n2.iconbox._private.image:get_width () == 32) assert(n2.iconbox._private.image:get_height() == 32) assert(n2.iconbox._private.image:get_width () == n2.size_info.icon_w) assert(n2.iconbox._private.image:get_height() == n2.size_info.icon_h) assert(not n2.size_info.icon_scale_factor) -- Downscaled non square icons (aspect ratio should be kept). local n3 = naughty.notification { icon = wierd_ratio1, title = "Has a nice icon!", message = "big", timeout = 25000, } local n4 = naughty.notification { icon = wierd_ratio2, title = "Has a nice icon!", message = "big", timeout = 25000, } assert(n3.iconbox) assert(n3.iconbox._private.image:get_width () == beautiful.notification_icon_size) assert(n3.iconbox._private.image:get_height() == beautiful.notification_icon_size/2) assert(n3.iconbox._private.image:get_width () == n3.size_info.icon_w) assert(n3.iconbox._private.image:get_height() == n3.size_info.icon_h) assert(n3.size_info.icon_scale_factor == 1/4) assert(n4.iconbox) assert(n4.iconbox._private.image:get_width () == beautiful.notification_icon_size/2) assert(n4.iconbox._private.image:get_height() == beautiful.notification_icon_size) assert(n4.iconbox._private.image:get_width () == n4.size_info.icon_w) assert(n4.iconbox._private.image:get_height() == n4.size_info.icon_h) assert(n4.size_info.icon_scale_factor == 1/4) -- The notification size should change proportionally to the icon size. assert(n1.box.width == n2.box.width + 32) assert(n1.box.height == n2.box.height + 32) assert(n1.box.height == n3.box.height + 32) assert(n1.box.width == n4.box.width + 32) assert(n1.box.height == n4.box.height) assert(n1.box.width == n3.box.width ) -- Make sure unconstrained icons work as expected. beautiful.notification_icon_size = nil local n5 = naughty.notification { icon = big_icon, title = "Has a nice icon!", message = "big", timeout = 25000, } assert(n5.iconbox) assert(n5.iconbox._private.image:get_width () == 256) assert(n5.iconbox._private.image:get_height() == 256) assert(n5.iconbox._private.image:get_width () == n5.size_info.icon_w) assert(n5.iconbox._private.image:get_height() == n5.size_info.icon_h) assert(not n5.size_info.icon_scale_factor) -- Make sure invalid icons don't prevent the message from being shown. local n6 = naughty.notification { icon = "this/is/an/invlid/path", title = "Has a nice icon!", message = "Very important life saving advice", timeout = 25000, } n1:destroy() n2:destroy() n3:destroy() n4:destroy() n5:destroy() n6:destroy() assert(#active == 0) return true end) -- Test notifications with size constraints. table.insert(steps, function() local str = "foobar! " assert(#active == 0) -- 2^9 foobars is a lot of foobars. for _=1, 10 do str = str .. str end -- First, see what happen without any constraint and enormous messages. -- This also test notifications larger than the workarea. local n1 = naughty.notification { title = str, message = str, timeout = 25000, } -- Same, but with an icon and larger borders. local n2 = naughty.notification { icon = big_icon, title = str, message = str, timeout = 25000, border_width = 40, } local wa = mouse.screen.workarea assert(n1.box.width +2*n1.box.border_width <= wa.width ) assert(n1.box.height+2*n1.box.border_width <= wa.height) assert(n2.box.width +2*n2.box.border_width <= wa.width ) assert(n2.box.height+2*n2.box.border_width <= wa.height) n1:destroy() n2:destroy() -- Now set a maximum size and try again. beautiful.notification_max_width = 256 beautiful.notification_max_height = 96 local n3 = naughty.notification { title = str, message = str, timeout = 25000, } assert(n3.box.width <= 256) assert(n3.box.height <= 96 ) -- Now test when the icon is larger than the maximum. local n4 = naughty.notification { icon = big_icon, title = str, message = str, timeout = 25000, } assert(n4.box.width <= 256) assert(n4.box.height <= 96 ) assert(n4.iconbox._private.image:get_width () == n4.size_info.icon_w) assert(n4.iconbox._private.image:get_height() == n4.size_info.icon_h) assert(n4.size_info.icon_w <= 256) assert(n4.size_info.icon_h <= 96 ) n3:destroy() n4:destroy() assert(#active == 0) return true end) -- Test more advanced features than what notify-send can provide. -- Test the actions. table.insert(steps, function() assert(#active == 0) send_notify("Awesome test", 0, "", "title", "message body", { "1", "one", "2", "two", "3", "three" }, {}, 25000) return true end) table.insert(steps, function() if #active == 0 then return end assert(#active == 1) local n = active[1] assert(n.box) assert(#n.actions == 3) assert(n.actions[1].name == "one" ) assert(n.actions[2].name == "two" ) assert(n.actions[3].name == "three") n:destroy() return true end) --TODO Test too many actions. --TODO Test action with long names. local nid, name_u, message_u, actions_u = nil -- Test updating a notification. table.insert(steps, function() send_notify("Awesome test", 0, "", "title", "message body", { "1", "one", "2", "two", "3", "three" }, {}, 25000, function(id) nid = id end) return true end) table.insert(steps, function() if #active == 0 or not nid then return end local n = active[1] n:connect_signal("property::title" , function() name_u = true end) n:connect_signal("property::message", function() message_u = true end) n:connect_signal("property::actions", function() actions_u = true end) send_notify("Awesome test", nid, "", "updated title", "updated message body", { "1", "four", "2", "five", "3", "six" }, {}, 25000) return true end) -- Test if all properties have been updated. table.insert(steps, function() if not name_u then return end if not message_u then return end if not actions_u then return end -- No new notification should have been created. assert(#active == 1) local n = active[1] assert(n.title == "updated title" ) assert(n.message == "updated message body") assert(#n.actions == 3) assert(n.actions[1].name == "four" ) assert(n.actions[2].name == "five" ) assert(n.actions[3].name == "six" ) return true end) local int_n = nil -- Test replace_id with internally generated notifications. -- Even if it makes little sense, it should work and is used in the wild. table.insert(steps, function() int_n = naughty.notification { title = "foo", message = "bar", timeout = 25000, } local nid2 = int_n.id assert(int_n.id) local res, err = pcall(function() int_n.id = 1337 end) assert(err:match("Notification identifier can only be set once")) assert(not res) assert(int_n.id == nid2) -- watch out, capi.dbus signals are not normal object signals send_notify("Awesome test", nid2, "", "title2", "text2", { "the one action" }, {}, 123) return true end) -- Test that the values were replaced table.insert(steps, function() if int_n.title ~= "title2" then return end assert(int_n.title == "title2") assert(int_n.message == "text2" ) --assert(int_n.timeout == 123 ) --FIXME int_n:destroy() --TODO test what happens when updating a destroyed notification -- There is currently no API to resurrect notifications. return true end) -- Now check if the old deprecated (but still supported) APIs don't have errors. table.insert(steps, function() -- Tests are (by default) not allowed to call deprecated APIs gdebug.deprecate = function() end local n = naughty.notification { title = "foo", message = "bar", timeout = 25000, } -- Make sure the suspension don't cause errors assert(not naughty.is_suspended()) assert(not naughty.suspended) naughty.suspend() assert(naughty.is_suspended()) assert(naughty.suspended) naughty.resume() assert(not naughty.is_suspended()) assert(not naughty.suspended) naughty.toggle() assert(naughty.is_suspended()) assert(naughty.suspended) naughty.toggle() assert(not naughty.is_suspended()) assert(not naughty.suspended) naughty.suspended = not naughty.suspended assert(naughty.is_suspended()) assert(naughty.suspended) naughty.suspended = not naughty.suspended assert(not naughty.is_suspended()) assert(not naughty.suspended) -- Replace the text assert(n.title == "foo") assert(n.message == "bar") assert(n.text == "bar") local width, height = n.width, n.height assert(width) assert(height) naughty.replace_text(n, "foobar", "baz") assert(n.title == "foobar") assert(n.message == "baz") assert(n.text == "baz") assert(n.width > width) assert(n.height == height) width, height = n.width, n.height naughty.replace_text(n, "foo", "bar\nbaz") assert(n.title == "foo") assert(n.message == "bar\nbaz") assert(n.text == "bar\nbaz") assert(n.width < width) assert(n.height > height) width, height = n.width, n.height naughty.replace_text(n, "foo", "bar") assert(n.title == "foo") assert(n.message == "bar") assert(n.text == "bar") assert(n.width == width) assert(n.height < height) -- Test the ID system local id = n.id assert(naughty.getById(id) == n) assert(naughty.get_by_id(id) == n) assert(naughty.getById(42) ~= n) assert(naughty.get_by_id(42) ~= n) -- The timeout naughty.reset_timeout(n, 1337) -- Destroy using the old API local old_count = #destroyed naughty.destroy(n) assert(old_count == #destroyed - 1) -- Destroy using the old API, while suspended local n2 = naughty.notification { title = "foo", message = "bar", timeout = 25000, } naughty.suspended = true naughty.destroy(n2) assert(old_count == #destroyed - 2) naughty.suspended = false -- The old notify function and "text" instead of "message" naughty.notify { text = "foo" } -- Finish by testing disconnect_signal naughty.disconnect_signal("destroyed", destroyed_callback) naughty.disconnect_signal("added", added_callback) return true end) -- Test many screens. require("_runner").run_steps(steps)