diff --git a/README.md b/README.md index 15f13dbc9..fa6c343e1 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ DFHack is a Dwarf Fortress memory access library, distributed with scripts and plugins implementing a wide variety of useful functions and tools. -The full documentation [is available online here](https://dfhack.readthedocs.org), -from the README.html page in the DFHack distribution, or as raw text in the `./docs` folder. +The full documentation [is available online here](https://dfhack.readthedocs.org). +It is also accessible via the README.html page in the DFHack distribution or as raw text in the `./docs` folder. If you're an end-user, modder, or interested in contributing to DFHack - go read those docs. -If that's unclear, or you need more help checkout our [support page](https://docs.dfhack.org/en/latest/docs/Support.html) for up-to-date options. +If the docs are unclear or you need more help, please check out our [support page](https://docs.dfhack.org/en/latest/docs/Support.html) for ways to contact the DFHack developers. diff --git a/ci/test.lua b/ci/test.lua index 9a4a33ef2..aca78e851 100644 --- a/ci/test.lua +++ b/ci/test.lua @@ -240,7 +240,7 @@ end -- output doesn't trigger its own dfhack.printerr usage detection (see -- detect_printerr below) local orig_printerr = dfhack.printerr -local function wrap_expect(func, private) +local function wrap_expect(func, private, path) return function(...) private.checks = private.checks + 1 local ret = {func(...)} @@ -269,7 +269,7 @@ local function wrap_expect(func, private) end -- Skip any frames corresponding to C calls, or Lua functions defined in another file -- these could include pcall(), with_finalize(), etc. - if info.what == 'Lua' and info.short_src == caller_src then + if info.what == 'Lua' and (info.short_src == caller_src or info.short_src == path) then orig_printerr((' at %s:%d'):format(info.short_src, info.currentline)) end frame = frame + 1 @@ -278,7 +278,7 @@ local function wrap_expect(func, private) end end -local function build_test_env() +local function build_test_env(path) local env = { test = utils.OrderedTable(), -- config values can be overridden in the test file to define @@ -309,7 +309,7 @@ local function build_test_env() checks_ok = 0, } for name, func in pairs(expect) do - env.expect[name] = wrap_expect(func, private) + env.expect[name] = wrap_expect(func, private, path) end setmetatable(env, {__index = _G}) return env, private @@ -345,9 +345,9 @@ local function finish_tests(done_command) end local function load_tests(file, tests) - local short_filename = file:sub((file:find('test') or -4)+5, -1) + local short_filename = file:sub((file:find('test') or -4) + 5, -1) print('Loading file: ' .. short_filename) - local env, env_private = build_test_env() + local env, env_private = build_test_env(file) local code, err = loadfile(file, 't', env) if not code then dfhack.printerr('Failed to load file: ' .. tostring(err)) diff --git a/depends/libexpat b/depends/libexpat index 3e877cbb3..6ac8628a3 160000 --- a/depends/libexpat +++ b/depends/libexpat @@ -1 +1 @@ -Subproject commit 3e877cbb3c9bc8f22946053e70490d2e5431f4d5 +Subproject commit 6ac8628a3c7a1677b27fb007db96f665b684a183 diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 5a5737b93..8ec347bef 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1007,17 +1007,19 @@ Fortress mode * ``dfhack.gui.pauseRecenter(pos[,pause])`` ``dfhack.gui.pauseRecenter(x,y,z[,pause])`` - Same as ``resetDwarfmodeView``, but also recenter if ``x`` isn't ``-30000``, and respects - RECENTER_INTERFACE_SHUTDOWN_MS (the delay before input is recognized when a recenter occurs) in DF's init.txt. + Same as ``resetDwarfmodeView``, but also recenter if ``x`` isn't ``-30000``. Respects + ``RECENTER_INTERFACE_SHUTDOWN_MS`` in DF's ``init.txt`` (the delay before input is recognized when a recenter occurs.) * ``dfhack.gui.recenterViewscreen(pos[,zoom])`` ``dfhack.gui.recenterViewscreen(x,y,z[,zoom])`` ``dfhack.gui.recenterViewscreen([zoom])`` Recenter the view on a position using a specific zoom type. If no position is given, - recenter on ``df.global.cursor``. Zoom types are ``df.report_zoom_type`` (see: `enum definition `_), - where ``Generic`` skips recentering and enforces valid view bounds (the same as x = -30000,) ``Item`` brings - the position onscreen without centering, and ``Unit`` centers the screen on the position. Default zoom type is Item. + recenter on ``df.global.cursor``. Zoom types are ``df.report_zoom_type`` + (see: `enum definition `_), + where ``df.report_zoom_type.Generic`` skips recentering and enforces valid view bounds (the same as x = -30000,) + ``df.report_zoom_type.Item`` brings the position onscreen without centering, and + ``df.report_zoom_type.Unit`` centers the screen on the position. Default zoom type is ``df.report_zoom_type.Item``. * ``dfhack.gui.revealInDwarfmodeMap(pos)`` @@ -1091,11 +1093,15 @@ Announcements * ``dfhack.gui.autoDFAnnouncement(report,text)`` ``dfhack.gui.autoDFAnnouncement(type,pos,text,color[,is_bright,unit1,unit2,is_sparring])`` - Takes a ``df.report_init`` (see: `structure definition `_) - and a string and processes them just like DF does. Sometimes this means the announcement won't occur. - Can also be built from parameters instead of a ``report_init``. Setting ``is_sparring`` to ``true`` means the report - will be added to sparring logs (if applicable) rather than hunting or combat. Text is parsed using ``&`` as an escape character, with ``&r`` - adding a blank line (equivalent to ``\n \n``,) ``&&`` being just ``&``, and any other combination causing neither character to display. + Takes a ``df.report_init`` (see: `structure definition `_) + and a string and processes them just like DF does. Can also be built from parameters instead of a ``report_init``. + Setting ``is_sparring`` to *true* means the report will be added to sparring logs (if applicable) rather than hunting or combat. + + The announcement will not display if units are involved and the player can't see them (or hear, for adventure mode sound announcement types.) + Text is parsed using ``&`` as an escape character, with ``&r`` adding a blank line (equivalent to ``\n \n``,) + ``&&`` being just ``&``, and any other combination causing neither character to display. + + If you want a guaranteed announcement without parsing, use ``dfhack.gui.showAutoAnnouncement`` instead. Other ~~~~~ diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua index 51f346bbd..952e8f734 100644 --- a/library/lua/gui/dialogs.lua +++ b/library/lua/gui/dialogs.lua @@ -36,7 +36,13 @@ end function MessageBox:getWantedFrameSize() local label = self.subviews.label local width = math.max(self.frame_width or 0, 20, #(self.frame_title or '') + 4) - return math.max(width, label:getTextWidth()), label:getTextHeight() + local text_area_width = label:getTextWidth() + if label.frame_inset then + -- account for scroll icons + text_area_width = text_area_width + (label.frame_inset.l or 0) + text_area_width = text_area_width + (label.frame_inset.r or 0) + end + return math.max(width, text_area_width), label:getTextHeight() end function MessageBox:onRenderFrame(dc,rect) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 21e03370b..c6e84d7a1 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -478,8 +478,16 @@ function Label:render_scroll_icons(dc, x, y1, y2) end end -function Label:postComputeFrame() - self:update_scroll_inset() +function Label:computeFrame(parent_rect) + local frame_rect,body_rect = Label.super.computeFrame(self, parent_rect) + + self.frame_rect = frame_rect + self.frame_body = parent_rect:viewport(body_rect or frame_rect) + + self:update_scroll_inset() -- frame_body is now set + + -- recalc with updated frame_inset + return Label.super.computeFrame(self, parent_rect) end function Label:preUpdateLayout() diff --git a/library/lua/test_util/mock.lua b/library/lua/test_util/mock.lua index c60646b77..8d253cc10 100644 --- a/library/lua/test_util/mock.lua +++ b/library/lua/test_util/mock.lua @@ -32,12 +32,17 @@ function _patch_impl(patches_raw, callback, restore_only) end --[[ + +Replaces `table[key]` with `value`, calls `callback()`, then restores the +original value of `table[key]`. + Usage: patch(table, key, value, callback) patch({ {table, key, value}, {table2, key2, value2}, }, callback) + ]] function mock.patch(...) local args = {...} @@ -57,12 +62,18 @@ function mock.patch(...) end --[[ + +Restores the original value of `table[key]` after calling `callback()`. + +Equivalent to: patch(table, key, table[key], callback) + Usage: restore(table, key, callback) restore({ {table, key}, {table2, key2}, }, callback) + ]] function mock.restore(...) local args = {...} @@ -81,9 +92,19 @@ function mock.restore(...) return _patch_impl(patches, callback, true) end -function mock.func(...) +--[[ + +Returns a callable object that tracks the arguments it is called with, then +passes those arguments to `callback()`. + +The returned object has the following properties: +- `call_count`: the number of times the object has been called +- `call_args`: a table of function arguments (shallow-copied) corresponding + to each time the object was called + +]] +function mock.observe_func(callback) local f = { - return_values = {...}, call_count = 0, call_args = {}, } @@ -101,11 +122,36 @@ function mock.func(...) end end table.insert(self.call_args, args) - return table.unpack(self.return_values) + return callback(...) end, }) return f end +--[[ + +Returns a callable object similar to `mock.observe_func()`, but which when +called, only returns the given `return_value`(s) with no additional side effects. + +Intended for use by `patch()`. + +Usage: + func(return_value [, return_value2 ...]) + +See `observe_func()` for a description of the return value. + +The return value also has an additional `return_values` field, which is a table +of values returned when the object is called. This can be modified. + +]] +function mock.func(...) + local f + f = mock.observe_func(function() + return table.unpack(f.return_values) + end) + f.return_values = {...} + return f +end + return mock diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 828c2632f..0dd50414e 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1370,46 +1370,15 @@ DFHACK_EXPORT void Gui::writeToGamelog(std::string message) namespace { // Utility functions for reports - /*bool parseReportString(std::vector *out, const std::string &str, size_t line_length = 73) - { + bool parseReportString(std::vector *out, const std::string &str, size_t line_length = 73) + { // parse a string into output strings like DF does for reports if (str.empty() || line_length == 0) return false; - string parsed = ""; + string parsed; size_t i = 0; - while (i < str.length()) - { - if (str[i] == '&') // escape character - { - i++; // ignore the '&' itself - if (i >= str.length()) - break; - - if (str[i] == 'r') // "&r" adds a blank line - parsed += "\n \n"; // DF adds a line with a space for some reason - else if (str[i] == '&') // "&&" is '&' - parsed += "&"; - // else next char is ignored - } - else - { - parsed += str[i]; - } - i++; - } - - return word_wrap(out, parsed, line_length, true); - }*/ - /*bool parseReportString(std::vector *out, const std::string &str, size_t line_length = 73) - { - if (str.empty() || line_length == 0) - return false; - - string parsed = ""; - size_t i = 0; - - while (i < str.length()) + do { if (str[i] == '&') // escape character { @@ -1419,95 +1388,21 @@ namespace if (str[i] == 'r') // "&r" adds a blank line { - word_wrap(out, parsed, line_length, true); + word_wrap(out, parsed, line_length, false, true); out->push_back(" "); // DF adds a line with a space for some reason - parsed = ""; + parsed.clear(); } else if (str[i] == '&') // "&&" is '&' - parsed += "&"; + parsed.push_back('&'); // else next char is ignored } else - { - parsed += str[i]; - } - i++; + parsed.push_back(str[i]); } + while (++i < str.length()); - if (parsed != "") - word_wrap(out, parsed, line_length, true); - - return true; - }*/ - bool parseReportString(std::vector *out, const std::string &str, size_t line_length) - { // out vector will contain strings cut to line_length, avoiding cutting up words - // Reverse-engineered from DF announcement code, fixes applied - - if (str.empty() || line_length == 0) - return false; - - bool ignore_space = false; - string current_line = ""; - size_t iter = 0; - do - { - if (ignore_space) - { - if (str[iter] == ' ') - continue; - ignore_space = false; - } - - if (str[iter] == '&') // escape character - { - iter++; // ignore the '&' itself - if (iter >= str.length()) - break; - - if (str[iter] == 'r') // "&r" adds a blank line - { - if (!current_line.empty()) - { - out->push_back(string(current_line)); - current_line = ""; - } - out->push_back(" "); // DF adds a line with a space for some reason - continue; // don't add 'r' to current_line - } - else if (str[iter] != '&') - { // not "&&", don't add character to current_line - continue; - } - } - - current_line += str[iter]; - if (current_line.length() > line_length) - { - size_t i = current_line.length(); // start of current word - size_t j; // end of previous word - while (--i > 0 && current_line[i] != ' '); // find start of current word - - if (i == 0) - { // need to push at least one char - j = i = line_length; // last char ends up on next line - } - else - { - j = i; - while (j > 1 && current_line[j - 1] == ' ') - j--; // consume excess spaces at the split point - } - out->push_back(current_line.substr(0, j)); // push string before j - - if (current_line[i] == ' ') - i++; // don't keep this space - current_line.erase(0, i); // current_line now starts at last word or is empty - ignore_space = current_line.empty(); // ignore leading spaces on new line - } - } while (++iter < str.length()); - - if (!current_line.empty()) - out->push_back(current_line); + if (parsed.length()) + word_wrap(out, parsed, line_length, false, true); return true; } @@ -1549,7 +1444,7 @@ namespace } int32_t check_repeat_report(vector &results) - { + { // returns the new repeat count, else 0 if (*gamemode == game_mode::DWARF && !results.empty() && world->status.reports.size() >= results.size()) { auto &reports = world->status.reports; @@ -1697,6 +1592,19 @@ bool Gui::addCombatReport(df::unit *unit, df::unit_report_type slot, int report_ return true; } +namespace +{ // An additional utility function for reports + bool add_proper_report(df::unit *unit, bool is_sparring, int report_index) + { + if (is_sparring) + return addCombatReport(unit, unit_report_type::Sparring, report_index); + else if (unit->job.current_job != NULL && unit->job.current_job->job_type == job_type::Hunt) + return addCombatReport(unit, unit_report_type::Hunting, report_index); + else + return addCombatReport(unit, unit_report_type::Combat, report_index); + } +} + bool Gui::addCombatReportAuto(df::unit *unit, df::announcement_flags mode, int report_index) { using df::global::world; @@ -1710,14 +1618,7 @@ bool Gui::addCombatReportAuto(df::unit *unit, df::announcement_flags mode, int r bool ok = false; if (mode.bits.UNIT_COMBAT_REPORT) - { - if (unit->flags2.bits.sparring) - ok |= addCombatReport(unit, unit_report_type::Sparring, report_index); - else if (unit->job.current_job && unit->job.current_job->job_type == job_type::Hunt) - ok |= addCombatReport(unit, unit_report_type::Hunting, report_index); - else - ok |= addCombatReport(unit, unit_report_type::Combat, report_index); - } + ok |= add_proper_report(unit, unit->flags2.bits.sparring, report_index); if (mode.bits.UNIT_COMBAT_REPORT_ALL_ACTIVE) { @@ -1783,28 +1684,24 @@ void Gui::showAutoAnnouncement( bool Gui::autoDFAnnouncement(df::report_init r, string message) { // Reverse-engineered from DF announcement code - if (!world->allow_announcements) { DEBUG(gui).print("Skipped announcement because world->allow_announcements is false:\n%s\n", message.c_str()); return false; - } - - df::announcement_flags a_flags; - if (is_valid_enum_item(r.type)) - a_flags = df::global::d_init->announcements.flags[r.type]; - else + } + else if (!is_valid_enum_item(r.type)) { WARN(gui).print("Invalid announcement type:\n%s\n", message.c_str()); return false; } - - if (message.empty()) + else if (message.empty()) { Core::printerr("Empty announcement %u\n", r.type); // DF would print this to errorlog.txt return false; } + df::announcement_flags a_flags = df::global::d_init->announcements.flags[r.type]; + // Check if the announcement will actually be announced if (*gamemode == game_mode::ADVENTURE) { @@ -1814,11 +1711,13 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) r.type != announcement_type::CONFLICT_CONVERSATION && r.type != announcement_type::MECHANISM_SOUND) { // If not sound, make sure we can see pos - if ((world->units.active.empty() || (r.unit1 != world->units.active[0] && r.unit2 != world->units.active[0])) && - ((Maps::getTileDesignation(r.pos)->whole & 0x10) == 0x0)) // Adventure mode uses this bit to determine current visibility - { - DEBUG(gui).print("Adventure mode announcement not heard:\n%s\n", message.c_str()); - return false; + if (world->units.active.empty() || (r.unit1 != world->units.active[0] && r.unit2 != world->units.active[0])) + { // Adventure mode reuses a dwarf mode digging designation bit to determine current visibility + if (!Maps::isValidTilePos(r.pos) || (Maps::getTileDesignation(r.pos)->whole & 0x10) == 0x0) + { + DEBUG(gui).print("Adventure mode announcement not detected:\n%s\n", message.c_str()); + return false; + } } } } @@ -1826,7 +1725,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) { // Dwarf mode if ((r.unit1 != NULL || r.unit2 != NULL) && (r.unit1 == NULL || Units::isHidden(r.unit1)) && (r.unit2 == NULL || Units::isHidden(r.unit2))) { - DEBUG(gui).print("Dwarf mode announcement not heard:\n%s\n", message.c_str()); + DEBUG(gui).print("Dwarf mode announcement not detected:\n%s\n", message.c_str()); return false; } @@ -1927,24 +1826,10 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) if (a_flags.bits.UNIT_COMBAT_REPORT) { if (r.unit1 != NULL) - { - if (r.flags.bits.hostile_combat) - success |= addCombatReport(r.unit1, unit_report_type::Combat, new_report_index); - else if (r.unit1->job.current_job != NULL && r.unit1->job.current_job->job_type == job_type::Hunt) - success |= addCombatReport(r.unit1, unit_report_type::Hunting, new_report_index); - else - success |= addCombatReport(r.unit1, unit_report_type::Sparring, new_report_index); - } + success |= add_proper_report(r.unit1, !r.flags.bits.hostile_combat, new_report_index); if (r.unit2 != NULL) - { - if (r.flags.bits.hostile_combat) - success |= addCombatReport(r.unit2, unit_report_type::Combat, new_report_index); - else if (r.unit2->job.current_job != NULL && r.unit2->job.current_job->job_type == job_type::Hunt) - success |= addCombatReport(r.unit2, unit_report_type::Hunting, new_report_index); - else - success |= addCombatReport(r.unit2, unit_report_type::Sparring, new_report_index); - } + success |= add_proper_report(r.unit2, !r.flags.bits.hostile_combat, new_report_index); } if (a_flags.bits.UNIT_COMBAT_REPORT_ALL_ACTIVE) @@ -1953,6 +1838,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) { if (recent_report(r.unit1, slot)) success |= addCombatReport(r.unit1, slot, new_report_index); + if (recent_report(r.unit2, slot)) success |= addCombatReport(r.unit2, slot, new_report_index); } @@ -2044,9 +1930,7 @@ df::coord Gui::getCursorPos() } void Gui::recenterViewscreen(int32_t x, int32_t y, int32_t z, df::report_zoom_type zoom) -{ - // Reverse-engineered from DF announcement code, also used when scrolling - +{ // Reverse-engineered from DF announcement code, also used when scrolling auto dims = getDwarfmodeViewDims(); int32_t w = dims.map_x2 - dims.map_x1 + 1; int32_t h = dims.map_y2 - dims.map_y1 + 1; @@ -2062,7 +1946,7 @@ void Gui::recenterViewscreen(int32_t x, int32_t y, int32_t z, df::report_zoom_ty } else // report_zoom_type::Item { - if (new_win_x > (x - 5)) + if (new_win_x > (x - 5)) // equivalent to: "while (new_win_x > x - 5) new_win_x -= 10;" new_win_x -= (new_win_x - (x - 5) - 1) / 10 * 10 + 10; if (new_win_y > (y - 5)) new_win_y -= (new_win_y - (y - 5) - 1) / 10 * 10 + 10; @@ -2072,32 +1956,28 @@ void Gui::recenterViewscreen(int32_t x, int32_t y, int32_t z, df::report_zoom_ty new_win_y += ((y + 5 - h) - new_win_y - 1) / 10 * 10 + 10; } - if (new_win_z != z) - ui_sidebar_menus->minimap.need_scan = true; new_win_z = z; } *df::global::window_x = clip_range(new_win_x, 0, (world->map.x_count - w)); *df::global::window_y = clip_range(new_win_y, 0, (world->map.y_count - h)); *df::global::window_z = clip_range(new_win_z, 0, (world->map.z_count - 1)); + + ui_sidebar_menus->minimap.need_render = true; + ui_sidebar_menus->minimap.need_scan = true; return; } void Gui::pauseRecenter(int32_t x, int32_t y, int32_t z, bool pause) -{ - // Reverse-engineered from DF announcement code - +{ // Reverse-engineered from DF announcement code if (*gamemode != game_mode::DWARF) return; resetDwarfmodeView(pause); + if (x != -30000) - { recenterViewscreen(x, y, z, report_zoom_type::Item); - ui_sidebar_menus->minimap.need_render = true; - ui_sidebar_menus->minimap.need_scan = true; - } if (init->input.pause_zoom_no_interface_ms > 0) { diff --git a/library/xml b/library/xml index a59495e8f..1dfe6c5ab 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit a59495e8f72115909772e6df20a7b9dec272f14c +Subproject commit 1dfe6c5ab9887507cdcdebdd9390352fe0bba2dd diff --git a/scripts b/scripts index 05d46b32a..741c84ada 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 05d46b32a3aff4f5f98534fdccfbf9ae88dd31a3 +Subproject commit 741c84ada2ec7fdd0083744afab294d9a1b6e370 diff --git a/test/library/gui/widgets.Label.lua b/test/library/gui/widgets.Label.lua new file mode 100644 index 000000000..49a75a235 --- /dev/null +++ b/test/library/gui/widgets.Label.lua @@ -0,0 +1,94 @@ +-- test -dhack/scripts/devel/tests -twidgets.Label + +local gui = require('gui') +local widgets = require('gui.widgets') + +local xtest = {} -- use to temporarily disable tests (change `function xtest.somename` to `function xxtest.somename`) +local wait = function(n) + delay(n or 30) -- enable for debugging the tests +end + +local fs = defclass(fs, gui.FramedScreen) +fs.ATTRS = { + frame_style = gui.GREY_LINE_FRAME, + frame_title = 'TestFramedScreen', + frame_width = 10, + frame_height = 10, + frame_inset = 0, + focus_path = 'test-framed-screen', +} + +function test.Label_correct_frame_body_with_scroll_icons() + local t = {} + for i = 1, 12 do + t[#t+1] = tostring(i) + t[#t+1] = NEWLINE + end + + function fs:init(args) + self:addviews{ + widgets.Label{ + view_id = 'text', + frame_inset = 0, + text = t, + --show_scroll_icons = 'right', + }, + } + end + + local o = fs{} + --o:show() + --wait() + expect.eq(o.subviews.text.frame_body.width, 9, "Label's frame_body.x2 and .width should be one smaller because of show_scroll_icons.") + --o:dismiss() +end + +function test.Label_correct_frame_body_with_few_text_lines() + local t = {} + for i = 1, 10 do + t[#t+1] = tostring(i) + t[#t+1] = NEWLINE + end + + function fs:init(args) + self:addviews{ + widgets.Label{ + view_id = 'text', + frame_inset = 0, + text = t, + --show_scroll_icons = 'right', + }, + } + end + + local o = fs{} + --o:show() + --wait() + expect.eq(o.subviews.text.frame_body.width, 10, "Label's frame_body.x2 and .width should not change with show_scroll_icons = false.") + --o:dismiss() +end + +function test.Label_correct_frame_body_without_show_scroll_icons() + local t = {} + for i = 1, 12 do + t[#t+1] = tostring(i) + t[#t+1] = NEWLINE + end + + function fs:init(args) + self:addviews{ + widgets.Label{ + view_id = 'text', + frame_inset = 0, + text = t, + show_scroll_icons = false, + }, + } + end + + local o = fs{} + --o:show() + --wait() + expect.eq(o.subviews.text.frame_body.width, 10, "Label's frame_body.x2 and .width should not change with show_scroll_icons = false.") + --o:dismiss() +end diff --git a/test/library/test_util/mock.lua b/test/library/test_util/mock.lua index 32aed28e1..1031a496a 100644 --- a/test/library/test_util/mock.lua +++ b/test/library/test_util/mock.lua @@ -208,9 +208,44 @@ function test.func_call_return_value() end function test.func_call_return_multiple_values() - local f = mock.func(7,5,{imatable='snarfsnarf'}) + local f = mock.func(7, 5, {imatable='snarfsnarf'}) local a, b, c = f() expect.eq(7, a) expect.eq(5, b) expect.table_eq({imatable='snarfsnarf'}, c) end + +function test.observe_func() + -- basic end-to-end test for common cases; + -- most edge cases are covered by mock.func() tests + local counter = 0 + local function target() + counter = counter + 1 + return counter + end + local observer = mock.observe_func(target) + + expect.eq(observer(), 1) + expect.eq(counter, 1) + expect.eq(observer.call_count, 1) + expect.table_eq(observer.call_args, {{}}) + + expect.eq(observer('x', 'y'), 2) + expect.eq(counter, 2) + expect.eq(observer.call_count, 2) + expect.table_eq(observer.call_args, {{}, {'x', 'y'}}) +end + +function test.observe_func_error() + local function target() + error('asdf') + end + local observer = mock.observe_func(target) + + expect.error_match('asdf', function() + observer('x') + end) + -- make sure the call was still tracked + expect.eq(observer.call_count, 1) + expect.table_eq(observer.call_args, {{'x'}}) +end