From 848ca5f44632e60302004dd15796cf94a3fee714 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 13 Oct 2026 23:21:54 -0400 Subject: [PATCH 1/8] llui's text tests check what they say they check Old test bugs TM23's idiom pass found (b9f739bcda), fixed. - altextundo: - "the saved mark follows forgotten steps" forgot nothing: the steps are held by weight, and 105 one-character steps weigh nothing. It types 300 KB steps, of which three are kept, and checks that the mark moves down as the oldest go and is lost with the step it stands on; - "a saved text among the steps forward is found forward" never put the mark ahead; it does now; - "two of 400 KB kept" checked only that one step could be undone; it undoes one and checks a second is there; - a redo's caret is required before it is read; an unused variable is gone. - altextview: - "Tab is a tab now" passed when Tab was not handled at all; it checks the key is taken, the atom's view not focused, and four spaces put in; - eight signal connections over locals were let go; each is a scoped_connection, as the file's other cases hold theirs; - substitutions, styles, an atom's index, a line's first row and the ruler are required before they are read. The ruler's also stops a mouse capture check passing with both sides null. - altextview_gl: - "the wave covers something" checked the test's own model of the wave; it checks what was drawn; - the fixture lets go of focus and kills its views, so a failed REQUIRE no longer skips that, and a raised font generation is put back at the case's end; - ten checks compared whole frames, or a map's strip of them, with == or !=, so a failure printed some 262,000 values; they count the pixels that differ and show the first; - the ruler is required before it is read. - altextediting, altextindent, altextlayout, altextsearch: optionals, a selection, layout rows and glyph runs, and matches are required before they are read; the helper that applies an indent and requires one selection is require_applied. "The text after a label goes to the next row" also passed with the label's row the second of two; it checks the next row. - altextsearch: two threads folded their answers into one flag as wrong = wrong || ..., so one could put back a false over the other's true; each only ever sets it. - altextgotoline: asking reset neither the quick open nor its popover, so a second ask could pass on the first's; require_asked resets and requires both, and require_chosen requires the field. A repeated block is gone. - altextdocument: an edit at a place past the end is checked to be clamped there, not only to do nothing. - alanchoredranges: hints and matches are required before they are read. - alprefixsums: an unused variable is gone. Each new or corrected check fails under a small change to the test's own input or expectation, at the check and not by crashing. Co-Authored-By: Claude Opus 5.5 --- indra/llui/tests/alanchoredranges_test.cpp | 2 + indra/llui/tests/alprefixsums_test.cpp | 11 +- indra/llui/tests/altextdocument_test.cpp | 6 +- indra/llui/tests/altextediting_test.cpp | 31 +++-- indra/llui/tests/altextgotoline_test.cpp | 43 +++--- indra/llui/tests/altextindent_test.cpp | 46 ++++--- indra/llui/tests/altextlayout_test.cpp | 9 +- indra/llui/tests/altextsearch_test.cpp | 12 +- indra/llui/tests/altextundo_test.cpp | 69 ++++++---- indra/llui/tests/altextview_gl_test.cpp | 152 ++++++++++++++------- indra/llui/tests/altextview_test.cpp | 49 +++++-- 11 files changed, 280 insertions(+), 150 deletions(-) diff --git a/indra/llui/tests/alanchoredranges_test.cpp b/indra/llui/tests/alanchoredranges_test.cpp index 11b6c2a2e9..aca1307e85 100644 --- a/indra/llui/tests/alanchoredranges_test.cpp +++ b/indra/llui/tests/alanchoredranges_test.cpp @@ -137,6 +137,7 @@ namespace [&gone](Hint& h) { gone.push_back(h.name); }); { INFO("before stays, after moves"); + REQUIRE(hints.size() == 3); CHECK(hints[0].name == "inside"); CHECK(hints[1].name == "before"); CHECK(hints[1].at == ALTextPos(0, 4)); @@ -168,6 +169,7 @@ namespace { INFO("past the edit's line, not looked at, and where it was"); CHECK(current == 3); + REQUIRE(matches.size() == 4); CHECK(matches[3] == on(1, 3, 5)); } matches.apply(doc.replace(on(1, 3, 5), "E"), ¤t); diff --git a/indra/llui/tests/alprefixsums_test.cpp b/indra/llui/tests/alprefixsums_test.cpp index a8e90590a0..8092df4629 100644 --- a/indra/llui/tests/alprefixsums_test.cpp +++ b/indra/llui/tests/alprefixsums_test.cpp @@ -35,16 +35,7 @@ namespace { struct alprefixsums_data { - // What the sums should say, added up the long way. - static S32 before(const std::vector& values, size_t index) - { - S32 sum = 0; - for (size_t i = 0; i < index && i < values.size(); ++i) - { - sum += values[i]; - } - return sum; - } + // The place a sum reaches, found the long way. static size_t reach(const std::vector& values, S32 sum) { size_t at = 0; diff --git a/indra/llui/tests/altextdocument_test.cpp b/indra/llui/tests/altextdocument_test.cpp index 876155369d..7a67d8de81 100644 --- a/indra/llui/tests/altextdocument_test.cpp +++ b/indra/llui/tests/altextdocument_test.cpp @@ -133,8 +133,10 @@ namespace CHECK(doc.version() == before); UNSCOPED_INFO("nobody told"); CHECK(heard.empty()); - UNSCOPED_INFO("clamped past the end"); - CHECK(doc.insert(ALTextPos(7, 7), "").nothing()); + const ALTextDocument::Edit past = doc.insert(ALTextPos(7, 7), ""); + CHECK(past.nothing()); + UNSCOPED_INFO("answered where it was asked for, clamped to the end"); + CHECK(past.range == ALTextRange(ALTextPos(0, 3), ALTextPos(0, 3))); } TEST_CASE_METHOD(altextdocument_data, "altextdocument: offsets and positions agree and the whole text kept follows edits", "[altextdocument]") diff --git a/indra/llui/tests/altextediting_test.cpp b/indra/llui/tests/altextediting_test.cpp index e1dcd4d4b9..2609d799de 100644 --- a/indra/llui/tests/altextediting_test.cpp +++ b/indra/llui/tests/altextediting_test.cpp @@ -160,10 +160,14 @@ namespace CHECK(applied(text, *j) == std::string("call(a, b)\n\nend")); UNSCOPED_INFO("the caret where the first join is"); CHECK(j->caret == ALTextPos(0, 7)); + const std::optional blank = joinLines(doc, 2, 4, false); + REQUIRE(blank.has_value()); UNSCOPED_INFO("a blank line takes no space"); - CHECK(applied(text, *joinLines(doc, 2, 4, false)) == std::string("call(a,\n b\n ) end")); + CHECK(applied(text, *blank) == std::string("call(a,\n b\n ) end")); + const std::optional kept = joinLines(doc, 0, 1, true); + REQUIRE(kept.has_value()); UNSCOPED_INFO("the blanks kept"); - CHECK(applied(text, *joinLines(doc, 0, 1, true)) == std::string("call(a, b\n )\n\nend")); + CHECK(applied(text, *kept) == std::string("call(a, b\n )\n\nend")); { INFO("one line is nothing to join"); CHECK_FALSE(joinLines(doc, 4, 4, false)); @@ -188,18 +192,22 @@ namespace CHECK(down->caret == ALTextPos(3, 0)); const std::optional up = moveLinesTo(doc, 3, 4, 0); + REQUIRE(up.has_value()); UNSCOPED_INFO("under a"); CHECK(applied(text, *up) == std::string("a\nd\ne\nb\nc")); UNSCOPED_INFO("the caret on e"); CHECK(up->caret == ALTextPos(2, 0)); const std::optional top = moveLinesTo(doc, 2, 2, -1); + REQUIRE(top.has_value()); UNSCOPED_INFO("to the top"); CHECK(applied(text, *top) == std::string("c\na\nb\nd\ne")); UNSCOPED_INFO("the caret on c"); CHECK(top->caret == ALTextPos(0, 0)); + const std::optional end = moveLinesTo(doc, 0, 0, 4); + REQUIRE(end.has_value()); UNSCOPED_INFO("to the end"); - CHECK(applied(text, *moveLinesTo(doc, 0, 0, 4)) == std::string("b\nc\nd\ne\na")); + CHECK(applied(text, *end) == std::string("b\nc\nd\ne\na")); UNSCOPED_INFO("into themselves, nothing"); CHECK_FALSE(moveLinesTo(doc, 1, 3, 2)); @@ -340,14 +348,21 @@ namespace { const std::string text = "a \nb\n\nc\t\n d\n\n\ne"; const ALTextDocument doc(text); + const std::optional trailing = joinLines(doc, 0, 1, false); + REQUIRE(trailing.has_value()); UNSCOPED_INFO("a line's trailing blanks stand for the space"); - CHECK(applied(text, *joinLines(doc, 0, 1, false)) == std::string("a b\n\nc\t\n d\n\n\ne")); + CHECK(applied(text, *trailing) == std::string("a b\n\nc\t\n d\n\n\ne")); + const std::optional tab = joinLines(doc, 3, 4, false); + REQUIRE(tab.has_value()); UNSCOPED_INFO("a tab as well"); - CHECK(applied(text, *joinLines(doc, 3, 4, false)) == std::string("a \nb\n\nc\td\n\n\ne")); + CHECK(applied(text, *tab) == std::string("a \nb\n\nc\td\n\n\ne")); + const std::optional onto_empty = joinLines(doc, 5, 7, false); + REQUIRE(onto_empty.has_value()); UNSCOPED_INFO("nothing joined onto yet takes no space"); - CHECK(applied(text, *joinLines(doc, 5, 7, false)) == std::string("a \nb\n\nc\t\n d\ne")); + CHECK(applied(text, *onto_empty) == std::string("a \nb\n\nc\t\n d\ne")); + const std::optional across_empty = joinLines(doc, 1, 3, false); + REQUIRE(across_empty.has_value()); UNSCOPED_INFO("an empty line between joins what comes after it with a space"); - CHECK(applied(text, *joinLines(doc, 1, 3, false)) == - std::string("a \nb c\t\n d\n\n\ne")); + CHECK(applied(text, *across_empty) == std::string("a \nb c\t\n d\n\n\ne")); } } diff --git a/indra/llui/tests/altextgotoline_test.cpp b/indra/llui/tests/altextgotoline_test.cpp index 79c8b51129..dfb16a34b9 100644 --- a/indra/llui/tests/altextgotoline_test.cpp +++ b/indra/llui/tests/altextgotoline_test.cpp @@ -106,9 +106,11 @@ namespace return *view; } - // Go to Line asked over the text, by lines counted as `base` says. - void ask(S32 base = 0) + // Go to Line asked over the text, by lines counted as `base` says: + // its field up, in a popover. + void require_asked(S32 base = 0) { + quick = nullptr; ALTextGoToLine::ask( [this](std::function chose, std::function escaped, std::function left) { quick = asker.ask({}, "Line, or line:column", "Go to Line", std::move(chose), view, 420, ALQuickOpen::heightForRows(1), @@ -122,12 +124,20 @@ namespace return name; }, [this](const ALTextPos& was) { went.push_back(was); }); + INFO("asked"); + REQUIRE(quick != nullptr); + REQUIRE(popover() != nullptr); } ALPopover* popover() const { return quick ? ALViewType::as(quick->getParentByType()) : nullptr; } // Return, as the field gives it. - void choose() { quick->findChild("query")->onCommit(); } + void require_chosen() + { + LLLineEditor* field = quick->findChild("query"); + REQUIRE(field != nullptr); + field->onCommit(); + } }; TEST_CASE_METHOD(altextgotoline_data, "altextgotoline: placeTyped reads line or line:column past blanks and a comma or as none", "[altextgotoline]") @@ -163,12 +173,7 @@ namespace { ALTextView& v = make("zero\none\ntwo\n\tthree\nfour\n"); v.setCaret(ALTextPos(1, 2)); - ask(); - { - INFO("asked"); - REQUIRE(quick != nullptr); - REQUIRE(popover() != nullptr); - } + require_asked(); UNSCOPED_INFO("nothing typed: says how many"); CHECK(word == std::string("GoToLineHint")); UNSCOPED_INFO("the first line, counted from one"); @@ -192,7 +197,7 @@ namespace UNSCOPED_INFO("nothing typed: back where it was"); CHECK(v.caret() == ALTextPos(1, 2)); quick->setQuery("5"); - choose(); + require_chosen(); UNSCOPED_INFO("kept"); CHECK(v.caret() == ALTextPos(4, 0)); { @@ -208,7 +213,7 @@ namespace { ALTextView& v = make("a\nb\nc\nd\n"); v.setCaret(ALTextPos(0, 1)); - ask(); + require_asked(); quick->setQuery("3"); popover()->escape(); UNSCOPED_INFO("escaped: back"); @@ -216,7 +221,7 @@ namespace UNSCOPED_INFO("nothing told"); CHECK(went.empty()); - ask(); + require_asked(); quick->setQuery("4"); popover()->onFocusLost(); UNSCOPED_INFO("looked away: it stands"); @@ -231,7 +236,7 @@ namespace TEST_CASE_METHOD(altextgotoline_data, "altextgotoline: lines counted from 0 as in a notecard make 0 the first line", "[altextgotoline]") { ALTextView& v = make("zero\none\ntwo\n"); - ask(-1); + require_asked(-1); UNSCOPED_INFO("the first line said as 0"); CHECK(said["[FIRST]"]() == std::string("0")); UNSCOPED_INFO("the last line, counted from 0"); @@ -253,7 +258,7 @@ namespace { ALTextView& v = make("zero\none\ntwo\n"); v.setCaret(ALTextPos(2, 1)); - ask(-1); + require_asked(-1); for (const char* typed : { "x", ":", "-3" }) { CAPTURE(typed); @@ -263,7 +268,7 @@ namespace } UNSCOPED_INFO("said as typed"); CHECK(said["[LINE]"]() == std::string("-3")); - choose(); + require_chosen(); UNSCOPED_INFO("Return leaves it there"); CHECK(v.caret() == ALTextPos(2, 1)); UNSCOPED_INFO("and tells of no way back"); @@ -274,7 +279,7 @@ namespace { ALTextView& v = make("zero\none\ntwo\n"); v.setCaret(ALTextPos(2, 1)); - ask(); + require_asked(); quick->setQuery("4294967298"); UNSCOPED_INFO("no such line"); CHECK(word == std::string("GoToLineNone")); @@ -292,7 +297,7 @@ namespace // The most a number holds, counted from 0: past the last line, not // round past the least. - ask(-1); + require_asked(-1); quick->setQuery("2147483647"); UNSCOPED_INFO("no such line, from 0"); CHECK(word == std::string("GoToLineNone")); @@ -307,7 +312,7 @@ namespace ALTextView& v = make("zero\none\ntwo\nthree\n"); const ALTextRange block(ALTextPos(0, 1), ALTextPos(1, 2)); v.setSelections(block, { ALTextRange(ALTextPos(3, 0), ALTextPos(3, 0)) }); - ask(); + require_asked(); UNSCOPED_INFO("opened: the selection as it was"); CHECK(v.selection() == block); UNSCOPED_INFO("and the caret besides it"); @@ -318,7 +323,7 @@ namespace UNSCOPED_INFO("and the other caret"); CHECK(v.otherSelections().size() == size_t(1)); - ask(); + require_asked(); quick->setQuery("3"); UNSCOPED_INFO("gone to the line typed"); CHECK(v.caret() == ALTextPos(2, 0)); diff --git a/indra/llui/tests/altextindent_test.cpp b/indra/llui/tests/altextindent_test.cpp index 256118bff0..5e2a5fc7ca 100644 --- a/indra/llui/tests/altextindent_test.cpp +++ b/indra/llui/tests/altextindent_test.cpp @@ -77,9 +77,10 @@ namespace } // A command at one selection, as its groups make it: the text left, // and where the selection goes, where its group places it. - static std::string applied(const std::string& text, const std::vector& groups, std::optional& placed) + static std::string require_applied(const std::string& text, const std::vector& groups, std::optional& placed) { const ALTextEditing::Combined combined = ALTextEditing::combine(groups, 1); + REQUIRE(combined.selections.size() == 1); ALTextDocument doc(text); std::vector> edits; for (const Replacement& one : combined.replacements) @@ -127,20 +128,26 @@ namespace const std::string text = "a\n\nb\n c"; const ALTextDocument doc(text); std::optional placed; - UNSCOPED_INFO("each line with anything on it a level in"); - CHECK(applied(text, indentLines(doc, { ALTextRange(ALTextPos(0, 1), ALTextPos(3, 3)) }, true, spaces), placed) == - std::string(" a\n\n b\n c")); + { + INFO("each line with anything on it a level in"); + CHECK(require_applied(text, indentLines(doc, { ALTextRange(ALTextPos(0, 1), ALTextPos(3, 3)) }, true, spaces), placed) == + std::string(" a\n\n b\n c")); + } UNSCOPED_INFO("the anchor moved with its line, and the caret too"); CHECK(placed == ALTextRange(ALTextPos(0, 5), ALTextPos(3, 7))); - UNSCOPED_INFO("a tab where tabs are typed"); - CHECK(applied(text, indentLines(doc, { ALTextRange(ALTextPos(0, 0), ALTextPos(0, 0)) }, true, tabs), placed) == - std::string("\ta\n\nb\n c")); + { + INFO("a tab where tabs are typed"); + CHECK(require_applied(text, indentLines(doc, { ALTextRange(ALTextPos(0, 0), ALTextPos(0, 0)) }, true, tabs), placed) == + std::string("\ta\n\nb\n c")); + } const std::string deep = "\tx\n y\n z"; const ALTextDocument deep_doc(deep); - UNSCOPED_INFO("a tab, or up to a level of spaces, out"); - CHECK(applied(deep, indentLines(deep_doc, { ALTextRange(ALTextPos(0, 0), ALTextPos(2, 1)) }, false, spaces), placed) == - std::string("x\n y\nz")); + { + INFO("a tab, or up to a level of spaces, out"); + CHECK(require_applied(deep, indentLines(deep_doc, { ALTextRange(ALTextPos(0, 0), ALTextPos(2, 1)) }, false, spaces), placed) == + std::string("x\n y\nz")); + } { INFO("the caret never before its line's start"); REQUIRE(placed); @@ -239,6 +246,7 @@ namespace return true; }; const Outdent closed = outdentAsTyped(braces_doc, ALTextPos(2, 9), ALTextPos(2, 9), '}', lsl, matched, AutoOutdent(), spaces); + REQUIRE(closed.replacement); UNSCOPED_INFO("level with the bracket's line"); CHECK(applied(braces, *closed.replacement) == std::string(" f() {\n x;\n }")); UNSCOPED_INFO("a bracket is kept for nothing"); @@ -273,16 +281,20 @@ namespace { const std::string code = "\tif x\n \ty\n z\nw"; const ALTextDocument indented(code); + const std::optional to_spaces = convertIndentation(indented, 0, 3, true, 4); + REQUIRE(to_spaces.has_value()); UNSCOPED_INFO("to spaces, a tab to its stop"); - CHECK(applied(code, *convertIndentation(indented, 0, 3, true, 4)) == - std::string(" if x\n y\n z\nw")); + CHECK(applied(code, *to_spaces) == std::string(" if x\n y\n z\nw")); + const std::optional to_tabs = convertIndentation(indented, 0, 3, false, 4); + REQUIRE(to_tabs.has_value()); UNSCOPED_INFO("to tabs, as far as they go"); - CHECK(applied(code, *convertIndentation(indented, 0, 3, false, 4)) == - std::string("\tif x\n\ty\n\t z\nw")); + CHECK(applied(code, *to_tabs) == std::string("\tif x\n\ty\n\t z\nw")); UNSCOPED_INFO("nothing to change is nothing"); CHECK_FALSE(convertIndentation(ALTextDocument(" a\nb"), 0, 1, true, 4)); + const std::optional measured = convertIndentation(ALTextDocument("\ta"), 0, 0, false, 4, 2); + REQUIRE(measured.has_value()); UNSCOPED_INFO("measured as the tabs were: a tab of two, two spaces at four"); - CHECK(applied("\ta", *convertIndentation(ALTextDocument("\ta"), 0, 0, false, 4, 2)) == std::string(" a")); + CHECK(applied("\ta", *measured) == std::string(" a")); } TEST_CASE_METHOD(altextindent_data, "altextindent: detect reads tabs or spaces and the level from the commonest step", "[altextindent]") @@ -333,8 +345,10 @@ namespace CHECK(from(0, 9) == std::string("one")); Options wide = spaces; wide.tabWidth = 8; + const std::optional wide_back = backspaceFrom(doc, ALTextPos(0, 8), wide); + REQUIRE(wide_back.has_value()); UNSCOPED_INFO("a level as wide as the tab"); - CHECK(backspaceFrom(doc, ALTextPos(0, 8), wide)->column == 0); + CHECK(wide_back->column == 0); } TEST_CASE_METHOD(altextindent_data, "altextindent: pasted lines go where they belong keeping their depth from the first", "[altextindent]") diff --git a/indra/llui/tests/altextlayout_test.cpp b/indra/llui/tests/altextlayout_test.cpp index 11958649e6..3231427d09 100644 --- a/indra/llui/tests/altextlayout_test.cpp +++ b/indra/llui/tests/altextlayout_test.cpp @@ -197,6 +197,7 @@ namespace UNSCOPED_INFO("a tab has width"); CHECK(space > 0.f); const ALTextLayout::Line& line = layout.line(0); + REQUIRE_FALSE(line.placed.empty()); UNSCOPED_INFO("the tab draws nothing"); CHECK(line.placed.front().face == nullptr); // 'ab' then a tab to the next stop: twice the first tab's width. @@ -394,6 +395,7 @@ namespace const ALTextLayout::Line& second = layout.line(1); UNSCOPED_INFO("'ab', the box, 'cd', the tab, 'z'"); REQUIRE(second.glyphs.size() == size_t(2 + 1 + 2 + 1 + 1)); + REQUIRE(second.placed.size() > 2u); UNSCOPED_INFO("the box draws nothing"); CHECK(second.placed[2].face == nullptr); UNSCOPED_INFO("the box is as wide as asked"); @@ -441,7 +443,7 @@ namespace UNSCOPED_INFO("the label is not broken across rows"); CHECK(layout.rowOf(0, 4) == layout.rowOf(0, 31)); UNSCOPED_INFO("but the text after it goes to the next row"); - CHECK((layout.rowOf(0, 32) > layout.rowOf(0, 4) || layout.rowCount(0) >= 2)); + CHECK(layout.rowOf(0, 32) > layout.rowOf(0, 4)); } TEST_CASE_METHOD(altextlayout_data, "altextlayout: a stretch in a font of its own makes its row as tall as that font", "[altextlayout]") @@ -469,6 +471,8 @@ namespace runs = { run }; layout.invalidateLine(0); const ALTextLayout::Line& line = layout.line(0); + REQUIRE_FALSE(line.rows.empty()); + REQUIRE(line.placed.size() > 17u); UNSCOPED_INFO("the row's text is as tall as the larger font"); CHECK(line.rows[0].textHeight == big->getLineSpacing()); UNSCOPED_INFO("and its ascent the larger"); @@ -724,6 +728,7 @@ namespace } ready(text.c_str()); const ALTextLayout::Line& line = layout.line(0); + REQUIRE_FALSE(line.rows.empty()); const ALTextLayout::Row& row = line.rows[0]; const ALTextLayout::Row all = ALTextLayout::rowWithin(line, row, -100.f, line.width + 100.f); { @@ -756,6 +761,7 @@ namespace REQUIRE(k < seen.glyphEnd); } } + REQUIRE(seen.glyphBegin + 2 <= seen.glyphEnd); UNSCOPED_INFO("one more before, no more"); CHECK(line.glyphs[seen.glyphBegin + 1].pen + line.glyphs[seen.glyphBegin + 1].advance - row.xStart >= from); UNSCOPED_INFO("one more after, no more"); @@ -1253,6 +1259,7 @@ namespace // A word after the text before 'b', which the caret at b's column // stands before. const ALTextLayout::Line& line = layout.line(0); + REQUIRE_FALSE(line.rows.empty()); F32 b_left = -1.f; for (const ALTextLayout::Glyph& glyph : line.glyphs) { diff --git a/indra/llui/tests/altextsearch_test.cpp b/indra/llui/tests/altextsearch_test.cpp index 0987cd429b..b75fb9e03d 100644 --- a/indra/llui/tests/altextsearch_test.cpp +++ b/indra/llui/tests/altextsearch_test.cpp @@ -89,6 +89,7 @@ namespace UNSCOPED_INFO("and says why"); CHECK_FALSE(error.empty()); const std::vector found = ALTextSearch::matches(doc, "([a-z])(\\d+)", options); + REQUIRE(found.size() == size_t(3)); UNSCOPED_INFO("groups filled"); CHECK(ALTextSearch::replacement(doc, found[1], "([a-z])(\\d+)", options, "$2$1") == std::string("22b")); options.regex = false; @@ -131,6 +132,7 @@ namespace UNSCOPED_INFO("mixed, as typed"); CHECK(ALTextSearch::replacement(doc, found[3], "hello", options, "wORld") == std::string("wORld")); const std::vector digits = ALTextSearch::matches(doc, "123", options); + REQUIRE(digits.size() == size_t(1)); UNSCOPED_INFO("no letters, as typed"); CHECK(ALTextSearch::replacement(doc, digits[0], "123", options, "Abc") == std::string("Abc")); options.preserveCase = false; @@ -335,12 +337,18 @@ namespace std::thread other([&]() { for (int round = 0; round < 200; ++round) { - wrong = wrong || ALTextSearch::matches(doc, "be\\w+", regex).size() != 3; + if (ALTextSearch::matches(doc, "be\\w+", regex).size() != 3) + { + wrong = true; + } } }); for (int round = 0; round < 200; ++round) { - wrong = wrong || ALTextSearch::matches(doc, "al\\w+|ga\\w+", regex).size() != 3; + if (ALTextSearch::matches(doc, "al\\w+|ga\\w+", regex).size() != 3) + { + wrong = true; + } } other.join(); UNSCOPED_INFO("each thread its own answers"); diff --git a/indra/llui/tests/altextundo_test.cpp b/indra/llui/tests/altextundo_test.cpp index 361da58978..8653cea67b 100644 --- a/indra/llui/tests/altextundo_test.cpp +++ b/indra/llui/tests/altextundo_test.cpp @@ -79,6 +79,7 @@ namespace } CHECK_FALSE(undo.canUndo()); caret = undo.redo(); + REQUIRE(caret.has_value()); UNSCOPED_INFO("redone"); CHECK(doc.text() == std::string("abc")); { @@ -148,7 +149,7 @@ namespace undo.markSaved(); UNSCOPED_INFO("saved is pristine"); CHECK(undo.isPristine()); - ALTextPos at = type(ALTextPos(0, 1), "y"); + type(ALTextPos(0, 1), "y"); UNSCOPED_INFO("changed"); CHECK_FALSE(undo.isPristine()); undo.undo(); @@ -199,44 +200,51 @@ namespace CHECK_FALSE(undo.canUndo()); } + // Steps of 300 KB, three of which the steps back hold: a fourth lets the + // oldest go, and the saved mark, counted from the bottom, moves down + // with it -- until the step that led to the saved text is let go too, + // and stepping back can never reach it, and the journal must not say it + // has. TEST_CASE_METHOD(altextundo_data, "altextundo: the saved mark follows forgotten steps and is lost once out of reach", "[altextundo]") { - // Five steps, saved, then more than the stack keeps: the steps - // that led to the saved text are gone, so stepping back can never - // reach it, and the journal must not say it has. - ALTextPos at = ALTextPos(0, 0); - for (S32 i = 0; i < 5; ++i) - { - at = type(at, "a", 2.0); - } + const std::string part(300 * 1024, 'a'); + ALTextPos at(0, 0); + at = type(at, part.c_str(), 2.0); + at = type(at, part.c_str(), 2.0); undo.markSaved(); UNSCOPED_INFO("saved"); CHECK(undo.isPristine()); - for (S32 i = 0; i < 100; ++i) - { - at = type(at, "b", 2.0); - } + at = type(at, part.c_str(), 2.0); + at = type(at, part.c_str(), 2.0); UNSCOPED_INFO("changed since"); CHECK_FALSE(undo.isPristine()); - for (S32 i = 0; i < 95; ++i) - { - undo.undo(); - } - UNSCOPED_INFO("five steps in force again, but not the saved five"); + undo.undo(); + UNSCOPED_INFO("two steps in force again, but not the saved two"); CHECK_FALSE(undo.isPristine()); - // Saved again with a full stack: another step drops one, and the - // mark with it keeps counting from the bottom. - for (S32 i = 0; i < 95; ++i) + undo.undo(); + UNSCOPED_INFO("back to the saved text, the mark having moved down"); + CHECK(undo.isPristine()); + undo.undo(); + UNSCOPED_INFO("and one step back past it, the oldest having gone"); + CHECK_FALSE(undo.canUndo()); + + // Forward again, and on until the step that led to the saved text is + // let go too. + for (S32 i = 0; i < 3; ++i) { undo.redo(); } - undo.markSaved(); - at = type(at, "c", 2.0); - UNSCOPED_INFO("changed"); + at = type(at, part.c_str(), 2.0); + UNSCOPED_INFO("the saved text at the bottom of the stack, still reached"); + CHECK(undo.savedText().has_value()); + type(at, part.c_str(), 2.0); + UNSCOPED_INFO("the step that led to it gone, and the saved text with it"); + CHECK_FALSE(undo.savedText().has_value()); + while (undo.undo()) + { + } + UNSCOPED_INFO("stepping back as far as it goes never finds it"); CHECK_FALSE(undo.isPristine()); - undo.undo(); - UNSCOPED_INFO("back to the saved text, the mark having moved down"); - CHECK(undo.isPristine()); } // A save point is taken when a text is sent to be saved, and marks that @@ -383,7 +391,10 @@ namespace CHECK(other.text() == std::string("alpha gamma")); // A saved text among the steps forward is found forward. + undo.markSaved(); undo.undo(); + UNSCOPED_INFO("saved a step forward"); + CHECK(undo.savedText() == std::optional("alpha beta")); undo.markSaved(); undo.redo(); UNSCOPED_INFO("saved a step back"); @@ -696,8 +707,10 @@ namespace put(1.0); big.markSaved(); put(3.0); - UNSCOPED_INFO("two of 400 KB kept"); + big.undo(); + UNSCOPED_INFO("two of 400 KB kept: one still back past the newest"); CHECK(big.canUndo()); + big.redo(); put(5.0); UNSCOPED_INFO("three written"); CHECK(big_doc.text().size() == part.size() * 3); diff --git a/indra/llui/tests/altextview_gl_test.cpp b/indra/llui/tests/altextview_gl_test.cpp index de6b2be60d..9c4e464beb 100644 --- a/indra/llui/tests/altextview_gl_test.cpp +++ b/indra/llui/tests/altextview_gl_test.cpp @@ -32,10 +32,10 @@ #include "altextruler.h" #include "altextview.h" #include "../llui.h" -#include "../llfocusmgr.h" #include "../lluictrlfactory.h" #include "alheadlessui_fixture.h" +#include "aluistatescope.h" #include @@ -43,6 +43,7 @@ #include #include #include +#include #include #include @@ -78,6 +79,49 @@ namespace return out; } + // Where two frames, or two strips of them, part: how many pixels differ + // and the first of them, so that a failure names a place rather than + // printing both whole. A row is width pixels of channels bytes, bottom + // row first; frames of different sizes differ by what the longer has + // over the shorter. + struct FrameDiff + { + size_t pixels = 0; + S32 x = -1; + S32 y = -1; + + bool operator==(const FrameDiff&) const = default; + }; + + std::ostream& operator<<(std::ostream& out, const FrameDiff& d) + { + if (d.pixels == 0) + { + return out << "no pixel"; + } + return out << d.pixels << " pixels, the first at (" << d.x << ", " << d.y << ")"; + } + + FrameDiff frameDiff(const std::vector& a, const std::vector& b, S32 width = W, S32 channels = 4) + { + FrameDiff d; + const size_t step = static_cast(channels); + const size_t common = std::min(a.size(), b.size()); + for (size_t i = 0; i + step <= common; i += step) + { + if (!std::equal(a.begin() + i, a.begin() + i + step, b.begin() + i)) + { + if (d.pixels++ == 0) + { + d.x = static_cast((i / step) % static_cast(width)); + d.y = static_cast((i / step) / static_cast(width)); + } + } + } + d.pixels += (std::max(a.size(), b.size()) - common + step - 1) / step; + return d; + } + // A keymap that is always typing the same line, and counts how often // the view asks it what the line is. struct TypingKeymap : public ALModalKeymap @@ -143,12 +187,33 @@ namespace { struct altextview_gl_data { + // Focus as the case found it, whatever it leaves it on. + ll_test::FocusScope focus; + // The views a case made, dying when it ends however it ends: a + // failed REQUIRE skips whatever comes after it. + std::vector views; + altextview_gl_data() { ll_test::uiGL(); ll_test::installWhiteTexture(); } + ~altextview_gl_data() + { + for (LLView* view : views) + { + view->die(); + } + } + + template + T* keep(T* view) + { + views.push_back(view); + return view; + } + // What a squiggle from x0 at height y covers of a pixel, worked out // afresh: how near the wave comes to the pixel's middle, the line // solid for three quarters of a point either side and fading over @@ -217,8 +282,8 @@ namespace const Difference d = differ(was, now, 40, 140); { CAPTURE(d.most, d.covered, d.compared); - UNSCOPED_INFO("the wave covers something"); - CHECK(d.covered > 0); + UNSCOPED_INFO("the squiggle covers something"); + CHECK(d.compared > 0); UNSCOPED_INFO("no pixel far from the wave's"); CHECK(d.most <= 6); UNSCOPED_INFO("as much covered as the wave"); @@ -290,8 +355,8 @@ namespace scale = was_scale; const Difference d = differ(was, now, 20, 220); CAPTURE(d.most, d.covered, d.compared); - UNSCOPED_INFO("the wave covers something"); - CHECK(d.covered > 0); + UNSCOPED_INFO("the squiggle covers something"); + CHECK(d.compared > 0); UNSCOPED_INFO("no pixel far from the wave's"); CHECK(d.most <= 6); UNSCOPED_INFO("as much covered as the wave"); @@ -316,7 +381,7 @@ namespace p.name = "view"; p.rect = LLRect(0, H, W, 0); p.default_text = text; - ALTextView* view = LLUICtrlFactory::create(p); + ALTextView* view = keep(LLUICtrlFactory::create(p)); view->setFont(LLFontGL::getFontMonospace()); view->setScrollMap(true); // The map's columns, at the right of the view. @@ -342,14 +407,13 @@ namespace UNSCOPED_INFO("the map draws its lines"); REQUIRE(std::adjacent_find(first.begin(), first.end(), std::not_equal_to()) != first.end()); UNSCOPED_INFO("the same map when nothing changed"); - CHECK(map_of() == first); + CHECK(frameDiff(map_of(), first, W - map_left) == FrameDiff()); view->document().replace(ALTextRange(ALTextPos(3, 4), ALTextPos(3, 5)), "\t"); const std::vector tabbed = map_of(); UNSCOPED_INFO("a space made a tab changes the map"); - CHECK(tabbed != first); + CHECK(frameDiff(tabbed, first, W - map_left).pixels > 0); UNSCOPED_INFO("and it stays as it now is"); - CHECK(map_of() == tabbed); - view->die(); + CHECK(frameDiff(map_of(), tabbed, W - map_left) == FrameDiff()); } // The code editor draws its rows in passes -- what is behind the text, // every row's glyphs in one call, then what is over it -- and each @@ -374,7 +438,7 @@ namespace p.rect = LLRect(0, H, W, 0); p.default_text = text; p.syntax = "lsl"; - ALCodeEditor* editor = LLUICtrlFactory::create(p); + ALCodeEditor* editor = keep(LLUICtrlFactory::create(p)); editor->setFont(LLFontGL::getFontMonospace()); // With the keyboard, so that a selection is drawn as the one being // worked in; read-only, so that no caret blinks between frames. @@ -422,13 +486,13 @@ namespace const std::vector plain = frame(); UNSCOPED_INFO("the same frame again"); - CHECK(frame() == plain); + CHECK(frameDiff(frame(), plain) == FrameDiff()); editor->setSelection(ALTextRange(ALTextPos(2, 0), ALTextPos(2, 20))); require_only_on("a selection", plain, frame(), 2); editor->setSelection(ALTextRange(ALTextPos(0, 0), ALTextPos(0, 0))); UNSCOPED_INFO("the selection gone"); - CHECK(frame() == plain); + CHECK(frameDiff(frame(), plain) == FrameDiff()); // A colour of its own over part of a line: each row's glyphs carry // their own colours into the one call. @@ -439,7 +503,7 @@ namespace require_only_on("a colour of its own", plain, frame(), 5); editor->clearStyles(); UNSCOPED_INFO("the colour gone"); - CHECK(frame() == plain); + CHECK(frameDiff(frame(), plain) == FrameDiff()); ALCodeEditor::Decoration d; d.range = ALTextRange(ALTextPos(7, 8), ALTextPos(7, 13)); @@ -459,8 +523,6 @@ namespace glFinish(); UNSCOPED_INFO("the frame is a handful of draws"); CHECK(capture.size() <= 25); - gFocusMgr.setKeyboardFocus(nullptr); - editor->die(); } // The preview of the lines under the mouse on the map is drawn over the @@ -482,13 +544,14 @@ namespace p.name = "view"; p.rect = LLRect(0, H, W, 0); p.default_text = text; - ALTextView* view = LLUICtrlFactory::create(p); + ALTextView* view = keep(LLUICtrlFactory::create(p)); view->setFont(LLFontGL::getFontMonospace()); view->setScrollMap(true); view->setScrollMapWidth(60); view->setBackgroundColor(LLColor4(0.1f, 0.1f, 0.12f, 1.f)); view->setTextColor(LLColor4(0.9f, 0.9f, 0.9f, 1.f)); ALTextRuler* map = view->findChild("ruler"); + REQUIRE(map != nullptr); // Every pixel of the view, as it draws. const auto drawn = [&]() { ll_test::uiGL().clearFramebuffer(); @@ -525,7 +588,6 @@ namespace UNSCOPED_INFO("the preview over the text, the map on the " << (left ? "left" : "right")); REQUIRE(changed(plain, shown, text_from, text_to) > 1000); } - view->die(); } // A row's squiggle is under its text: nothing at or above the baseline, @@ -585,7 +647,7 @@ namespace p.default_text = text; // The skin's colours are not loaded here: the caret's is given. p.cursor_color = LLUIColor(LLColor4::white); - ALTextView* view = LLUICtrlFactory::create(p); + ALTextView* view = keep(LLUICtrlFactory::create(p)); view->setFont(LLFontGL::getFontMonospace()); view->setSelectionColor(LLUIColor(LLColor4(0.2f, 0.4f, 0.9f, 0.6f))); // Shown, and still from frame to frame. @@ -636,7 +698,7 @@ namespace require_only_on("another caret", one, two, 2); view->setSelections(at(2, 5), { at(4, 0) }); UNSCOPED_INFO("the two the other way round, the same frame"); - CHECK(frame() == two); + CHECK(frameDiff(frame(), two) == FrameDiff()); const ALTextRange span(ALTextPos(7, 2), ALTextPos(7, 10)); view->setSelections(at(4, 0), { span }); @@ -644,7 +706,7 @@ namespace require_only_on("another selection, its caret at its end", one, other_span, 7); view->setSelections(span, { at(4, 0) }); UNSCOPED_INFO("the same as the main selection"); - CHECK(frame() == other_span); + CHECK(frameDiff(frame(), other_span) == FrameDiff()); // A caret on every line: the carets are one draw, not one each. std::vector many; @@ -669,8 +731,6 @@ namespace glFinish(); UNSCOPED_INFO("twelve carets cost what one does"); CHECK(with_many == capture.size()); - gFocusMgr.setKeyboardFocus(nullptr); - view->die(); } // A gap: its tint across the text and nothing else in it, the line // under it drawn as far down as the gap is tall, and the rows below @@ -690,7 +750,7 @@ namespace // black. p.text_color = LLUIColor(LLColor4::white); p.bg_visible = false; - ALTextView* view = LLUICtrlFactory::create(p); + ALTextView* view = keep(LLUICtrlFactory::create(p)); view->setFont(LLFontGL::getFontMonospace()); const auto frame = [&]() { ll_test::uiGL().clearFramebuffer(); @@ -745,7 +805,6 @@ namespace CHECK(lit(after, 5, 1, 1, 200) >= width * (row_h - 2)); CHECK(lit(after, 6, 1, 1, 200) == 0); } - view->die(); } // What goes by row is cut at the text's edge: a code editor scrolled @@ -769,7 +828,7 @@ namespace { text += "MMMM " + std::to_string(n) + "\n"; } - ALCodeEditor* editor = LLUICtrlFactory::create(p); + ALCodeEditor* editor = keep(LLUICtrlFactory::create(p)); editor->setFont(LLFontGL::getFontMonospace()); editor->setText(text); const S32 row_h = editor->layout().rowHeight(); @@ -800,7 +859,6 @@ namespace // A clip takes in the row its top edge names, as a rect is drawn. UNSCOPED_INFO("nothing of it above the text"); CHECK(drawnIn(with_atom, x0, x0 + 40, editor->getRect().mBottom + area.mTop + 1, H) == 0); - editor->die(); } // The ruler's marks cost what one does, however many lines have one: a @@ -821,7 +879,7 @@ namespace p.name = "view"; p.rect = LLRect(0, H, W, 0); p.default_text = text; - ALTextView* view = LLUICtrlFactory::create(p); + ALTextView* view = keep(LLUICtrlFactory::create(p)); view->setFont(LLFontGL::getFontMonospace()); ALTextRuler* ruler = view->findChild("ruler"); // A frame's draws, the ruler put in its place by a frame before it. @@ -857,7 +915,6 @@ namespace const size_t every = draws(); UNSCOPED_INFO("two hundred marks and a gap's cost what one does"); CHECK(every == one); - view->die(); } // A tint behind every line in sight and every gap costs what one does, @@ -880,7 +937,7 @@ namespace p.name = "view"; p.rect = LLRect(0, H, W, 0); p.default_text = text; - ALTextView* view = LLUICtrlFactory::create(p); + ALTextView* view = keep(LLUICtrlFactory::create(p)); view->setFont(LLFontGL::getFontMonospace()); // A frame's draws, laid out by a frame before it. const auto draws = [&]() { @@ -942,7 +999,6 @@ namespace const size_t every_line = draws(); UNSCOPED_INFO("underlines on every line cost what one line's do"); CHECK(every_line == one_line); - view->die(); } // The blocks the gutter draws its markers by are found again once the @@ -967,7 +1023,7 @@ namespace p.rect = LLRect(0, H, W, 0); p.syntax = "lsl"; p.show_fold_markers = true; - ALCodeEditor* editor = LLUICtrlFactory::create(p); + ALCodeEditor* editor = keep(LLUICtrlFactory::create(p)); editor->setFont(LLFontGL::getFontMonospace()); editor->setText(text); const S32 last = editor->document().lineCount() - 1; @@ -990,7 +1046,6 @@ namespace frame(); UNSCOPED_INFO("lexed to its end: the block gone"); CHECK(editor->foldRegions().empty()); - editor->die(); } // What is being composed is underlined under its own text on each row @@ -1016,7 +1071,7 @@ namespace // black. p.text_color = LLUIColor(LLColor4::white); p.bg_visible = false; - ALTextView* view = LLUICtrlFactory::create(p); + ALTextView* view = keep(LLUICtrlFactory::create(p)); view->setFont(LLFontGL::getFontMonospace()); view->setWordWrap(true); const auto frame = [&]() { @@ -1095,7 +1150,6 @@ namespace CHECK(changed_in(second, 0, second_from) == 0); UNSCOPED_INFO("nothing on the second row past where it ends"); CHECK(changed_in(second, second_to, W) == 0); - view->die(); } // The preview of the lines under the mouse on the map numbers them as @@ -1117,7 +1171,7 @@ namespace p.name = "editor"; p.rect = LLRect(0, H, W, 0); p.default_text = text; - ALCodeEditor* editor = LLUICtrlFactory::create(p); + ALCodeEditor* editor = keep(LLUICtrlFactory::create(p)); editor->setFont(LLFontGL::getFontMonospace()); // No gutter, so that only the preview says a line's number; read // only, so that no caret blinks between frames. @@ -1164,7 +1218,6 @@ namespace editor->setLineNumberBase(0); UNSCOPED_INFO("and from one again as before"); CHECK(changed(from_one, drawn()) == 0); - editor->die(); } // A gap put between the lines moves the map's rows below it down, each @@ -1193,7 +1246,7 @@ namespace p.name = "view"; p.rect = LLRect(0, H, W, 0); p.default_text = text; - ALTextView* made = LLUICtrlFactory::create(p); + ALTextView* made = keep(LLUICtrlFactory::create(p)); made->setFont(LLFontGL::getFontMonospace()); made->setScrollMap(true); made->setScrollMapWidth(60); @@ -1232,13 +1285,11 @@ namespace drawn_first->setLineAnnotations(gapped); const std::vector after = map_of(drawn_first); UNSCOPED_INFO("the gap moved the map"); - CHECK(after != before); + CHECK(frameDiff(after, before, 60, 3).pixels > 0); ALTextView* gapped_first = make(); gapped_first->setLineAnnotations(gapped); UNSCOPED_INFO("as a view with the gap from the start draws it"); - CHECK(after == map_of(gapped_first)); - drawn_first->die(); - gapped_first->die(); + CHECK(frameDiff(after, map_of(gapped_first), 60, 3) == FrameDiff()); } // The code editor's words beside the text -- an inlay's in its pill, a @@ -1256,7 +1307,7 @@ namespace p.name = "editor"; p.rect = LLRect(0, H, W, 0); p.syntax = "lsl"; - ALCodeEditor* editor = LLUICtrlFactory::create(p); + ALCodeEditor* editor = keep(LLUICtrlFactory::create(p)); editor->setFont(LLFontGL::getFontMonospace()); // Read-only, so that no caret blinks between frames. editor->setReadOnly(true); @@ -1306,7 +1357,6 @@ namespace const size_t every = draws(); UNSCOPED_INFO("hints, notes and tabs on every line cost what one line's do"); CHECK(every == one); - editor->die(); } // A selection that goes on past a line's end is drawn a little past the @@ -1327,7 +1377,7 @@ namespace // black. p.text_color = LLUIColor(LLColor4::white); p.bg_visible = false; - ALTextView* view = LLUICtrlFactory::create(p); + ALTextView* view = keep(LLUICtrlFactory::create(p)); view->setFont(LLFontGL::getFontMonospace()); view->setSelectionColor(LLUIColor(LLColor4(0.f, 0.f, 1.f, 1.f))); // The first line in from the edge, as a log's are. @@ -1359,7 +1409,6 @@ namespace CHECK(past > 0); UNSCOPED_INFO("a little, not as far again as the first line is in"); CHECK(past <= 10); - view->die(); } // The band a modal keymap has under the text is read and measured once @@ -1377,7 +1426,7 @@ namespace p.name = "view"; p.rect = LLRect(0, H, W, 0); p.default_text = "text"; - ALTextView* view = LLUICtrlFactory::create(p); + ALTextView* view = keep(LLUICtrlFactory::create(p)); view->setFont(LLFontGL::getFontMonospace()); S32 asked = 0; auto keymap = std::make_unique(); @@ -1396,11 +1445,16 @@ namespace frame(); UNSCOPED_INFO("not again while nothing moved"); CHECK(asked == once); - // What a font reload does: the faces swapped under the same font. + // What a font reload does: the faces swapped under the same font; + // put back however the case ends, for the views of the cases after. + struct Generation + { + const S32 was = LLFontGL::sResolutionGeneration; + ~Generation() { LLFontGL::sResolutionGeneration = was; } + } generation; ++LLFontGL::sResolutionGeneration; frame(); UNSCOPED_INFO("read again once the fonts were loaded again"); CHECK(asked > once); - view->die(); } } diff --git a/indra/llui/tests/altextview_test.cpp b/indra/llui/tests/altextview_test.cpp index 4a95e41065..27ae1f8c04 100644 --- a/indra/llui/tests/altextview_test.cpp +++ b/indra/llui/tests/altextview_test.cpp @@ -100,6 +100,14 @@ namespace } } + // Where a line's first row ends. + S32 firstRowEnd(S32 line) + { + const ALTextLayout::Line& laid = view->layout().line(line); + REQUIRE_FALSE(laid.rows.empty()); + return laid.rows[0].end; + } + // The local point of a column on a line, in the middle of its row. void pointOf(S32 line, S32 column, S32& x, S32& y) { @@ -652,7 +660,7 @@ namespace CHECK(v.caret() == ALTextPos(0, 4)); // A click on the label follows the link once let go of. std::string followed; - v.onLinkClicked([&followed](const ALTextView::Substitution& s) { followed = s.value.asString(); }); + boost::signals2::scoped_connection heard = v.onLinkClicked([&followed](const ALTextView::Substitution& s) { followed = s.value.asString(); }); S32 x, y; pointOf(0, 4, x, y); x += 4; @@ -681,9 +689,11 @@ namespace // An edit above slides it; one through it drops it. v.setReadOnly(false); v.document().insert(ALTextPos(0, 0), "1\n"); + REQUIRE(v.substitutions().size() == size_t(1)); UNSCOPED_INFO("slid down a line"); CHECK(v.substitutions().front().range.begin == ALTextPos(1, 4)); v.document().insert(ALTextPos(1, 0), "ab"); + REQUIRE(v.substitutions().size() == size_t(1)); UNSCOPED_INFO("slid along"); CHECK(v.substitutions().front().range == ALTextRange(ALTextPos(1, 6), ALTextPos(1, 34))); v.document().remove(ALTextRange(ALTextPos(1, 10), ALTextPos(1, 12))); @@ -728,7 +738,7 @@ namespace UNSCOPED_INFO("a click inside lands at a side"); CHECK((v.posAtLocal(x, y, true) == ALTextPos(0, 2) || v.posAtLocal(x, y, true) == ALTextPos(0, 5))); S32 clicked = 0; - v.onAtomClicked([&clicked](const ALTextView::Atom& a) { clicked = a.value.asInteger(); }); + boost::signals2::scoped_connection heard = v.onAtomClicked([&clicked](const ALTextView::Atom& a) { clicked = a.value.asInteger(); }); v.handleMouseDown(x, y, MASK_NONE); v.handleMouseUp(x, y, MASK_NONE); UNSCOPED_INFO("clicked"); @@ -889,6 +899,7 @@ namespace v.document().insert(ALTextPos(0, 0), "\n"); { INFO("slid down a line"); + REQUIRE(v.styles().size() == size_t(2)); CHECK(v.styles()[0].range.begin == ALTextPos(1, 0)); CHECK(v.styles()[1].range.begin == ALTextPos(2, 5)); } @@ -1100,8 +1111,12 @@ namespace // An edited text keeps Tab for itself; F6 goes into the views. v.setReadOnly(false); v.setCaret(ALTextPos(0, 0)); - UNSCOPED_INFO("Tab is a tab now"); - CHECK((!v.handleKeyHere(KEY_TAB, MASK_NONE) || !v.atomViewFocused())); + { + INFO("Tab is a tab now: soft, to the first stop, the keyboard kept"); + CHECK(v.handleKeyHere(KEY_TAB, MASK_NONE)); + CHECK_FALSE(v.atomViewFocused()); + CHECK(v.document().line(0) == " line " + ALTextView::atomPlaceholder() + " 1"); + } require_key(KEY_F6); UNSCOPED_INFO("F6 goes into the first view"); CHECK(second->hasFocus()); @@ -1292,8 +1307,10 @@ namespace REQUIRE(found->value.asInteger() == 2); REQUIRE(found->at.column == glyph.cluster); } + const size_t laid_as = static_cast(glyph.substitution - (1 << 30)); + REQUIRE(laid_as < v.atoms().size()); UNSCOPED_INFO("where the number it was laid out with is another's now"); - REQUIRE(v.atoms()[static_cast(glyph.substitution - (1 << 30))].value.asInteger() != 2); + REQUIRE(v.atoms()[laid_as].value.asInteger() != 2); } UNSCOPED_INFO("a box on the line"); CHECK(seen); @@ -2454,6 +2471,7 @@ namespace v.setScrollMap(true); v.setScrollMapWidth(80); ALTextRuler* ruler = v.findChild("ruler"); + REQUIRE(ruler); UNSCOPED_INFO("the press is taken"); CHECK(v.handleMouseDown(360, 100, MASK_NONE)); { @@ -3344,7 +3362,7 @@ namespace lines[5].gapStop = true; v.setLineAnnotations(lines); S32 moved = 0; - v.onCaretMoved([&moved]() { ++moved; }); + boost::signals2::scoped_connection heard = v.onCaretMoved([&moved]() { ++moved; }); v.setCaret(ALTextPos(1, 2)); moved = 0; @@ -3436,7 +3454,7 @@ namespace } ALTextView& v = make(many.c_str()); S32 told = 0; - v.onScrolled([&told]() { ++told; }); + boost::signals2::scoped_connection heard = v.onScrolled([&told]() { ++told; }); v.setScrollY(60); UNSCOPED_INFO("scrolled"); CHECK(told == 1); @@ -3485,7 +3503,7 @@ namespace v.setLineAnnotations(lines); v.setCaret(ALTextPos(4, 2)); U32 told = 0; - v.onTextChanged([&told]() { ++told; }); + boost::signals2::scoped_connection heard = v.onTextChanged([&told]() { ++told; }); // Lines one and two, one line now. v.replaceText(ALTextRange(ALTextPos(1, 0), ALTextPos(2, 3)), "ONE"); CHECK(v.text() == std::string("xzero\nONE\nthree\nfour")); @@ -3714,7 +3732,7 @@ namespace v.setCaret(ALTextPos(1, 2)); S32 told = 0; S32 seen = -2; - v.onCaretMoved([&]() { + boost::signals2::scoped_connection heard = v.onCaretMoved([&]() { ++told; seen = v.caretGap(); }); @@ -3769,7 +3787,7 @@ namespace // How many letters a row holds: a word too long for one breaks // where it overflows. v.setText(std::string(200, 'a')); - const S32 fits = v.layout().line(0).rows[0].end; + const S32 fits = firstRowEnd(0); UNSCOPED_INFO("a row holds a few letters"); CHECK(fits > 4); // Rows of 'a..a ', 'bb ' -- the word after it does not fit beside @@ -3806,7 +3824,7 @@ namespace ALTextView& v = make(""); v.setWordWrap(true); v.setText(std::string(200, 'a')); - const S32 fits = v.layout().line(0).rows[0].end; + const S32 fits = firstRowEnd(0); UNSCOPED_INFO("a row holds a few letters"); CHECK(fits > 6); // A word too long for a row, shown as it is: all of the first two @@ -3822,7 +3840,7 @@ namespace UNSCOPED_INFO("three rows"); CHECK(v.layout().rowCount(0) == 3); std::string followed; - v.onLinkClicked([&followed](const ALTextView::Substitution& s) { followed = s.value.asString(); }); + boost::signals2::scoped_connection heard = v.onLinkClicked([&followed](const ALTextView::Substitution& s) { followed = s.value.asString(); }); const auto click = [&](S32 column) { S32 x = 0, y = 0; pointOf(0, column, x, y); @@ -3861,7 +3879,7 @@ namespace CHECK(v.layout().line(0).rows[0].end == 5); } S32 clicked = 0; - v.onAtomClicked([&clicked](const ALTextView::Atom& a) { clicked = a.value.asInteger(); }); + boost::signals2::scoped_connection heard = v.onAtomClicked([&clicked](const ALTextView::Atom& a) { clicked = a.value.asInteger(); }); S32 x, y; pointOf(0, 2, x, y); x += picture.width / 2; @@ -4264,8 +4282,9 @@ namespace v.setScrollMap(true); v.setScrollMapWidth(80); ALTextRuler* ruler = v.findChild("ruler"); - S32 top = 0; - S32 bottom = 0; + REQUIRE(ruler); + S32 top = 0; + S32 bottom = 0; ll_test::TextViewProbe::mapWindow(*ruler, top, bottom); { INFO("the window at the map's top"); From 7d911b2965bc29485d2f61365220f59076be174a Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 13 Oct 2026 23:21:54 -0400 Subject: [PATCH 2/8] llui's code editor and vim tests check what they say they check Old test bugs TM23's idiom pass found (b9f739bcda), fixed. - alcodeeditor: - completions, decorations, highlights, fixes, the signature, the card and what a provider was asked are required before they are read. Where an open list was only CHECKed before its first item was read, the CHECK is a REQUIRE: the editor shows a list only while it has one; - the fold parity check required each line's state only once one was wrong, so a passing run never ran it; it notes the first wrong line and requires none, once a step; - the row colours case never killed its own editor; it does. - alchangepeek: - "F7 says nothing with the peek closed" checked again that the peek was closed; it checks F7 is not taken; - three peekChange() results went unchecked; the changes since saved are required before they are read. - alsyntaxgrammars, alsyntaxhighlighter: the load messages were built before the loads that fill their error, so a failure printed an empty one; each load's result is kept and checked after it. A grammar found by extension, a state's frames and a line's first and last spans are required before they are read. - alcompletionmodel: lists are required before their first item is read; an unused member is gone. - alfixlistmodel, alsnippetsession: fixes, a join, tab stops and mirrors are required before they are read. - alvimkeymap: - two CHECK(true)s: "recording says so while it runs" checks it while it runs, and "the tab gone: nothing" checks the : history ran no line, as "vim gone" now also does; - "none of them unknown" checked only the last of fifteen abbreviations; each is checked as it runs; - listings are required before they are read. - alvimmappings: parsed keys and matches are required before they are read. Each new or corrected check fails under a small change to the test's own input or expectation, at the check and not by crashing. Co-Authored-By: Claude Opus 5.5 --- indra/llui/tests/alchangepeek_test.cpp | 18 +++--- indra/llui/tests/alcodeeditor_test.cpp | 47 +++++++++++---- indra/llui/tests/alcompletionmodel_test.cpp | 11 +--- indra/llui/tests/alfixlistmodel_test.cpp | 2 + indra/llui/tests/alsnippetsession_test.cpp | 5 ++ indra/llui/tests/alsyntaxgrammars_test.cpp | 30 ++++++---- indra/llui/tests/alsyntaxhighlighter_test.cpp | 16 +++-- indra/llui/tests/alvimkeymap_test.cpp | 60 ++++++++++++------- indra/llui/tests/alvimmappings_test.cpp | 19 ++++-- 9 files changed, 138 insertions(+), 70 deletions(-) diff --git a/indra/llui/tests/alchangepeek_test.cpp b/indra/llui/tests/alchangepeek_test.cpp index 9308c83424..957a809567 100644 --- a/indra/llui/tests/alchangepeek_test.cpp +++ b/indra/llui/tests/alchangepeek_test.cpp @@ -189,7 +189,7 @@ namespace TEST_CASE_METHOD(alchangepeek_data, "alchangepeek: take_back reverts a change as one undo step and other edits close it", "[alchangepeek]") { ALCodeEditor& e = make("a\nb\nc\nd\ne", "a\nB\nc\nx\nd"); - e.peekChange(1); + CHECK(e.peekChange(1)); ALChangePeek& peek = *e.changePeek(); press(peek, "take_back"); UNSCOPED_INFO("made as saved"); @@ -269,7 +269,7 @@ namespace TEST_CASE_METHOD(alchangepeek_data, "alchangepeek: changed words are marked in the peek and the editor until it closes", "[alchangepeek]") { ALCodeEditor& e = make("integer count = 1;\nstring s;", "integer total = 2;\nstring s;"); - e.peekChange(0); + CHECK(e.peekChange(0)); ALChangePeek& peek = *e.changePeek(); const auto has = [](const ALTextRange& range, S32 line, S32 begin, S32 end) { return range.begin.line == line && range.begin.column <= begin && range.end.column >= end; @@ -294,7 +294,7 @@ namespace editor->die(); editor = nullptr; ALCodeEditor& f = make("a\nb", "a\nput in\nb"); - f.peekChange(1); + CHECK(f.peekChange(1)); { INFO("nothing marked"); CHECK(f.highlights(ALCodeEditor::Highlight::Change).empty()); @@ -497,6 +497,7 @@ namespace INFO("undone: again, three"); CHECK(e.perform(ALEditorCommand::Undo)); CHECK(e.changesSinceSaved() != edited); + REQUIRE(e.changesSinceSaved()); CHECK(e.changesSinceSaved()->changes.size() == 3); } e.resetDirty(); @@ -507,8 +508,11 @@ namespace } // Changed by its document, which no journal is told of: again. e.document().replace(ALTextRange(ALTextPos(0, 0), ALTextPos(0, 0)), "z"); - UNSCOPED_INFO("the text as it is"); - CHECK(e.changesSinceSaved()->now.front() == "za"); + { + INFO("the text as it is"); + REQUIRE(e.changesSinceSaved()); + CHECK(e.changesSinceSaved()->now.front() == "za"); + } } TEST_CASE_METHOD(alchangepeek_data, "alchangepeek: an edit above a peek that adds or takes lines closes it leaving no gap", "[alchangepeek]") @@ -606,8 +610,8 @@ namespace CHECK_FALSE(peek.isOpen()); CHECK(e.lineAnnotation(2).gap == 0); } - UNSCOPED_INFO("closed, F7 is the text's again"); - CHECK_FALSE(peek.isOpen()); + UNSCOPED_INFO("closed, F7 is the text's again: with no misspelling to go to, nothing takes it"); + CHECK_FALSE(e.handleKeyHere(KEY_F7, MASK_NONE)); e.setModalKeymap(std::make_unique()); { diff --git a/indra/llui/tests/alcodeeditor_test.cpp b/indra/llui/tests/alcodeeditor_test.cpp index 7990ca5def..4a3aa80a0b 100644 --- a/indra/llui/tests/alcodeeditor_test.cpp +++ b/indra/llui/tests/alcodeeditor_test.cpp @@ -185,6 +185,7 @@ namespace CHECK(e.markAt(2) == ALCodeEditor::Mark::Error); CHECK(e.markAt(1) == ALCodeEditor::Mark::None); } + REQUIRE_FALSE(e.decorations().empty()); UNSCOPED_INFO("the decoration slid"); CHECK(e.decorations().front().range.begin.line == 3); e.document().replace(ALTextRange(ALTextPos(3, 0), ALTextPos(3, 2)), "TH"); @@ -318,7 +319,7 @@ namespace type("u"); { INFO("still open, narrowed"); - CHECK(e.completionOpen()); + REQUIRE(e.completionOpen()); CHECK(e.completions()[0].text == "count"); } key(KEY_RETURN); @@ -351,7 +352,7 @@ namespace type(" cu"); { INFO("the provider's word"); - CHECK(e.completionOpen()); + REQUIRE(e.completionOpen()); CHECK(e.completions()[0].text == "custom"); CHECK(e.completions()[0].detail == "made up"); } @@ -394,7 +395,7 @@ namespace type("a"); { INFO("narrowing keeps it"); - CHECK(e.completionOpen()); + REQUIRE(e.completionOpen()); CHECK(e.completions()[0].text == "abacus"); } UNSCOPED_INFO("no new request while narrowing"); @@ -1218,10 +1219,12 @@ namespace REQUIRE(e.completions().size() == 4); CHECK(e.completions()[0].text == "sayAgain"); } + REQUIRE(e.chosenCompletion() >= 0); UNSCOPED_INFO("the choice is still SAY_CHANNEL"); CHECK(e.completions()[e.chosenCompletion()].text == std::string("SAY_CHANNEL")); // More typed starts from the best. type("i"); + REQUIRE(e.chosenCompletion() >= 0); UNSCOPED_INFO("narrowed to the best"); CHECK(e.completions()[e.chosenCompletion()].text == std::string("saying")); key(KEY_ESCAPE); @@ -1242,7 +1245,7 @@ namespace } }); type("llOld"); - CHECK(e.completionOpen()); + REQUIRE(e.completionOpen()); CHECK(e.completions()[0].deprecated); key(KEY_RETURN); UNSCOPED_INFO("with its brackets and its parameter"); @@ -1271,6 +1274,7 @@ namespace const S32 y = text.mTop - e.layout().rowHeight() / 2; e.handleHover(x, y, MASK_NONE); CHECK(e.handleToolTip(x, y, MASK_NONE)); + REQUIRE(e.cardShown()); const ALTextView& card = *e.card(); UNSCOPED_INFO("the problem, a blank, then the word"); CHECK(card.text() == std::string("Too few arguments\n\ninteger llSay(integer channel, string msg)\nSays something.")); @@ -2188,6 +2192,7 @@ namespace CHECK(told.back().first != was); } e.noteFixes(was, { "stale", "stale" }); + REQUIRE(e.fixes().size() == 2); UNSCOPED_INFO("an old making's dropped"); CHECK(e.fixes()[0].note.empty()); e.noteFixes(told.back().first, { "one" }); @@ -2600,7 +2605,9 @@ namespace type("\n"); { INFO("each slid"); + REQUIRE_FALSE(e.highlights(H::References).empty()); CHECK(e.highlights(H::References)[0].begin == ALTextPos(1, 8)); + REQUIRE_FALSE(e.highlights(H::Block).empty()); CHECK(e.highlights(H::Block)[0].begin == ALTextPos(2, 8)); } e.clearHighlights(); @@ -2612,6 +2619,7 @@ namespace later.range = ALTextRange(ALTextPos(1, 0), ALTextPos(1, 13)); later.message = "later"; e.setDecorations({ worst, later }); + REQUIRE(e.decorations().size() == 2); UNSCOPED_INFO("as they lie"); CHECK(e.decorations()[0].message == "later"); const std::vector listed = e.decorationsOn(1); @@ -2707,6 +2715,7 @@ namespace type("nt"); UNSCOPED_INFO("asked once for the word, narrowed as it grew"); CHECK(asked == 1); + REQUIRE_FALSE(e.completions().empty()); UNSCOPED_INFO("the word typed out first"); CHECK(e.completions()[0].text == std::string("count")); key(KEY_RETURN); @@ -3176,11 +3185,13 @@ namespace e.setCaret(ALTextPos(0, 2)); e.showSignature(ALTextPos(0, 2), forms); key(KEY_DOWN); + REQUIRE(e.signature() != nullptr); UNSCOPED_INFO("Down: the next form"); CHECK(e.signature()->label == std::string("f(string text)")); UNSCOPED_INFO("the caret where it was"); CHECK(e.caret() == ALTextPos(0, 2)); key(KEY_DOWN); + REQUIRE(e.signature() != nullptr); UNSCOPED_INFO("round to the first"); CHECK(e.signature()->label == sig.label); } @@ -3268,6 +3279,7 @@ namespace p.bracket_color_2 = LLUIColor(LLColor4::green); p.bracket_color_3 = LLUIColor(LLColor4::blue); editor = LLUICtrlFactory::create(p); + made.push_back(editor); editor->setFont(LLFontGL::getFontMonospace()); std::vector lit; for (size_t at = text.find('x'); at != std::string::npos; at = text.find('x', at + 1)) @@ -3299,7 +3311,10 @@ namespace } }; + // A colour for each character: the line is ASCII in the monospace + // face, one glyph to a character. const std::vector all = whole_of(); + REQUIRE(all.size() == text.size()); // What the line is coloured with: a bracket at each depth, the // analyzer's colour in code, and the grammar's in the string. const S32 first_open = static_cast(text.find('(')); @@ -3325,6 +3340,7 @@ namespace editor->setWordWrap(false); const ALTextLayout::Line& one = editor->layout().line(0); + REQUIRE_FALSE(one.rows.empty()); for (F32 from = 0.f; from < one.width; from += 1500.f) { CAPTURE(from); @@ -3337,6 +3353,7 @@ namespace editor->setCaret(ALTextPos(0, 0)); editor->insertText("("); const std::vector deeper = whole_of(); + REQUIRE(deeper.size() == text.size() + 1); UNSCOPED_INFO("the outer bracket one deeper"); CHECK(deeper[static_cast(first_open) + 1] == LLColor4U(LLColor4::green)); editor->setWordWrap(true); @@ -3784,7 +3801,7 @@ namespace type("--!st"); { INFO("open after --!"); - CHECK(e.completionOpen()); + REQUIRE(e.completionOpen()); CHECK(e.completions()[0].text == "strict"); } key(KEY_TAB); @@ -3835,6 +3852,7 @@ namespace INFO("open at the quote, asked of nothing yet"); CHECK(e.completionOpen()); CHECK(e.completions().size() == 3); + REQUIRE_FALSE(asked.empty()); CHECK(asked.back().empty()); } type("li"); @@ -3857,6 +3875,7 @@ namespace CHECK(requested.back() == "lib/"); } type("ut"); + REQUIRE_FALSE(e.completions().empty()); UNSCOPED_INFO("narrowed in the folder"); CHECK(e.completions()[0].text == std::string("util")); key(KEY_TAB); @@ -3880,7 +3899,7 @@ namespace type("--#include \"m"); { INFO("an include's name too"); - CHECK(e.completionOpen()); + REQUIRE(e.completionOpen()); CHECK(e.completions()[0].text == "main"); } key(KEY_ESCAPE); @@ -3904,7 +3923,7 @@ namespace type("require(\"ma"); { INFO("open in a string not closed"); - CHECK(e.completionOpen()); + REQUIRE(e.completionOpen()); CHECK(e.completions()[0].text == "main"); } key(KEY_TAB); @@ -3918,7 +3937,7 @@ namespace type("\"ma"); { INFO("open before what follows"); - CHECK(e.completionOpen()); + REQUIRE(e.completionOpen()); CHECK(e.completions()[0].text == "main"); } key(KEY_TAB); @@ -3982,6 +4001,7 @@ namespace INFO("open at the quote, asked of nothing yet"); CHECK(e.completionOpen()); CHECK(e.completions().size() == 3); + REQUIRE_FALSE(asked.empty()); CHECK(asked.back().empty()); } type("Door "); @@ -5033,18 +5053,23 @@ namespace } } } + // The first line the layout hides for the folds where no folded + // block does, or shows where one does: one check a step, not one + // a line. S32 hidden = 0; + S32 wrong = -1; for (S32 l = 0; l < lines; ++l) { const bool by_folds = e.layout().hiddenBy(l, By::Folds); const bool wanted = folded[static_cast(l)] != 0; hidden += by_folds ? 1 : 0; - if (by_folds != wanted) + if (by_folds != wanted && wrong < 0) { - CAPTURE(l); - REQUIRE(by_folds == wanted); + wrong = l; } } + UNSCOPED_INFO("each line hidden for the folds just where a folded block hides it"); + REQUIRE(wrong == -1); UNSCOPED_INFO("as many hidden for the folds as the layout counts"); REQUIRE(e.layout().hiddenCount(By::Folds) == hidden); folded_steps += hidden > 0 ? 1 : 0; diff --git a/indra/llui/tests/alcompletionmodel_test.cpp b/indra/llui/tests/alcompletionmodel_test.cpp index 43b5a8ce35..2b22144631 100644 --- a/indra/llui/tests/alcompletionmodel_test.cpp +++ b/indra/llui/tests/alcompletionmodel_test.cpp @@ -45,15 +45,6 @@ namespace c.detail = detail; return c; } - static std::vector texts(const std::vector& list) - { - std::vector out; - for (const ALCompletion& c : list) - { - out.push_back(c.text); - } - return out; - } static std::string joined(const std::vector& list) { std::string out; @@ -225,6 +216,7 @@ namespace const ALTextDocument text("pr\n"); const ALTextPos start(0, 0); model.narrow(start, ALTextPos(0, 2), "pr", std::string(), { word("print"), word("pairs") }, text); + REQUIRE_FALSE(model.list().empty()); UNSCOPED_INFO("guessed until told"); CHECK(model.list()[0].brackets == ALCompletion::Brackets::Guess); ALCompletion told = word("print"); @@ -234,6 +226,7 @@ namespace model.narrow(start, ALTextPos(0, 2), "pr", std::string(), {}, text); { INFO("told it fits, and where its brackets go"); + REQUIRE_FALSE(model.list().empty()); CHECK(model.list()[0].text == "print"); CHECK(model.list()[0].fits); CHECK(model.list()[0].brackets == ALCompletion::Brackets::None); diff --git a/indra/llui/tests/alfixlistmodel_test.cpp b/indra/llui/tests/alfixlistmodel_test.cpp index 0d92b5b1bd..8accd108e1 100644 --- a/indra/llui/tests/alfixlistmodel_test.cpp +++ b/indra/llui/tests/alfixlistmodel_test.cpp @@ -79,6 +79,7 @@ namespace { INFO("this showing's"); CHECK(model.note(second, { "-12 bytes", "" })); + REQUIRE(model.fixes().size() == 2); CHECK(model.fixes()[0].note == "-12 bytes"); } model.close(); @@ -115,6 +116,7 @@ namespace model.close(); model.ask(at, caret); joined = model.join(at, caret, { fix("Extract") }, false, -1); + REQUIRE(joined.has_value()); UNSCOPED_INFO("alone, the list having closed"); CHECK(titles(joined->fixes) == std::string("Extract")); model.ask(at, caret); diff --git a/indra/llui/tests/alsnippetsession_test.cpp b/indra/llui/tests/alsnippetsession_test.cpp index 45ec8c850a..d2f1909b2b 100644 --- a/indra/llui/tests/alsnippetsession_test.cpp +++ b/indra/llui/tests/alsnippetsession_test.cpp @@ -85,6 +85,7 @@ namespace session.slide(doc.replace(range(0, 2, 3), "alpha")); { INFO("typed over"); + REQUIRE(session.stops().size() == 2); CHECK(doc.text(session.stops()[0]) == "alpha"); CHECK(doc.text(session.stops()[1]) == "b"); } @@ -93,6 +94,7 @@ namespace session.slide(doc.replace(range(0, 0, 0), "x")); { INFO("moved with the text"); + REQUIRE(session.stops().size() == 2); CHECK(doc.text(session.stops()[0]) == "alpha"); CHECK(doc.text(session.stops()[1]) == "b"); } @@ -131,6 +133,7 @@ namespace session.setSyncing(false); { INFO("each brought up whole"); + REQUIRE(session.mirrors().size() == 2); CHECK(doc.text(session.mirrors()[0].range) == "count"); CHECK(doc.text(session.mirrors()[1].range) == "count"); } @@ -202,6 +205,7 @@ namespace CHECK(whole.stops[1] == whole.mirrors[0].range); } session.start(whole.stops, whole.landing.begin, whole.mirrors); + REQUIRE(session.stops().size() == 2); session.slide(doc.replace(session.stops()[0], "json")); sync(doc, 0); CHECK(doc.text() == std::string("local json = require(\"json\")")); @@ -215,6 +219,7 @@ namespace const ALSnippetSession::Expansion around = ALSnippetSession::expand("${1:x} ${2:the $1 thing}", ALTextPos(0, 0), ""); ALTextDocument text(around.text); + REQUIRE(around.stops.size() == 2); session.start(around.stops, around.landing.begin, around.mirrors); session.slide(text.replace(session.stops()[0], "abc")); sync(text, 0); diff --git a/indra/llui/tests/alsyntaxgrammars_test.cpp b/indra/llui/tests/alsyntaxgrammars_test.cpp index 5d51d961bb..58b2de4eac 100644 --- a/indra/llui/tests/alsyntaxgrammars_test.cpp +++ b/indra/llui/tests/alsyntaxgrammars_test.cpp @@ -46,8 +46,9 @@ namespace for (const char* name : { "lsl", "slua", "xml", "json", "text", "config" }) { std::string error; - UNSCOPED_INFO(std::string(name) + " loads: " + error); - REQUIRE(library.loadFile(std::string(LLUI_TEST_APP_DIR) + "/app_settings/syntax/" + name + ".xml", error)); + const bool loaded = library.loadFile(std::string(LLUI_TEST_APP_DIR) + "/app_settings/syntax/" + name + ".xml", error); + INFO(name << " loads: " << error); + REQUIRE(loaded); } } @@ -85,10 +86,17 @@ namespace TEST_CASE_METHOD(alsyntaxgrammars_data, "alsyntaxgrammars: the six grammars load and answer to their extensions", "[alsyntaxgrammars]") { REQUIRE(library.names().size() == size_t(6)); - CHECK(library.forExtension(".lsl")->name() == std::string("lsl")); - CHECK(library.forExtension("luau")->name() == std::string("slua")); - CHECK(library.forExtension("xui")->name() == std::string("xml")); - CHECK(library.forExtension("ini")->name() == std::string("config")); + const auto named = [this](std::string_view extension) + { + std::shared_ptr grammar = library.forExtension(extension); + CAPTURE(extension); + REQUIRE(grammar); + return grammar->name(); + }; + CHECK(named(".lsl") == std::string("lsl")); + CHECK(named("luau") == std::string("slua")); + CHECK(named("xui") == std::string("xml")); + CHECK(named("ini") == std::string("config")); REQUIRE_FALSE(library.forExtension("exe")); } @@ -258,8 +266,9 @@ namespace description["states"] = states; ALSyntaxGrammar grammar; std::string error; - UNSCOPED_INFO("loads: " + error); - REQUIRE(grammar.load(description, error)); + const bool loaded = grammar.load(description, error); + UNSCOPED_INFO("loads: " << error); + REQUIRE(loaded); ALSyntaxWords words; // Nested repeats over a long line, which Boost.Regex gave up on: @@ -307,8 +316,9 @@ namespace regex_description["name"] = "tested"; regex_description["states"] = regex_states; ALSyntaxGrammar regex_grammar; - UNSCOPED_INFO("loads: " + error); - REQUIRE(regex_grammar.load(regex_description, error)); + const bool regex_loaded = regex_grammar.load(regex_description, error); + UNSCOPED_INFO("loads: " << error); + REQUIRE(regex_loaded); state = regex_grammar.initialState(); regex_grammar.lexLine("<(); std::string error; - UNSCOPED_INFO("grammar loads: " + error); - REQUIRE(grammar->load(description, error)); + const bool loaded = grammar->load(description, error); + INFO("grammar loads: " << error); + REQUIRE(loaded); return grammar; } @@ -150,6 +151,7 @@ namespace UNSCOPED_INFO("the longer operator first"); CHECK(lexed(*grammar, "a==b", state, words) == std::string("text:a|operator:==|text:b")); REQUIRE(grammar->wordTables().size() == size_t(2)); + REQUIRE_FALSE(grammar->extensions().empty()); CHECK(grammar->extensions().front() == std::string("mini")); } @@ -189,6 +191,7 @@ namespace UNSCOPED_INFO("level two ends at level two"); CHECK(lexed(*grammar, "[==[ a ]=] b ]==] c", state, words) == std::string("string:[==[ a ]=] b ]==]|text: c")); CHECK(lexed(*grammar, "[=[ x", state, words) == std::string("string:[=[ x")); + REQUIRE_FALSE(state.frames.empty()); UNSCOPED_INFO("with the level in the state"); CHECK(state.frames.back().payload == std::string("=")); UNSCOPED_INFO("and closed on the next"); @@ -243,8 +246,9 @@ namespace LLSD good = mini(); good["states"]["main"] = rules({ LLSD().with("regex", "(x)y").with("consume", 1).with("not_after_chars", "a-z").with("kind", "keyword") }); + const bool both = grammar.load(good, error); UNSCOPED_INFO("both on a regex: " << error); - CHECK(grammar.load(good, error)); + CHECK(both); } TEST_CASE_METHOD(alsyntaxhighlighter_data, "alsyntaxhighlighter: the highlighter follows a document and re-lexes only what an edit reaches", "[alsyntaxhighlighter]") @@ -484,10 +488,12 @@ namespace LLSD good = mini(); good["states"]["main"] = rules({ LLSD().with("span", "\"").with("end", "\"").with("escape", "\\").with("multiline", false).with("unless_after", "\\"), LLSD().with("eol", true).with("unless_after", "\\").with("pop", true) }); + const bool backslashed = grammar.load(good, error); UNSCOPED_INFO("a span its line ends, but for a backslash, and an eol that says the same: " << error); - CHECK(grammar.load(good, error)); + CHECK(backslashed); + const bool little = grammar.load(mini(), error); UNSCOPED_INFO("and the little language: " << error); - CHECK(grammar.load(mini(), error)); + CHECK(little); } TEST_CASE_METHOD(alsyntaxhighlighter_data, "alsyntaxhighlighter: a line's revision moves on only when its tokens may have changed", "[alsyntaxhighlighter]") diff --git a/indra/llui/tests/alvimkeymap_test.cpp b/indra/llui/tests/alvimkeymap_test.cpp index 5be45a964c..21771ae3ee 100644 --- a/indra/llui/tests/alvimkeymap_test.cpp +++ b/indra/llui/tests/alvimkeymap_test.cpp @@ -571,14 +571,15 @@ namespace TEST_CASE_METHOD(alvimkeymap_data, "alvimkeymap: macros: q records keys as text; @ and @@ play them; qA adds", "[alvimkeymap]") { ALCodeEditor& e = make("one\ntwo\nthree\nfour\n"); - keys("qaA;jq"); + keys("qaA;j"); + UNSCOPED_INFO("recording says so while it runs"); + CHECK(vim->recording()); + keys("q"); CHECK_FALSE(vim->recording()); UNSCOPED_INFO("the register holds the keys as text"); CHECK(vim->registerText('a') == std::string("A;j")); UNSCOPED_INFO("the recording ran as it was typed"); CHECK(flat(e.text()) == std::string("one;|two|three|four|")); - UNSCOPED_INFO("recording says so while it runs"); - CHECK(true); keys("@a"); UNSCOPED_INFO("played once"); CHECK(flat(e.text()) == std::string("one;|two;|three|four|")); @@ -2003,6 +2004,7 @@ namespace keys(":di a"); { INFO("only those named"); + REQUIRE(listed.size() == size_t(2)); CHECK(listed[1].find("\"0") == std::string::npos); CHECK(listed[1].find("\"a") != std::string::npos); } @@ -2012,9 +2014,11 @@ namespace listed.pop_back(); keys("mb:marks"); + REQUIRE(listed.size() == size_t(3)); CHECK(listed[2] == std::string("mark line col file/text\n b 2 0 two three")); keys("G"); keys(":marks"); + REQUIRE(listed.size() == size_t(4)); UNSCOPED_INFO("a jump's"); CHECK(listed[3].find("\n ' 2 0 two three") != std::string::npos); @@ -3011,11 +3015,16 @@ namespace keys("q:"); UNSCOPED_INFO("the window holds the pick"); REQUIRE(static_cast(later)); + // The : history outlives the vim it was typed into: a line run + // would be the last on it. + const std::shared_ptr vim_shared = vim->sharedState(); e.setModalKeymap(nullptr); vim = nullptr; later("set nonumber", true); - UNSCOPED_INFO("vim gone: nothing"); + UNSCOPED_INFO("vim gone: nothing put back"); CHECK_FALSE(e.modalKeymap()); + UNSCOPED_INFO("and nothing run"); + CHECK(vim_shared->command == std::vector{ "set number" }); auto keymap = std::make_unique(); vim = keymap.get(); @@ -3025,12 +3034,13 @@ namespace keys(":set numberq:"); UNSCOPED_INFO("held again"); REQUIRE(static_cast(later)); + const std::shared_ptr tab_shared = vim->sharedState(); editor->die(); editor = nullptr; LLMortician::updateClass(); later("set nonumber", true); - UNSCOPED_INFO("the tab gone: nothing"); - CHECK(true); + UNSCOPED_INFO("the tab gone: nothing run"); + CHECK(tab_shared->command == std::vector{ "set number" }); } TEST_CASE_METHOD(alvimkeymap_data, "alvimkeymap: vim's Control motions ^H ^J ^N ^P ^M and ^L; Tab forward in the jumps", "[alvimkeymap]") @@ -6630,62 +6640,66 @@ namespace TEST_CASE_METHOD(alvimkeymap_data, "alvimkeymap: the : commands are taken by any abbreviation vim takes", "[alvimkeymap]") { + // Each run, and none of them unknown. + const auto run = [this](const char* line) { + ex(line); + INFO(":" << line << " said: " << vim->message()); + CHECK_FALSE(vim->messageIsError()); + }; ALCodeEditor& e = make("a b\nc\n"); - ex("joi"); + run("joi"); UNSCOPED_INFO(":joi joins"); CHECK(flat(e.text()) == std::string("a b c|")); make("a\nb\nc\n"); - ex("dele"); + run("dele"); UNSCOPED_INFO(":dele deletes"); CHECK(flat(editor->text()) == std::string("b|c|")); make("abc\n"); - ex("norma x"); + run("norma x"); UNSCOPED_INFO(":norma plays the keys"); CHECK(flat(editor->text()) == std::string("bc|")); make(" a\n"); - ex("lef"); + run("lef"); UNSCOPED_INFO(":lef to the left"); CHECK(flat(editor->text()) == std::string("a|")); make("a\n"); - ex("rig 5"); + run("rig 5"); UNSCOPED_INFO(":rig to the right"); CHECK(flat(editor->text()) == std::string(" a|")); make("a\nb\nc\n"); - ex("3mo 0"); + run("3mo 0"); UNSCOPED_INFO(":mo moves"); CHECK(flat(editor->text()) == std::string("c|a|b|")); make("a\nb\nc\n"); - ex("3cop 0"); + run("3cop 0"); UNSCOPED_INFO(":cop copies"); CHECK(flat(editor->text()) == std::string("c|a|b|c|")); make("a\nxb\nc\n"); - ex("gl/x/d"); + run("gl/x/d"); CHECK(flat(editor->text()) == std::string("a|c|")); make("a\nxb\nc\n"); - ex("vg/x/d"); + run("vg/x/d"); CHECK(flat(editor->text()) == std::string("xb|")); make("aa\n"); - ex("su/a/b/"); + run("su/a/b/"); CHECK(flat(editor->text()) == std::string("ba|")); make("a\nb\n"); - ex("ya"); - ex("pu"); + run("ya"); + run("pu"); UNSCOPED_INFO(":ya then :pu"); CHECK(flat(editor->text()) == std::string("a|a|b|")); make("a\n"); keys("x"); - ex("und"); + run("und"); UNSCOPED_INFO(":und undoes"); CHECK(flat(editor->text()) == std::string("a|")); - UNSCOPED_INFO("none of them unknown: " << vim->message()); - CHECK_FALSE(vim->messageIsError()); make("a\nb\n"); keys("gg"); - ex("2mar a"); + run("2mar a"); keys("'a"); UNSCOPED_INFO(":mar sets a mark"); CHECK(caretText() == std::string("1:0")); - ex("setlo ic"); + run("setlo ic"); UNSCOPED_INFO(":setlo sets"); CHECK(vim->shared().ignoreCase); } diff --git a/indra/llui/tests/alvimmappings_test.cpp b/indra/llui/tests/alvimmappings_test.cpp index f83c28843f..f1e0759805 100644 --- a/indra/llui/tests/alvimmappings_test.cpp +++ b/indra/llui/tests/alvimmappings_test.cpp @@ -103,9 +103,13 @@ namespace } UNSCOPED_INFO("shown back as :map lists them"); CHECK(spelt("a") == std::string("a")); - CHECK(maps.keysOf("").front().isChar); - CHECK(maps.keysOf("").front().ch == 'A'); - CHECK(maps.keysOf("").front().key == KEY_ESCAPE); + const std::vector shifted = maps.keysOf(""); + REQUIRE(shifted.size() == size_t(1)); + CHECK(shifted[0].isChar); + CHECK(shifted[0].ch == 'A'); + const std::vector escape = maps.keysOf(""); + REQUIRE(escape.size() == size_t(1)); + CHECK(escape[0].key == KEY_ESCAPE); } TEST_CASE_METHOD(alvimmappings_data, "alvimmappings: is the leader :let set when the mapping was made", "[alvimmappings]") @@ -162,8 +166,10 @@ namespace } UNSCOPED_INFO("Q in operator-pending mode too"); CHECK(maps.match(ALVimMappings::OPERATOR, chars("Q"), true).full != nullptr); + const std::vector control_l = maps.keysOf(""); + REQUIRE(control_l.size() == size_t(1)); UNSCOPED_INFO("the : line has "); - CHECK(maps.starts(ALVimMappings::COMMAND_LINE, maps.keysOf("").front())); + CHECK(maps.starts(ALVimMappings::COMMAND_LINE, control_l[0])); CHECK(run("map", "")); CHECK(listing.find("Q") != std::string::npos); CHECK(listing.find("gj") != std::string::npos); @@ -249,7 +255,9 @@ namespace { INFO(" kept"); CHECK(run("nnoremap", " g G")); - CHECK(maps.match(ALVimMappings::NORMAL, chars("g"), true).full->nowait); + const ALVimMappings::Match nowait = maps.match(ALVimMappings::NORMAL, chars("g"), true); + REQUIRE(nowait.full); + CHECK(nowait.full->nowait); } { INFO(" refuses one there"); @@ -264,6 +272,7 @@ namespace UNSCOPED_INFO("a bar ends it"); CHECK(run("nmap", "zb a\\|b|c")); const ALVimMappings::Match bar = maps.match(ALVimMappings::NORMAL, chars("zb"), true); + REQUIRE(bar.full); UNSCOPED_INFO("\\| is one"); CHECK(ALVimMappings::shown(bar.full->to) == std::string("ab")); } From 4b2b6787e063ab95c358cc9d6425634a80ccf8fa Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 13 Oct 2026 23:21:55 -0400 Subject: [PATCH 3/8] llui's comparison tests check what they say they check Old test bugs TM23's idiom pass found (b9f739bcda), fixed. - aldiffview_gl: - "range bands draw in the same number of batches" passed only after another case of its file had drawn. Its first frames rebuild text: the first puts every label's glyphs together, and a frame that adds glyphs to the font's cache rebuilds the labels drawn before them on the next. It counts the first frame that rebuilds none, as llfonttextcache's tests settle, and now passes alone. It also checks that the bands were drawn, which "as many for four as for two" never did; - "a folded run's gap row is written over" checked only that something was drawn there; it draws a run a line longer and checks the row changed with its count. - aldiffview: - the bar's buttons were read through as() unchecked, in press() and eight other places; require_button() requires each; - a menu's items are required before they are read, and a dead if (menu) after its REQUIRE is gone; - two reads of a line's gap rows indexed a vector unchecked; they ask the layout, which answers 0 past the end. - aldiffmodel: changes, folds, ranges, words and pairs are required before they are read. - altextdiff: - minimal was compared with patience twice where histogram was meant; - a walk's runs, each line's results, a list of hunks and a right side are required before they are read. - aldifflexer: the LSL grammar is required before it is used. Each new or corrected check fails under a small change to the test's own input or expectation, at the check and not by crashing. Co-Authored-By: Claude Opus 5.5 --- indra/llui/tests/aldifflexer_test.cpp | 2 + indra/llui/tests/aldiffmodel_test.cpp | 10 ++ indra/llui/tests/aldiffview_gl_test.cpp | 127 ++++++++++++++++++++---- indra/llui/tests/aldiffview_test.cpp | 67 +++++++------ indra/llui/tests/altextdiff_test.cpp | 38 +++++-- 5 files changed, 188 insertions(+), 56 deletions(-) diff --git a/indra/llui/tests/aldifflexer_test.cpp b/indra/llui/tests/aldifflexer_test.cpp index 485d2a3f05..fbef66fa13 100644 --- a/indra/llui/tests/aldifflexer_test.cpp +++ b/indra/llui/tests/aldifflexer_test.cpp @@ -87,6 +87,8 @@ namespace TEST_CASE_METHOD(aldifflexer_data, "aldifflexer: an edit is read again only until a line starts as it did", "[aldifflexer]") { + UNSCOPED_INFO("the LSL grammar"); + REQUIRE(lsl != nullptr); std::vector text; for (S32 n = 0; n < 200; ++n) { diff --git a/indra/llui/tests/aldiffmodel_test.cpp b/indra/llui/tests/aldiffmodel_test.cpp index d7799a146f..847cd4583d 100644 --- a/indra/llui/tests/aldiffmodel_test.cpp +++ b/indra/llui/tests/aldiffmodel_test.cpp @@ -334,6 +334,7 @@ namespace CHECK(m.changeStep(Column::Left, 1, true) == -1); CHECK(m.changeStep(Column::Left, 1, false) == -1); } + REQUIRE(m.changeCount() == 1); UNSCOPED_INFO("the caret for its row: under its gap"); CHECK(m.caretLineOfRow(Column::Left, m.changeFirst(Layout::Sides, 0)) == 1); } @@ -489,6 +490,7 @@ namespace UNSCOPED_INFO("the gap now the right's"); CHECK(paddingOf(Column::Right) == std::vector{ 0, 0, 0, 1 }); CHECK(static_cast(m.rightColumn()) == static_cast(Column::Left)); + REQUIRE(m.changeCount() == 2); const ALDiffModel::ChangeLines& last = m.changeLines(1); { INFO("the line put in still the right's"); @@ -509,6 +511,7 @@ namespace m.setSwapped(false); m.setTexts(lines(30).c_str(), lines(30, { { 2, "line two" }, { 27, "line 27 changed" } }).c_str()); + REQUIRE(m.foldCount() == 1); m.setFoldOpen(0, true); m.setSwapped(true); { @@ -573,6 +576,7 @@ namespace { const std::string right = lines(30, { { 2, "line two" }, { 27, "line 27 changed" } }); m.setTexts(lines(30).c_str(), right.c_str()); + REQUIRE(m.foldCount() == 1); m.setFoldOpen(0, true); const ALDiffModel::LineMap map = m.setRightText("inserted\n" + right); { @@ -695,6 +699,7 @@ namespace CHECK(beside(5, 5)); UNSCOPED_INFO("two rows of nothing over it on the left, beside the if's own"); CHECK(m.line(Column::Left, 5).padding == 2); + REQUIRE(m.ranges().size() == 5); // The state's first line beside nothing, the handler having the // SLua's first; the call in the if on the if's own line, which is // beside the if's head: neither lined up, so the if is bracketed, @@ -728,6 +733,7 @@ namespace m.setRightText(std::string("-- a line more\n") + slua); { INFO("carried with the right's lines"); + REQUIRE(m.ranges().size() == 5); CHECK(m.ranges()[2].rightFirst == 3); CHECK(m.ranges()[2].rightLast == 5); CHECK(beside(4, 3)); @@ -1136,6 +1142,7 @@ namespace // SLua's then and end, with the blanks beside them. { INFO("the if: the LSL's brackets"); + REQUIRE_FALSE(m.line(Column::Left, 1).words.empty()); CHECK(marked(Column::Left, 1) <= 3); CHECK(m.line(Column::Left, 1).words.front() == std::make_pair(3, 4)); } @@ -1840,6 +1847,7 @@ namespace // Lines put in above on the right, then on the left: carried. m.setRightText("-- a note\n" + right); + REQUIRE(m.pairs().size() == 2); UNSCOPED_INFO("the right's lines moved along"); CHECK(m.pairs()[0].rightFirst == 6); { @@ -1848,6 +1856,7 @@ namespace CHECK(beside(4, 9)); } m.setLeftText("-- a note\n" + left); + REQUIRE(m.pairs().size() == 2); UNSCOPED_INFO("the left's too"); CHECK(m.pairs()[0].leftFirst == 1); CHECK(beside(1, 6)); @@ -2016,6 +2025,7 @@ namespace m.setAlgorithm(ALTextDiff::Algorithm::Histogram); { INFO("and the other way about"); + REQUIRE(m.foldCount() == 2); CHECK(m.foldOpen(0)); CHECK_FALSE(m.foldOpen(1)); } diff --git a/indra/llui/tests/aldiffview_gl_test.cpp b/indra/llui/tests/aldiffview_gl_test.cpp index 72713d9242..f12f87db47 100644 --- a/indra/llui/tests/aldiffview_gl_test.cpp +++ b/indra/llui/tests/aldiffview_gl_test.cpp @@ -24,6 +24,7 @@ #include "linden_common.h" +#include "llfonttextcache.h" #include "llglheaders.h" #include "aluigl_fixture.h" @@ -39,6 +40,7 @@ #include #include +#include #include namespace @@ -104,27 +106,35 @@ namespace } } - // Forty lines, the third changed and the thirty-first, a line put - // in after the fifth: the run between folded. The sides in colours - // of their own, which the headless colour table has none of. - ALDiffView& make() + // Forty lines, the third changed and the thirty-first -- or the one + // asked for -- and a line put in after the fifth: the run between + // folded. + static std::pair texts(S32 changed = 30) { - // The fonts' glyphs in textures, once there is a context. - if (!ll_test::HeadlessUI::get(/*gl_textures=*/true).ok()) - { - SKIP("no UI: LLUI_TEST_APP_DIR does not point at the source tree"); - } std::string left; std::string right; for (S32 i = 0; i < 40; ++i) { left += "line " + std::to_string(i) + "\n"; - right += (i == 2 ? std::string("two") : i == 30 ? std::string("thirty") : "line " + std::to_string(i)) + "\n"; + right += (i == 2 ? std::string("two") : i == changed ? std::string("thirty") : "line " + std::to_string(i)) + "\n"; if (i == 4) { right += "added\n"; } } + return { left, right }; + } + + // The texts above, the sides in colours of their own, which the + // headless colour table has none of. + ALDiffView& make() + { + // The fonts' glyphs in textures, once there is a context. + if (!ll_test::HeadlessUI::get(/*gl_textures=*/true).ok()) + { + SKIP("no UI: LLUI_TEST_APP_DIR does not point at the source tree"); + } + const auto [left, right] = texts(); ALCodeEditor::Params side(LLUICtrlFactory::getDefaultParams()); const LLColor4 ink(0.85f, 0.85f, 0.85f, 1.f); const LLColor4 ground(0.12f, 0.12f, 0.14f, 1.f); @@ -198,7 +208,7 @@ namespace CHECK(count(plain, b.left + 4, b.right - 4, b.top - 2, b.top + 1, yellow) < (b.right - b.left) / 4); } - TEST_CASE_METHOD(aldiffview_gl_data, "aldiffview_gl: a folded run's gap row is written over and goes once the run opens", "[aldiffview_gl]") + TEST_CASE_METHOD(aldiffview_gl_data, "aldiffview_gl: a folded run's gap row says how many lines it stands for and goes once the run opens", "[aldiffview_gl]") { ALDiffView& d = make(); CHECK(d.foldedCount() == 1); @@ -218,6 +228,43 @@ namespace const std::vector folded = drawn(d); UNSCOPED_INFO("words over it"); CHECK(count(folded, b.left, b.right, b.bottom + 1, b.top - 1, inked) > 20); + + // The run a line longer, its row the gap above the line after it: + // the words over that row say another count, so they differ. + const S32 lower = under + 1; + { + const auto [left, right] = texts(31); + d.setTexts(left, right); + } + CHECK(layout.gapRows(lower) == 1); + Band c = band(d.right(), lower, lower); + c.bottom = c.top; + c.top = c.top + layout.gapHeight(lower); + const std::vector longer = drawn(d); + // Pixel for pixel up the two rows from their bottoms. + S32 differing = 0; + for (S32 up = 1; up < b.top - b.bottom; ++up) + { + const S32 was = b.bottom + up; + const S32 now = c.bottom + up; + if (was < 0 || was >= H || now < 0 || now >= H) + { + continue; + } + for (S32 x = llmax(0, b.left); x < llmin(W, b.right); ++x) + { + const size_t i = (static_cast(was) * W + x) * 4; + const size_t j = (static_cast(now) * W + x) * 4; + differing += folded[i] != longer[j] || folded[i + 1] != longer[j + 1] || folded[i + 2] != longer[j + 2] ? 1 : 0; + } + } + UNSCOPED_INFO("another count over a run a line longer"); + CHECK(differing > 0); + { + const auto [left, right] = texts(); + d.setTexts(left, right); + } + d.right()->goTo(ALTextPos(12, 0)); UNSCOPED_INFO("opened"); CHECK(d.foldedCount() == 0); @@ -313,7 +360,8 @@ namespace TEST_CASE_METHOD(aldiffview_gl_data, "aldiffview_gl: range bands draw in the same number of batches however many there are", "[aldiffview_gl]") { - ALDiffView& d = make(); + ALDiffView& d = make(); + const S32 narrow = d.right()->getRect().mLeft - d.left()->getRect().mRight; // Eight lines the same on each side, ranges of two lines each, level: // a band each, which line up nothing the lines would not, so the // sides are drawn alike whatever bands there are. @@ -322,20 +370,57 @@ namespace { text += (n ? "\n" : "") + ("same " + std::to_string(n)); } - // How many draws the comparison makes, drawn once already: what is - // put in a texture the first time it is drawn, put in. - const auto draws = [&](const ALTextDiff::ranges_t& ranges) { - d.setTexts(text, text, ranges); - drawn(d); + // How many draws a frame of the comparison makes, and what it drew. + std::vector shown; + const auto frame = [&]() { gGL.pushUIMatrix(); const U32 before = LLRender::sUICalls; - drawn(d); - const U32 made = LLRender::sUICalls - before; + shown = drawn(d); + const U32 made = LLRender::sUICalls - before; gGL.popUIMatrix(); return made; }; - const U32 two = draws({ { 0, 1, 0, 1 }, { 4, 5, 4, 5 } }); - const U32 four = draws({ { 0, 1, 0, 1 }, { 2, 3, 2, 3 }, { 4, 5, 4, 5 }, { 6, 7, 6, 7 } }); + // How many a frame makes once no text in it is put together anew: + // drawn until a frame rebuilds none, at most eight times. A frame + // that adds glyphs to the font's cache moves it under every text + // drawn before them, and each of those is rebuilt on the next frame, + // so a frame can make as many draws as the one before and still not + // be the frame that settles. + const auto draws = [&](const ALTextDiff::ranges_t& ranges) { + d.setTexts(text, text, ranges); + for (S32 tries = 0; tries < 8; ++tries) + { + const U64 rebuilt = LLFontTextCache::regenCount(); + const U32 made = frame(); + if (LLFontTextCache::regenCount() == rebuilt) + { + return made; + } + } + FAIL("no frame drew without rebuilding a text"); + return 0U; + }; + // The bands drawn in the frame counted: the gap widened for them, and + // its columns lit beside each band's rows. + const auto lit = [](U8 r, U8 g, U8 b) { return r > 8 || g > 8 || b > 8; }; + const auto banded = [&](const ALTextDiff::ranges_t& ranges) { + UNSCOPED_INFO("the gap widened for the bands"); + CHECK(d.right()->getRect().mLeft - d.left()->getRect().mRight > narrow); + const S32 from = d.left()->getRect().mRight + 1; + const S32 to = d.right()->getRect().mLeft - 1; + for (const ALTextDiff::Range& range : ranges) + { + const Band rows = band(d.right(), range.rightFirst, range.rightLast); + UNSCOPED_INFO("a band beside the right's lines from " << range.rightFirst); + CHECK(count(shown, from, to, rows.bottom + 2, rows.top - 2, lit) > (to - from) * 4); + } + }; + const ALTextDiff::ranges_t two_bands = { { 0, 1, 0, 1 }, { 4, 5, 4, 5 } }; + const ALTextDiff::ranges_t four_bands = { { 0, 1, 0, 1 }, { 2, 3, 2, 3 }, { 4, 5, 4, 5 }, { 6, 7, 6, 7 } }; + const U32 two = draws(two_bands); + banded(two_bands); + const U32 four = draws(four_bands); + banded(four_bands); CHECK(two > 0); UNSCOPED_INFO("as many draws for four bands as for two"); CHECK(four == two); diff --git a/indra/llui/tests/aldiffview_test.cpp b/indra/llui/tests/aldiffview_test.cpp index 762368d587..8dd406a10d 100644 --- a/indra/llui/tests/aldiffview_test.cpp +++ b/indra/llui/tests/aldiffview_test.cpp @@ -86,7 +86,15 @@ namespace return *view; } - static void press(ALDiffView& d, const char* name) { ALViewType::as(d.bar()->getChild(name))->press(); } + // The bar's button of that name. getChild makes a stand-in for a + // name the bar lacks, which is no button. + static ALFlatButton& require_button(ALDiffView& d, const char* name) + { + ALFlatButton* button = ALViewType::as(d.bar()->getChild(name)); + REQUIRE(button != nullptr); + return *button; + } + static void press(ALDiffView& d, const char* name) { require_button(d, name).press(); } static bool enabled(ALDiffView& d, const char* name) { return d.bar()->getChild(name)->getEnabled(); } // The lines "line 0" on, so many, with some of them said otherwise. @@ -540,7 +548,7 @@ namespace S32 said = -1; d.setOnInline([&said](bool inlined) { said = inlined ? 1 : 0; }); - ALFlatButton* inlined = ALViewType::as(d.bar()->getChild("inline")); + ALFlatButton* inlined = &require_button(d, "inline"); UNSCOPED_INFO("side by side: not lit"); CHECK_FALSE(inlined->getToggleState()); inlined->press(); @@ -570,7 +578,7 @@ namespace { INFO("swapped, and lit"); CHECK(d.isSwapped()); - CHECK(ALViewType::as(d.bar()->getChild("swap"))->getToggleState()); + CHECK(require_button(d, "swap").getToggleState()); } UNSCOPED_INFO("the right's text on the left"); CHECK(d.left()->text() == std::string("a\nb = 2\nc\nd")); @@ -852,7 +860,7 @@ namespace } { INFO("the gap"); - CHECK(gapsOf(d.right())[16] == 1); + CHECK(d.right()->layout().gapRows(16) == 1); CHECK(d.right()->layout().lineTop(16) == d.right()->layout().rowHeight()); } @@ -885,7 +893,7 @@ namespace INFO("folded, inline"); CHECK(hidden(d.inlined(), 1)); CHECK_FALSE(hidden(d.inlined(), 16)); - CHECK(gapsOf(d.inlined())[16] == 1); + CHECK(d.inlined()->layout().gapRows(16) == 1); } d.inlined()->goTo(ALTextPos(16, 0)); d.setInline(false); @@ -1145,7 +1153,7 @@ namespace { INFO("on, and the bar's button lit"); CHECK(d.ignores("whitespace")); - CHECK(ALViewType::as(d.bar()->getChild("ignore"))->getToggleState()); + CHECK(require_button(d, "ignore").getToggleState()); } UNSCOPED_INFO("one line changed: the word, not its indent"); CHECK(d.changeCount() == 1); @@ -1291,27 +1299,30 @@ namespace LLContextMenu* menu = holder->findChild("menu_diff_ignore"); UNSCOPED_INFO("shown"); REQUIRE(menu != nullptr); - if (menu) - { - menu->arrangeAndClear(); - UNSCOPED_INFO("comments offered, with LSL's grammar"); - CHECK(menu->findChild("comments")->getVisible()); - UNSCOPED_INFO("case not, for code"); - CHECK_FALSE(menu->findChild("case")->getVisible()); - LLMenuItemGL* trailing = menu->findChild("trailing"); - trailing->onCommit(); - { - INFO("an item turns its own on"); - CHECK(d.ignores("trailing")); - CHECK_FALSE(d.ignores("whitespace")); - } + menu->arrangeAndClear(); + LLMenuItemGL* comments = menu->findChild("comments"); + LLMenuItemGL* letter_case = menu->findChild("case"); + LLMenuItemGL* trailing = menu->findChild("trailing"); + REQUIRE(comments != nullptr); + REQUIRE(letter_case != nullptr); + REQUIRE(trailing != nullptr); + UNSCOPED_INFO("comments offered, with LSL's grammar"); + CHECK(comments->getVisible()); + UNSCOPED_INFO("case not, for code"); + CHECK_FALSE(letter_case->getVisible()); + trailing->onCommit(); + { + INFO("an item turns its own on"); + CHECK(d.ignores("trailing")); + CHECK_FALSE(d.ignores("whitespace")); } d.setIgnore("comments", true); d.setIgnore("blank_lines", true); UNSCOPED_INFO("a comment reworded, a blank line gone and blanks at an end: no change"); CHECK(d.changeCount() == 0); + const ALFlatButton& ignore = require_button(d, "ignore"); UNSCOPED_INFO("lit"); - CHECK(ALViewType::as(d.bar()->getChild("ignore"))->getToggleState()); + CHECK(ignore.getToggleState()); menus.reset(); LLMortician::updateClass(); delete holder; @@ -1750,7 +1761,7 @@ namespace INFO("comments let go of: no change, the bar lit"); CHECK(d.ignores("comments")); CHECK(d.changeCount() == 0); - CHECK(ALViewType::as(d.bar()->getChild("ignore"))->getToggleState()); + CHECK(require_button(d, "ignore").getToggleState()); } d.setGrammar(nullptr); { @@ -1758,7 +1769,7 @@ namespace CHECK_FALSE(d.ignores("comments")); CHECK_FALSE(d.offersIgnore("comments")); CHECK(d.changeCount() == 1); - CHECK_FALSE(ALViewType::as(d.bar()->getChild("ignore"))->getToggleState()); + CHECK_FALSE(require_button(d, "ignore").getToggleState()); } } @@ -2120,7 +2131,7 @@ namespace INFO("opened, and the button so"); CHECK_FALSE(d.foldsSame()); CHECK(d.foldedCount() == 0); - CHECK_FALSE(ALViewType::as(d.bar()->getChild("fold"))->getToggleState()); + CHECK_FALSE(require_button(d, "fold").getToggleState()); } d.right()->handleKey('F', MASK_ALT, false); { @@ -2296,7 +2307,7 @@ namespace d.setMergeBase(base); d.setVersions(3, 1, [](S32) {}); LLView* count = d.bar()->getChild("count"); - const auto glyph = [&d](const char* name) { return ALViewType::as(d.bar()->getChild(name))->glyph(); }; + const ALFlatButton& take_theirs = require_button(d, "take_theirs"); const auto nothing_past_left = [&d]() { for (LLView* child : *d.bar()->getChildList()) { @@ -2311,7 +2322,7 @@ namespace { INFO("wide: all of it"); CHECK(count->getVisible()); - CHECK(glyph("take_theirs") == "Theirs"); + CHECK(take_theirs.glyph() == "Theirs"); CHECK(d.bar()->getChild("versions")->getVisible()); } S32 lost_count = 0; @@ -2324,7 +2335,7 @@ namespace UNSCOPED_INFO("nothing past the left edge"); REQUIRE(nothing_past_left()); lost_count = !count->getVisible() && !lost_count ? width : lost_count; - lost_words = glyph("take_theirs") == "T" && !lost_words ? width : lost_words; + lost_words = take_theirs.glyph() == "T" && !lost_words ? width : lost_words; lost_slider = !d.bar()->getChild("versions")->getVisible() && !lost_slider ? width : lost_slider; } { @@ -2337,7 +2348,7 @@ namespace { INFO("and all back"); CHECK(count->getVisible()); - CHECK(glyph("take_theirs") == "Theirs"); + CHECK(take_theirs.glyph() == "Theirs"); CHECK(d.bar()->getChild("versions")->getVisible()); CHECK(d.bar()->getChild("take_theirs")->getVisible()); CHECK(d.bar()->getChild("done")->getVisible()); diff --git a/indra/llui/tests/altextdiff_test.cpp b/indra/llui/tests/altextdiff_test.cpp index 93a196d63e..4a67438a95 100644 --- a/indra/llui/tests/altextdiff_test.cpp +++ b/indra/llui/tests/altextdiff_test.cpp @@ -72,6 +72,8 @@ namespace REQUIRE(run.right == r); if (run.kind == Kind::Same) { + UNSCOPED_INFO("a Same run inside both texts"); + REQUIRE((l + run.count <= static_cast(left.size()) && r + run.count <= static_cast(right.size()))); for (S32 i = 0; i < run.count; ++i) { UNSCOPED_INFO("a Same run's lines alike"); @@ -739,6 +741,7 @@ namespace { INFO("the operator whole"); REQUIRE_FALSE(left.empty()); + REQUIRE_FALSE(right.empty()); CHECK(left[0] == std::make_pair(6, 8)); CHECK(right[0] == std::make_pair(6, 8)); } @@ -753,6 +756,7 @@ namespace { INFO("without them, by bytes: = and ! alone"); REQUIRE_FALSE(left.empty()); + REQUIRE_FALSE(right.empty()); CHECK(left[0] == std::make_pair(6, 7)); CHECK(right[0] == std::make_pair(6, 7)); } @@ -835,8 +839,9 @@ namespace } return -1; }; - const std::vector patience = ALTextDiff::lines(was, now, by(A::Patience)); - const std::vector minimal = ALTextDiff::lines(was, now, by(A::Minimal)); + const std::vector patience = ALTextDiff::lines(was, now, by(A::Patience)); + const std::vector minimal = ALTextDiff::lines(was, now, by(A::Minimal)); + const std::vector histogram = ALTextDiff::lines(was, now, by(A::Histogram)); require_walks(was, now, patience); require_walks(was, now, minimal); { @@ -850,8 +855,8 @@ namespace CHECK(require_walks(was, now, minimal) <= require_walks(was, now, patience)); } { - INFO("all three alike here: minimal no more"); - CHECK(require_walks(was, now, minimal) <= require_walks(was, now, patience)); + INFO("minimal: fewer or as few as histogram"); + CHECK(require_walks(was, now, minimal) <= require_walks(was, now, histogram)); } // Where they differ. A line moved from the top to the bottom past @@ -1204,6 +1209,14 @@ namespace } return true; }; + // Each line of each text has its marks and says whether it was read + // as tokens: what the checks below index. + const auto require_each_line = [](const Result& r, size_t left, size_t right) { + REQUIRE(r.leftMarks.size() == left); + REQUIRE(r.leftByTokens.size() == left); + REQUIRE(r.rightMarks.size() == right); + REQUIRE(r.rightByTokens.size() == right); + }; const std::vector call = { "default", "{", " llSay(0, \"a\" + b);", "}" }; const std::vector flown = { "default", "{", " llSay(", " 0,", " \"a\" + b", " );", "}" }; const Result reflowed = ALStructuralDiff::compare(call, flown, ALTextDiff::Options()); @@ -1214,6 +1227,7 @@ namespace CHECK(unmarked(reflowed)); CHECK_FALSE(reflowed.tooLarge); } + require_each_line(reflowed, call.size(), flown.size()); { INFO("its lines read as tokens, those the same not"); REQUIRE(reflowed.leftByTokens[2]); @@ -1241,6 +1255,7 @@ namespace const std::vector changed = { "default", "{", " llSay(", " 1,", " \"a\" + b", " );", "}" }; const Result real = ALStructuralDiff::compare(call, changed, ALTextDiff::Options()); require_walks(call, changed, real.runs); + require_each_line(real, call.size(), changed.size()); { INFO("the change inside the reformatting marked, and nothing else"); CHECK(real.leftMarks[2] == ALTextDiff::spans_t{ { 10, 11 } }); @@ -1253,6 +1268,7 @@ namespace const std::vector one = { "f(a)(b)" }; const std::vector two = { "f(a(b))" }; const Result nested = ALStructuralDiff::compare(one, two, ALTextDiff::Options()); + require_each_line(nested, one.size(), two.size()); { INFO("a bracket whose other went elsewhere marked"); REQUIRE_FALSE(nested.leftMarks[0].empty()); @@ -1260,6 +1276,7 @@ namespace } // f's ( kept by the tokens alone, though its ) is not: let go of. const Result closed = ALStructuralDiff::compare({ "f(a); g(b);" }, { "f(a; g(b));" }, ALTextDiff::Options()); + require_each_line(closed, 1, 1); { INFO("an opening bracket whose closing one was not kept, marked"); REQUIRE_FALSE(closed.leftMarks[0].empty()); @@ -1278,6 +1295,7 @@ namespace const std::vector mine = numbered(ALStructuralDiff::MOST_TOKENS / 2 + 1, "mine "); const std::vector theirs = numbered(ALStructuralDiff::MOST_TOKENS / 2 + 1, "theirs "); const Result big = ALStructuralDiff::compare(mine, theirs, ALTextDiff::Options()); + require_each_line(big, mine.size(), theirs.size()); { INFO("too large: by lines, said so"); CHECK(big.tooLarge); @@ -1306,7 +1324,7 @@ namespace kinds.push_back(hunk.kind); } UNSCOPED_INFO("each kind, in order"); - CHECK(kinds == std::vector{ K::Same, K::Ours, K::Same, K::Theirs, K::Same, K::Both, K::Same, K::Conflict }); + REQUIRE(kinds == std::vector{ K::Same, K::Ours, K::Same, K::Theirs, K::Same, K::Both, K::Same, K::Conflict }); { INFO("a hunk's lines in each"); CHECK(hunks[7] == H{ K::Conflict, 7, 1, 7, 1, 7, 1 }); @@ -1361,10 +1379,14 @@ namespace std::vector in_two = base; in_one.insert(in_one.begin() + 4, "mine"); in_two.insert(in_two.begin() + 4, "theirs"); + const ALTextMerge::hunks_t put_in = ALTextMerge::merge(base, in_one, in_two); + REQUIRE(put_in.size() >= 2u); UNSCOPED_INFO("put in at one place"); - CHECK(ALTextMerge::merge(base, in_one, in_two)[1].kind == K::Conflict); + CHECK(put_in[1].kind == K::Conflict); + const ALTextMerge::hunks_t put_in_alike = ALTextMerge::merge(base, in_one, in_one); + REQUIRE(put_in_alike.size() >= 2u); UNSCOPED_INFO("put in alike at one place"); - CHECK(ALTextMerge::merge(base, in_one, in_one)[1].kind == K::Both); + CHECK(put_in_alike[1].kind == K::Both); // A block moved down by ours, a line in it changed by theirs where // it was: a conflict, left so. @@ -1498,6 +1520,8 @@ namespace } if (run.kind == Kind::Same) { + UNSCOPED_INFO("a Same run inside both texts"); + REQUIRE((left + run.count <= static_cast(a.size()) && right + run.count <= static_cast(b.size()))); for (S32 i = 0; i < run.count; ++i) { UNSCOPED_INFO("kept the same"); From 67c8d313220f6ca32f6333d8cb7ef243d22e9067 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 13 Oct 2026 23:21:55 -0400 Subject: [PATCH 4/8] llui's field and property grid tests check what they say they check Old test bugs TM23's idiom pass found (b9f739bcda), fixed. - alpropertygrid: - "rows stack one row apart inside their section" never checked where the first row was; require_stacked requires it at the section's top. Both stacking cases are renamed to say so; - "a whole number steps by at least one" checks only the step it is given, as its comment now says, and is renamed for it; - a second heading is checked too; fields and the back button are required before they are read. - alsettingrow: - a plain row's press was checked through params(), which a press never touches; it checks the press fires neither callback; - value boxes and centres are required before they are read. - alcolortablepanel: "valueText writes four numbers with commas" checked only the commas; it reads the text back as the colour. - alcolorfield: the colour the swatch shows is checked, as its comment said, and the popover is required before it is read. The last case's comment says what it checks. - alcornerfield: a left box beside the picture was checked twice over the top one; the bottom one is checked too. - alcolorsheet, alfontfield, alimagefield: what a declaration shadows, a font, a gallery and a popover are required before they are read. - alfollowscontrol: build() needs the UI and skips without it, as other fixtures' do. Each new or corrected check fails under a small change to the test's own input or expectation, at the check and not by crashing. Co-Authored-By: Claude Opus 5.5 --- indra/llui/tests/alcolorfield_test.cpp | 14 +++++-- indra/llui/tests/alcolorsheet_test.cpp | 1 + indra/llui/tests/alcolortablepanel_test.cpp | 13 ++++-- indra/llui/tests/alcornerfield_test.cpp | 2 +- indra/llui/tests/alfollowscontrol_test.cpp | 6 ++- indra/llui/tests/alfontfield_test.cpp | 9 ++++- indra/llui/tests/alimagefield_test.cpp | 10 ++++- indra/llui/tests/alpropertygrid_test.cpp | 35 +++++++++++----- indra/llui/tests/alsettingrow_test.cpp | 45 ++++++++++++++++----- 9 files changed, 100 insertions(+), 35 deletions(-) diff --git a/indra/llui/tests/alcolorfield_test.cpp b/indra/llui/tests/alcolorfield_test.cpp index c2fde422c1..d4ce2cbbd3 100644 --- a/indra/llui/tests/alcolorfield_test.cpp +++ b/indra/llui/tests/alcolorfield_test.cpp @@ -308,8 +308,11 @@ namespace }); field->setValue("$Accent"); LLColor4 shown; - UNSCOPED_INFO("the field resolves its own name"); - CHECK(field->resolved(shown)); + { + INFO("the field resolves its own name"); + CHECK(field->resolved(shown)); + CHECK(shown == LLColor4(0.f, 0.5f, 1.f, 1.f)); + } field->handleMouseDown(4, 10, MASK_NONE); ALColorPicker* picker = gFloaterView->findChild("picker", true); @@ -323,13 +326,16 @@ namespace // The first swatch is the caller's own name. grid->handleMouseDown(5 + 14, grid->getRect().getHeight() - 5 - 14, MASK_NONE); LLFloater* popover = grid->getParentByType(); + UNSCOPED_INFO("the grid is in the popover"); + REQUIRE(popover != nullptr); UNSCOPED_INFO("the caller's name comes first"); CHECK(popover->getTitle() == std::string("$Accent")); field->die(); } - // A name means what the table says now: the swatch follows a colour - // changed elsewhere without the text having changed. + // A name means what the table says now: what the field resolves it to, + // which is what its swatch is drawn in, follows a colour changed + // elsewhere without the text having changed. TEST_CASE_METHOD(alcolorfield_data, "alcolorfield: a name resolves to its colour as the table has it now", "[alcolorfield]") { if (!ui.ok()) diff --git a/indra/llui/tests/alcolorsheet_test.cpp b/indra/llui/tests/alcolorsheet_test.cpp index 0cee6c5e90..dff236a939 100644 --- a/indra/llui/tests/alcolorsheet_test.cpp +++ b/indra/llui/tests/alcolorsheet_test.cpp @@ -165,6 +165,7 @@ namespace { INFO("which shadows the default's"); REQUIRE(base->shadowed >= 0); + REQUIRE(base->shadowed < static_cast(sheet.declarations().size())); CHECK(sheet.fileOf(sheet.declarations()[base->shadowed]) == "default.xml"); } } diff --git a/indra/llui/tests/alcolortablepanel_test.cpp b/indra/llui/tests/alcolortablepanel_test.cpp index de8a1f66e3..77e7b3ef96 100644 --- a/indra/llui/tests/alcolortablepanel_test.cpp +++ b/indra/llui/tests/alcolortablepanel_test.cpp @@ -26,6 +26,7 @@ #include "alcolortablepanel.h" +#include "alcolorfield.h" #include "alpropertygrid.h" #include "llcallbacklist.h" #include "../llfloater.h" @@ -163,9 +164,15 @@ namespace panel->die(); } - // What a value is written as, and read back from. - TEST_CASE_METHOD(alcolortablepanel_data, "alcolortablepanel: valueText writes four numbers with commas", "[alcolortablepanel]") + // What a value is written as, and read back from: a row's commit reads + // it as a colour field reads its text. + TEST_CASE_METHOD(alcolortablepanel_data, "alcolortablepanel: valueText writes four numbers with commas that read back as the colour", "[alcolortablepanel]") { - CHECK(ALColorTablePanel::valueText(LLColor4(1.f, 0.5f, 0.f, 1.f)) == std::string("1, 0.5, 0, 1")); + const LLColor4 colour(1.f, 0.5f, 0.f, 1.f); + const std::string text = ALColorTablePanel::valueText(colour); + CHECK(text == std::string("1, 0.5, 0, 1")); + LLColor4 back; + CHECK(ALColorField::textToColor(text, back)); + CHECK(back == colour); } } diff --git a/indra/llui/tests/alcornerfield_test.cpp b/indra/llui/tests/alcornerfield_test.cpp index 8b0aee14ab..24efd52051 100644 --- a/indra/llui/tests/alcornerfield_test.cpp +++ b/indra/llui/tests/alcornerfield_test.cpp @@ -147,8 +147,8 @@ namespace } { INFO("beside the picture, which is at the left"); - CHECK(tl.mLeft > 0); CHECK(tl.mLeft >= 72); + CHECK(bl.mLeft >= 72); } UNSCOPED_INFO("the link beside them"); CHECK(f->getChild("link")->getRect().mLeft > tr.mRight); diff --git a/indra/llui/tests/alfollowscontrol_test.cpp b/indra/llui/tests/alfollowscontrol_test.cpp index bbba4fdb22..1766984f8a 100644 --- a/indra/llui/tests/alfollowscontrol_test.cpp +++ b/indra/llui/tests/alfollowscontrol_test.cpp @@ -45,8 +45,12 @@ namespace // Wide enough for the picture and both thumbnails, so the picture is // a square of SIDE at the left of it. - static ALFollowsControl* build(S32 width = SIDE * 3 + 16, S32 height = SIDE) + ALFollowsControl* build(S32 width = SIDE * 3 + 16, S32 height = SIDE) { + if (!ui.ok()) + { + SKIP("no UI: LLUI_TEST_APP_DIR does not point at the source tree"); + } ALFollowsControl::Params p(LLUICtrlFactory::getDefaultParams()); p.name = "follows"; p.rect = LLRect(0, height, width, 0); diff --git a/indra/llui/tests/alfontfield_test.cpp b/indra/llui/tests/alfontfield_test.cpp index b43e403a74..ff74d2b4d9 100644 --- a/indra/llui/tests/alfontfield_test.cpp +++ b/indra/llui/tests/alfontfield_test.cpp @@ -116,9 +116,11 @@ namespace CHECK(field->style() == std::string("BOLD")); UNSCOPED_INFO("a style does not change the value"); CHECK(field->getValue().asString() == std::string("SansSerif")); + const LLFontGL* styled = field->font(); UNSCOPED_INFO("a style is a different face"); - CHECK(field->font() != plain); - CHECK((field->font()->getFontDesc().getStyle() & LLFontGL::BOLD) != 0); + CHECK(styled != plain); + REQUIRE(styled != nullptr); + CHECK((styled->getFontDesc().getStyle() & LLFontGL::BOLD) != 0); field->setSize("Large"); CHECK(field->size() == std::string("Large")); @@ -206,9 +208,11 @@ namespace UNSCOPED_INFO("opened again"); REQUIRE(popover != nullptr); bold = popover->findChild("bold", true); + REQUIRE(bold != nullptr); bold->setValue(true); bold->onCommit(); LLComboBox* sizes = popover->findChild("size", true); + REQUIRE(sizes != nullptr); sizes->setValue("Large"); sizes->onCommit(); CHECK(popover->handleKeyHere(KEY_RETURN, MASK_NONE)); @@ -250,6 +254,7 @@ namespace CHECK(gFocusMgr.getKeyboardFocus() != elsewhere); } LLCheckBoxCtrl* bold = popover->findChild("bold", true); + REQUIRE(bold != nullptr); bold->setValue(true); bold->onCommit(); field->die(); diff --git a/indra/llui/tests/alimagefield_test.cpp b/indra/llui/tests/alimagefield_test.cpp index 833c9e2d7b..5377c3e4c2 100644 --- a/indra/llui/tests/alimagefield_test.cpp +++ b/indra/llui/tests/alimagefield_test.cpp @@ -175,7 +175,9 @@ namespace ALSpecimenList* gallery = open(); REQUIRE(gallery != nullptr); gallery->choose("Icon_Close"); - gallery->getParentByType()->escape(); + ALPopover* popover = gallery->getParentByType(); + REQUIRE(popover != nullptr); + popover->escape(); { INFO("escaped: no change, no commit"); CHECK(commits == 0); @@ -183,8 +185,11 @@ namespace } gallery = open(); + REQUIRE(gallery != nullptr); gallery->choose("Icon_Close"); - gallery->getParentByType()->settle(); + popover = gallery->getParentByType(); + REQUIRE(popover != nullptr); + popover->settle(); { INFO("settled: the one chosen, committed"); CHECK(commits == 1); @@ -193,6 +198,7 @@ namespace elsewhere->setFocus(true); gallery = open(); + REQUIRE(gallery != nullptr); UNSCOPED_INFO("the popover has the keyboard"); CHECK(gFocusMgr.getKeyboardFocus() != elsewhere); gallery->choose("PushButton_Over"); diff --git a/indra/llui/tests/alpropertygrid_test.cpp b/indra/llui/tests/alpropertygrid_test.cpp index 292320efaa..ebc1964d6b 100644 --- a/indra/llui/tests/alpropertygrid_test.cpp +++ b/indra/llui/tests/alpropertygrid_test.cpp @@ -106,7 +106,11 @@ namespace const LLRect& r = labels[i]; CAPTURE(i, r, within); REQUIRE(within.contains(r)); - if (i > 0) + if (i == 0) + { + REQUIRE(r.mTop == within.mTop); + } + else { REQUIRE(labels[i - 1].mTop - r.mTop == ROW); } @@ -114,7 +118,7 @@ namespace } }; - TEST_CASE_METHOD(alpropertygrid_data, "alpropertygrid: freshly filled rows stack one row apart inside their section", "[alpropertygrid]") + TEST_CASE_METHOD(alpropertygrid_data, "alpropertygrid: freshly filled rows stack one row apart from the top of their section", "[alpropertygrid]") { if (!ui.ok()) { @@ -135,7 +139,7 @@ namespace // And they are still stacked from its top after the grid is resized, which // is the whole reason a resize no longer rebuilds them. - TEST_CASE_METHOD(alpropertygrid_data, "alpropertygrid: rows stay stacked inside their section after a resize", "[alpropertygrid]") + TEST_CASE_METHOD(alpropertygrid_data, "alpropertygrid: rows stay stacked from the top of their section after a resize", "[alpropertygrid]") { if (!ui.ok()) { @@ -406,11 +410,14 @@ namespace } // A number is stepped by its arrows, by the wheel over it and by a scrub - // along it, and all three take the same step. A field carrying no - // decimals rounds whatever the step leaves it at, so a step of less than - // one lands back on the number it started from: the arrows write what is - // already there and only the keyboard can change anything. - TEST_CASE_METHOD(alpropertygrid_data, "alpropertygrid: a whole number steps by at least one and a real by less", "[alpropertygrid]") + // along it, and all three take the one step its spinner is given, which + // is what this checks. The gestures are LLSpinCtrl's own, and its arrows + // and wheel read the keyboard's modifiers, which a headless test has no + // keyboard for. A field carrying no decimals rounds whatever the step + // leaves it at, so a step of less than one would land back on the number + // it started from: the arrows would write what is already there and only + // the keyboard could change anything. + TEST_CASE_METHOD(alpropertygrid_data, "alpropertygrid: a whole number is given a step of at least one and a real less", "[alpropertygrid]") { if (!ui.ok()) { @@ -582,6 +589,8 @@ namespace two->setFields(both); UNSCOPED_INFO("a heading over the first"); CHECK(two->getChild("first", true)->getHeaderHeight() > 0); + UNSCOPED_INFO("and one over the second"); + CHECK(two->getChild("second", true)->getHeaderHeight() > 0); one->die(); two->die(); @@ -788,6 +797,7 @@ namespace CHECK(editor->getToolTip() == label->getToolTip()); UNSCOPED_INFO("and it is the same editor, not a new one in its place"); CHECK(row->getChild("width", true) == editor); + REQUIRE(grid->fields().size() == 1u); UNSCOPED_INFO("which the grid remembers"); CHECK(grid->fields().front().authored); @@ -883,10 +893,11 @@ namespace grid->setFields(fields); LLPanel* row = grid->getChild("size_rows", true)->getChild("width_row", true); - LLView* back = row->getChild("width_remove", true); + LLButton* back = row->findChild("width_remove", true); + REQUIRE(back != nullptr); UNSCOPED_INFO("the way back is there for the half that is written"); CHECK(back->getVisible()); - back->as()->onCommit(); + back->onCommit(); UNSCOPED_INFO("and takes out that half alone"); REQUIRE(removed.size() == 1u); UNSCOPED_INFO("which is the second"); @@ -900,7 +911,7 @@ namespace UNSCOPED_INFO("until the first is written"); CHECK(back->getVisible()); removed.clear(); - back->as()->onCommit(); + back->onCommit(); UNSCOPED_INFO("and then it takes out the first"); REQUIRE(removed.size() == 1u); CHECK(removed.front() == std::string("width")); @@ -1044,6 +1055,7 @@ namespace } CHECK(label->getText() == std::string("After")); CHECK(width->getValue().asInteger() == 20); + REQUIRE(grid->fields().size() == 2u); UNSCOPED_INFO("and the grid knows it"); CHECK(grid->fields()[1].value == std::string("20")); @@ -1075,6 +1087,7 @@ namespace fields.back().authored = false; fields.push_back(field("border", 0)); grid->setFields(fields); + REQUIRE(grid->fields().size() == 3u); CHECK(grid->fields()[0].name == std::string("radius")); CHECK(grid->fields()[1].name == std::string("corners")); UNSCOPED_INFO("the written one stays where it was"); diff --git a/indra/llui/tests/alsettingrow_test.cpp b/indra/llui/tests/alsettingrow_test.cpp index 6b4d5bbff6..92942127d6 100644 --- a/indra/llui/tests/alsettingrow_test.cpp +++ b/indra/llui/tests/alsettingrow_test.cpp @@ -465,9 +465,12 @@ namespace ALSettingRow* large = row(std::string(), 0.f, 10.f, 3); LLSliderCtrl* plain_large = slider_like(10.f); + LLLineEditor* large_box = valueBoxOf(large->getSlider()); + LLLineEditor* plain_large_box = valueBoxOf(plain_large); + REQUIRE(large_box); + REQUIRE(plain_large_box); UNSCOPED_INFO("over 1, exactly a slider's"); - CHECK(valueBoxOf(large->getSlider())->getRect().getWidth() == - valueBoxOf(plain_large)->getRect().getWidth()); + CHECK(large_box->getRect().getWidth() == plain_large_box->getRect().getWidth()); small->die(); plain_small->die(); @@ -593,14 +596,20 @@ namespace ALSettingRow* r = LLUICtrlFactory::create(p); gFloaterView->addChild(r); - LLSlider* bar = nullptr; - for (LLView* child : *r->getSlider()->getChildList()) + // The track a row's slider presses on. + const auto barOf = [](ALSettingRow* of) { - if (LLSlider* candidate = ALViewType::as(child)) + LLSlider* bar = nullptr; + for (LLView* child : *of->getSlider()->getChildList()) { - bar = candidate; + if (LLSlider* candidate = ALViewType::as(child)) + { + bar = candidate; + } } - } + return bar; + }; + LLSlider* bar = barOf(r); REQUIRE(bar != nullptr); // On the track and off the thumb, which sits at the middle for 0.5: @@ -615,9 +624,19 @@ namespace CHECK(released == 1); CHECK(pressed == 1); - // A row that names neither is a row like every other. + // A row that names neither is a row like every other: a press and a + // release on its track are heard by neither callback. ALSettingRow* plain = row("SettingRowTestHeld"); - CHECK_FALSE(params("SettingRowTestHeld").mouse_down_callback.isProvided()); + gFloaterView->addChild(plain); + LLSlider* plain_bar = barOf(plain); + REQUIRE(plain_bar != nullptr); + plain_bar->handleMouseDown(x, plain_bar->getRect().getHeight() / 2, MASK_NONE); + plain_bar->handleMouseUp(x, plain_bar->getRect().getHeight() / 2, MASK_NONE); + { + INFO("the plain row's ends are not heard"); + CHECK(pressed == 1); + CHECK(released == 1); + } plain->die(); r->die(); } @@ -717,8 +736,10 @@ namespace p.components = "X Y"; p.letter_width = 18; ALSettingRow* r = LLUICtrlFactory::create(p); + const LLSpinCtrl* x_box = r->findChild("centre.X"); + REQUIRE(x_box != nullptr); const LLTextBox* letter = nullptr; - for (LLView* child : *r->findChild("centre.X")->getChildList()) + for (LLView* child : *x_box->getChildList()) { letter = letter ? letter : ALViewType::as(child); } @@ -746,8 +767,10 @@ namespace r->getResetButton()->onCommit(); UNSCOPED_INFO("the whole setting back"); CHECK(sameParts(control->getValue(), 0.0, 0.0, 0.0)); + const LLSpinCtrl* y_box = r->findChild("centre.Y"); + REQUIRE(y_box != nullptr); UNSCOPED_INFO("the boxes show it"); - CHECK(closeTo(r->findChild("centre.Y")->getValue().asReal(), 0.0)); + CHECK(closeTo(y_box->getValue().asReal(), 0.0)); r->die(); } From f31de36aa92d3b0a5d8006b6d48464cd8f29ac48 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 13 Oct 2026 23:21:56 -0400 Subject: [PATCH 5/8] llui's studio and widget tests check what they say they check Old test bugs TM23's idiom pass found (b9f739bcda), fixed. - alpanelist: - "no fold: left is the list's" could not fail, since with no fold nothing is ever asked; it chooses a row and checks the list takes the key; - the comparison's comment described a sort the default gives too; it says what the case's comparison is. - alpopover: - "the content is in it" checks it is the panel given; - "not leaked" checks the content given with no anchor is gone. - aljumpbar: the first crumb is checked to be a button, not a flyout. - alchoicelist: a message promised two styles a line where the check counts one, since no choice is given an ink. - alemptystate: "centred on its own" checked only that the sentence crossed the middle; it checks the room above it and below it differ by at most a pixel. - alscopebar: after a reshape each choice keeps its width, not only some width. - alquickopen: "in the order given" checks the whole order, and the second query its comment names is asked too. - alspecimenlist: "carries nothing" checks a hover with no starter throws nothing. - aldockpanel: build() skips without a UI, and the fixture's teardown no longer reads gFloaterView where there is none. - alstudiofloater: a popover left up by a failed REQUIRE is escaped while the counters its callbacks write to still live. - alhistorylist: the list was found with getChild(), which makes a stand-in, so the REQUIRE after it could never fail; it is found with findChild(). - Lookups, popovers, windows, lists and their items, menus and undo steps are required before they are read in alchoicelist, aldockpanel, alhistorylist, aljumpbar, alkeycapture, almenuslot, aloutputview, alpanefolds, alpanelist, alpopover, alquickopen, alspecimenlist, alstudiofloater, altabstrip and alundostack. - alviewtype: two stale comments say what their checks do, and a case that needs the UI skips without it. Each new or corrected check fails under a small change to the test's own input or expectation, at the check and not by crashing. Co-Authored-By: Claude Opus 5.5 --- indra/llui/tests/alchoicelist_test.cpp | 3 +- indra/llui/tests/aldockpanel_test.cpp | 34 +++++++++++--- indra/llui/tests/alemptystate_test.cpp | 13 +++--- indra/llui/tests/alhistorylist_test.cpp | 54 ++++++++++++++--------- indra/llui/tests/aljumpbar_test.cpp | 24 +++++++--- indra/llui/tests/alkeycapture_test.cpp | 2 + indra/llui/tests/almenuslot_test.cpp | 2 + indra/llui/tests/aloutputview_test.cpp | 7 ++- indra/llui/tests/alpanefolds_test.cpp | 1 + indra/llui/tests/alpanelist_test.cpp | 38 +++++++++++----- indra/llui/tests/alpopover_test.cpp | 21 +++++++-- indra/llui/tests/alquickopen_test.cpp | 28 ++++++++---- indra/llui/tests/alscopebar_test.cpp | 13 ++++-- indra/llui/tests/alspecimenlist_test.cpp | 49 +++++++++++++++----- indra/llui/tests/alstudiofloater_test.cpp | 31 +++++++++++-- indra/llui/tests/altabstrip_test.cpp | 8 +++- indra/llui/tests/alundostack_test.cpp | 28 +++++++----- indra/llui/tests/alviewtype_test.cpp | 8 +++- 18 files changed, 266 insertions(+), 98 deletions(-) diff --git a/indra/llui/tests/alchoicelist_test.cpp b/indra/llui/tests/alchoicelist_test.cpp index 8ce683ebb7..6db013df5c 100644 --- a/indra/llui/tests/alchoicelist_test.cpp +++ b/indra/llui/tests/alchoicelist_test.cpp @@ -101,7 +101,7 @@ namespace CHECK(std::abs(note0 - note2) < 1.f); UNSCOPED_INFO("past the widest text"); CHECK(note2 > v.layout().xOf(2, 10)); - UNSCOPED_INFO("two styles a line: the note's face on every line, and the ink where one is given"); + UNSCOPED_INFO("a style a line, the note's face: no choice here has an ink of its own"); CHECK(v.styles().size() == size_t(3)); v.moveChoice(1, true); UNSCOPED_INFO("down one"); @@ -206,6 +206,7 @@ namespace CHECK(v.document().line(1) == std::string("odd one\t(string)")); UNSCOPED_INFO("the choice below is on its own line"); CHECK(v.document().line(2) == std::string("last\t ")); + REQUIRE(v.choices().size() == size_t(3)); UNSCOPED_INFO("the words handed back as given"); CHECK(v.choices()[0].note == std::string("for each\nitem in a list")); S32 x, y; diff --git a/indra/llui/tests/aldockpanel_test.cpp b/indra/llui/tests/aldockpanel_test.cpp index b714e152cf..5fda7c8e6a 100644 --- a/indra/llui/tests/aldockpanel_test.cpp +++ b/indra/llui/tests/aldockpanel_test.cpp @@ -59,6 +59,10 @@ namespace // there. ~aldockpanel_data() { + if (!ui.ok()) + { + return; + } std::vector> out; for (LLView* child : *gFloaterView->getChildList()) { @@ -77,9 +81,6 @@ namespace LLMortician::updateClass(); } - // A window with a stack of two panes across it, the way a tool is - // laid out: a side pane of a fixed width and a middle that takes - // whatever is left. // A window that says which keys it was asked about, and takes only // Control-F. struct Home : public LLFloater @@ -102,8 +103,15 @@ namespace LLButton* inside = nullptr; // something to look for by name }; - static Window build(S32 width = 600, S32 height = 400, S32 side_width = 200) + // A window with a stack of two panes across it, the way a tool is + // laid out: a side pane of a fixed width and a middle that takes + // whatever is left. + Window build(S32 width = 600, S32 height = 400, S32 side_width = 200) { + if (!ui.ok()) + { + SKIP("no UI: LLUI_TEST_APP_DIR does not point at the source tree"); + } Window w; LLFloater::Params fp(LLFloater::getDefaultParams()); fp.name = "tool"; @@ -167,6 +175,7 @@ namespace { Window w = build(); ALDockPanel* pane = ALDockPanel::wrap(w.side, "Side"); + REQUIRE(pane != nullptr); const S32 was = w.middle->getRect().getWidth(); pane->popOut(); @@ -191,6 +200,7 @@ namespace { Window w = build(); ALDockPanel* pane = ALDockPanel::wrap(w.side, "Side"); + REQUIRE(pane != nullptr); const S32 dim = w.side->getTargetDim(); pane->popOut(); @@ -220,6 +230,7 @@ namespace { Window w = build(); ALDockPanel* pane = ALDockPanel::wrap(w.side, "Side"); + REQUIRE(pane != nullptr); pane->popOut(); ALPanelFloater* window = pane->getParentByType(); @@ -239,6 +250,7 @@ namespace { Window w = build(); ALDockPanel* pane = ALDockPanel::wrap(w.side, "Side"); + REQUIRE(pane != nullptr); pane->dock(); UNSCOPED_INFO("still where it was declared"); @@ -259,6 +271,7 @@ namespace { Window w = build(); ALDockPanel* pane = ALDockPanel::wrap(w.side, "Side"); + REQUIRE(pane != nullptr); pane->popOut(); ALPanelFloater* window = pane->getParentByType(); @@ -291,6 +304,7 @@ namespace { Window w = build(); ALDockPanel* pane = ALDockPanel::wrap(w.side, "Side"); + REQUIRE(pane != nullptr); const S32 was = pane->getRect().getHeight(); pane->popOut(); @@ -318,9 +332,11 @@ namespace { Window w = build(); ALDockPanel* pane = ALDockPanel::wrap(w.side, "Side"); + REQUIRE(pane != nullptr); pane->popOut(); LLFloater* window = pane->getParentByType(); + REQUIRE(window != nullptr); const LLRect moved(300, 700, 800, 300); window->setShape(moved); pane->dock(); @@ -328,8 +344,10 @@ namespace UNSCOPED_INFO("where it was is remembered"); CHECK(pane->floatingRect().getWidth() == moved.getWidth()); pane->popOut(); + window = pane->getParentByType(); + REQUIRE(window != nullptr); UNSCOPED_INFO("and it comes back out there"); - CHECK(pane->getParentByType()->getRect().getWidth() == moved.getWidth()); + CHECK(window->getRect().getWidth() == moved.getWidth()); w.floater->die(); } @@ -340,6 +358,7 @@ namespace { Window w = build(); ALDockPanel* pane = ALDockPanel::wrap(w.side, "Side"); + REQUIRE(pane != nullptr); pane->popOut(); w.stack->updateLayout(); @@ -442,6 +461,7 @@ namespace { Window w = build(); ALDockPanel* pane = ALDockPanel::wrap(w.side, "Side"); + REQUIRE(pane != nullptr); const LLHandle held = pane->getHandle(); pane->popOut(); @@ -470,7 +490,9 @@ namespace REQUIRE(pane->poppedOut()); CHECK_FALSE(pane->outAtQuit()); } - pane->getParentByType()->closeFloater(); + window = pane->getParentByType(); + REQUIRE(window != nullptr); + window->closeFloater(); { INFO("closed by hand, it is simply home"); CHECK_FALSE(pane->poppedOut()); diff --git a/indra/llui/tests/alemptystate_test.cpp b/indra/llui/tests/alemptystate_test.cpp index 6b8e8a8dca..d68c871b91 100644 --- a/indra/llui/tests/alemptystate_test.cpp +++ b/indra/llui/tests/alemptystate_test.cpp @@ -127,14 +127,13 @@ namespace CHECK(part(state, "sentence")->getVisible()); // And it is in the middle of the room, not under where a headline - // would have been. + // would have been: as much room above it as below, but for the odd + // pixel of halving. const LLRect sentence = part(state, "sentence")->getRect(); - const S32 middle = state->getRect().getHeight() / 2; - { - INFO("centred on its own " << where(sentence)); - CHECK(sentence.mTop >= middle); - CHECK(sentence.mBottom <= middle); - } + const S32 above = state->getRect().getHeight() - sentence.mTop; + const S32 below = sentence.mBottom; + CAPTURE(above, below); + CHECK(llabs(above - below) <= 1); state->die(); } diff --git a/indra/llui/tests/alhistorylist_test.cpp b/indra/llui/tests/alhistorylist_test.cpp index 776989958a..d71edff32b 100644 --- a/indra/llui/tests/alhistorylist_test.cpp +++ b/indra/llui/tests/alhistorylist_test.cpp @@ -62,9 +62,13 @@ namespace { "label on title", "floater_a.xml" } }; } - static LLScrollListCtrl* listOf(ALHistoryList* history) + // The history's list. Found, not got: getChild makes a stand-in for + // a list the history lacks, which reads as an empty list. + static LLScrollListCtrl* require_list(ALHistoryList* history) { - return history->getChild("steps"); + LLScrollListCtrl* list = history->findChild("steps"); + REQUIRE(list != nullptr); + return list; } }; @@ -82,8 +86,7 @@ namespace CHECK(history->count() == 3u); CHECK(history->inForce() == 3u); - LLScrollListCtrl* list = listOf(history); - REQUIRE(list != nullptr); + LLScrollListCtrl* list = require_list(history); REQUIRE(list->getItemCount() == 3); UNSCOPED_INFO("newest first"); CHECK(list->getFirstData()->getValue().asInteger() == 2); @@ -105,7 +108,7 @@ namespace UNSCOPED_INFO("all three are still listed"); CHECK(history->count() == 3u); CHECK(history->inForce() == 1u); - CHECK(listOf(history)->getItemCount() == 3); + CHECK(require_list(history)->getItemCount() == 3); // More in force than there are steps is not a thing a stack can be. history->setSteps(three(), 99); @@ -129,7 +132,7 @@ namespace std::vector asked; history->onGoTo([&asked](size_t want) { asked.push_back(want); }); - LLScrollListCtrl* list = listOf(history); + LLScrollListCtrl* list = require_list(history); list->setSelectedByValue(LLSD(0), true); // the oldest step history->goToSelected(); REQUIRE(asked.size() == 1u); @@ -159,7 +162,7 @@ namespace // Selecting is what says it: the list commits on a change of // selection, since a row chosen is the whole of the gesture. - listOf(history)->setSelectedByValue(LLSD(1), true); + require_list(history)->setSelectedByValue(LLSD(1), true); REQUIRE(chosen.size() == 1u); CHECK(chosen.front() == 1u); delete history; @@ -175,7 +178,7 @@ namespace } ALHistoryList* history = make(); history->setSteps({}, 0); - CHECK(listOf(history)->getItemCount() == 0); + CHECK(require_list(history)->getItemCount() == 0); ALPaneList* pane = history->getChild("steps"); CHECK(pane->saysEmpty()); @@ -190,16 +193,16 @@ namespace CHECK(asked == std::vector{ 1 }); } - LLScrollListCtrl* list = listOf(history); + LLScrollListCtrl* list = require_list(history); list->setSelectedByValue(LLSD(1), true); std::vector more = three(); more.push_back({ "name on title", "floater_a.xml" }); history->setSteps(more, 4); - CHECK(listOf(history)->getItemCount() == 4); + CHECK(list->getItemCount() == 4); UNSCOPED_INFO("the row a person was on is still the row they are on"); - REQUIRE(listOf(history)->getFirstSelected() != nullptr); - CHECK(listOf(history)->getFirstSelected()->getValue().asInteger() == 1); + REQUIRE(list->getFirstSelected() != nullptr); + CHECK(list->getFirstSelected()->getValue().asInteger() == 1); delete history; } @@ -217,15 +220,18 @@ namespace std::vector chosen; history->onStepChosen([&chosen](size_t at) { chosen.push_back(at); }); - listOf(history)->setSelectedByValue(LLSD(1), true); + require_list(history)->setSelectedByValue(LLSD(1), true); UNSCOPED_INFO("choosing a row says so"); REQUIRE(chosen.size() == 1u); std::vector more = three(); more.push_back({ "name on title", "floater_a.xml" }); history->setSteps(more, 4); - UNSCOPED_INFO("the row is kept"); - CHECK(listOf(history)->getFirstSelected()->getValue().asInteger() == 1); + { + INFO("the row is kept"); + REQUIRE(require_list(history)->getFirstSelected() != nullptr); + CHECK(require_list(history)->getFirstSelected()->getValue().asInteger() == 1); + } UNSCOPED_INFO("and not said again"); CHECK(chosen.size() == 1u); delete history; @@ -239,7 +245,7 @@ namespace } ALHistoryList* history = make(); history->setSteps({ { "width on close_btn", "" }, { "height on close_btn", "" } }, 2); - LLScrollListColumn* where = listOf(history)->getColumn("where"); + LLScrollListColumn* where = require_list(history)->getColumn("where"); REQUIRE(where != nullptr); UNSCOPED_INFO("no room when nothing names a document"); CHECK(where->getWidth() == 0); @@ -262,7 +268,7 @@ namespace } ALHistoryList* history = make("As the files were opened"); history->setSteps(three(), 2); - LLScrollListCtrl* list = listOf(history); + LLScrollListCtrl* list = require_list(history); UNSCOPED_INFO("a row a step, and the start"); REQUIRE(list->getItemCount() == 4); UNSCOPED_INFO("still three steps"); @@ -274,9 +280,10 @@ namespace history->onStepChosen([&pointed](size_t index) { pointed.push_back(index); }); const std::vector rows = list->getAllData(); + const LLScrollListCell* words = rows.back()->getColumn(1); + REQUIRE(words != nullptr); UNSCOPED_INFO("at the bottom, under the oldest"); - CHECK(rows.back()->getColumn(1)->getValue().asString() == - std::string("As the files were opened")); + CHECK(words->getValue().asString() == std::string("As the files were opened")); list->selectNthItem(3); UNSCOPED_INFO("pointed at, it tells nobody of a step"); CHECK(pointed.empty()); @@ -292,8 +299,12 @@ namespace history->setSteps(three(), 0); UNSCOPED_INFO("still chosen after a refill"); CHECK(list->getFirstSelectedIndex() == 3); + const std::vector refilled = list->getAllData(); + REQUIRE_FALSE(refilled.empty()); + const LLScrollListCell* mark = refilled.back()->getColumn(0); + REQUIRE(mark != nullptr); UNSCOPED_INFO("the present is the start"); - CHECK_FALSE(list->getAllData().back()->getColumn(0)->getValue().asString().empty()); + CHECK_FALSE(mark->getValue().asString().empty()); history->goToSelected(); CHECK(asked.size() == 1u); @@ -304,8 +315,9 @@ namespace history = make(); history->setSteps(three(), 3); + list = require_list(history); UNSCOPED_INFO("no words, no start"); - CHECK(listOf(history)->getItemCount() == 3); + CHECK(list->getItemCount() == 3); delete history; } } diff --git a/indra/llui/tests/aljumpbar_test.cpp b/indra/llui/tests/aljumpbar_test.cpp index cf88db1644..86d601a40b 100644 --- a/indra/llui/tests/aljumpbar_test.cpp +++ b/indra/llui/tests/aljumpbar_test.cpp @@ -88,8 +88,10 @@ namespace bar->setPath(path()); CHECK(crumbsIn(bar) == 4); + const LLView* first = bar->findChild("crumb_0", false); + REQUIRE(first != nullptr); UNSCOPED_INFO("a step with nowhere else to be is a plain button"); - CHECK(bar->getChild("crumb_0")->as() == nullptr); + CHECK(first->as() != nullptr); UNSCOPED_INFO("and one with siblings offers them"); REQUIRE(bar->getChild("crumb_2")->as() != nullptr); @@ -292,10 +294,12 @@ namespace LLTextBox* again = bar->findChild("trailer_0", true); REQUIRE(again == line); CHECK(again->getText() == std::string("Ln 120, Col 14")); + const LLTextBox* last = bar->findChild("trailer_2", true); + REQUIRE(last != nullptr); UNSCOPED_INFO("still in order"); - CHECK(again->getRect().mRight <= bar->findChild("trailer_2", true)->getRect().mLeft); + CHECK(again->getRect().mRight <= last->getRect().mLeft); UNSCOPED_INFO("against the far edge"); - CHECK(bar->findChild("trailer_2", true)->getRect().mRight <= bar->getChild("trailer")->getRect().getWidth()); + CHECK(last->getRect().mRight <= bar->getChild("trailer")->getRect().getWidth()); // Another piece, and the trailer is built again. moved.push_back({ "1 warning", "problems", "The problems" }); @@ -358,8 +362,10 @@ namespace REQUIRE(size); CHECK(size->getColor().get() == LLColor4::yellow); } + const LLTextBox* line = bar->findChild("trailer_0", true); + REQUIRE(line != nullptr); UNSCOPED_INFO("the others as they were"); - CHECK(bar->findChild("trailer_0", true)->getColor().get() == quiet); + CHECK(line->getColor().get() == quiet); parts[1].color = LLColor4::red; bar->setTrailer(parts); @@ -401,7 +407,11 @@ namespace const S32 two = font->getWidth(parts[0].text) + font->getWidth(dot) + font->getWidth(parts[4].text); ALJumpBar* bar = make(two + 12 + 6 + 4); bar->setTrailer(parts); - const auto said = [&](size_t i) { return bar->findChild("trailer_" + std::to_string(i), true)->getVisible(); }; + const auto said = [&](size_t i) { + const LLTextBox* piece = bar->findChild("trailer_" + std::to_string(i), true); + REQUIRE(piece != nullptr); + return piece->getVisible(); + }; { INFO("the place and the errors, one separator between"); CHECK(said(0)); @@ -415,8 +425,10 @@ namespace CHECK_FALSE(said(2)); CHECK_FALSE(said(6)); } + const LLTextBox* errors = bar->findChild("trailer_4", true); + REQUIRE(errors != nullptr); UNSCOPED_INFO("against the far edge"); - CHECK(bar->findChild("trailer_4", true)->getRect().mRight <= bar->getChild("trailer")->getRect().getWidth()); + CHECK(errors->getRect().mRight <= bar->getChild("trailer")->getRect().getWidth()); bar->reshape(two * 4, 22); for (size_t i = 0; i < parts.size(); ++i) diff --git a/indra/llui/tests/alkeycapture_test.cpp b/indra/llui/tests/alkeycapture_test.cpp index ffa4ee4e67..46a8cba636 100644 --- a/indra/llui/tests/alkeycapture_test.cpp +++ b/indra/llui/tests/alkeycapture_test.cpp @@ -155,6 +155,7 @@ namespace TEST_CASE_METHOD(alkeycapture_data, "alkeycapture: a one-key capture keeps the last key and Escape gives nothing", "[alkeycapture]") { ALKeyCapture* capture = show(false); + REQUIRE(capture != nullptr); { INFO("Return with nothing pressed gives nothing"); CHECK(ALKeyChords::takeKey(KEY_RETURN, MASK_NONE)); @@ -167,6 +168,7 @@ namespace CHECK(capture->pressed() == (ALKeyChord{ 'S', MASK_NONE })); ALKeyChords::takeChar('s'); ALPopover* up = capture->popover(); + REQUIRE(up != nullptr); CHECK(ALKeyChords::takeKey(KEY_ESCAPE, MASK_NONE)); CHECK(chosen.empty()); { diff --git a/indra/llui/tests/almenuslot_test.cpp b/indra/llui/tests/almenuslot_test.cpp index f122c57153..101de118ff 100644 --- a/indra/llui/tests/almenuslot_test.cpp +++ b/indra/llui/tests/almenuslot_test.cpp @@ -112,6 +112,7 @@ namespace { ALMenuSlot slot; LLContextMenu* menu = make(slot); + REQUIRE(menu); menu->setVisible(true); slot.close(); { @@ -121,6 +122,7 @@ namespace CHECK(slot.get() == nullptr); } open = make(slot); + REQUIRE(open); open->setVisible(true); } { diff --git a/indra/llui/tests/aloutputview_test.cpp b/indra/llui/tests/aloutputview_test.cpp index 8235367333..dc01fbcb9d 100644 --- a/indra/llui/tests/aloutputview_test.cpp +++ b/indra/llui/tests/aloutputview_test.cpp @@ -289,8 +289,11 @@ namespace CHECK(v.shows(v.entries().back())); CHECK_FALSE(v.shows(v.entries().front())); } - UNSCOPED_INFO("the links laid again where the entry moved"); - CHECK(v.substitutions()[1].range == ALTextRange(ALTextPos(2, 2), ALTextPos(2, 16))); + { + INFO("the links laid again where the entry moved"); + REQUIRE(v.substitutions().size() == size_t(3)); + CHECK(v.substitutions()[1].range == ALTextRange(ALTextPos(2, 2), ALTextPos(2, 16))); + } } TEST_CASE_METHOD(aloutputview_data, "aloutputview: each lane keeps its own fill so one lane does not push out another", "[aloutputview]") diff --git a/indra/llui/tests/alpanefolds_test.cpp b/indra/llui/tests/alpanefolds_test.cpp index d5572b6572..1acab3db1e 100644 --- a/indra/llui/tests/alpanefolds_test.cpp +++ b/indra/llui/tests/alpanefolds_test.cpp @@ -206,6 +206,7 @@ namespace folds.bind(w.floater, panes()); folds.toggleOut("page"); LLFloater* window = folds.window("page"); + REQUIRE(window != nullptr); window->setRect(LLRect(700, 500, 1000, 300)); folds.save(state); UNSCOPED_INFO("the side's fold is written"); diff --git a/indra/llui/tests/alpanelist_test.cpp b/indra/llui/tests/alpanelist_test.cpp index 3988dc3eb9..04562e2861 100644 --- a/indra/llui/tests/alpanelist_test.cpp +++ b/indra/llui/tests/alpanelist_test.cpp @@ -103,8 +103,11 @@ namespace return value["group"].asString() + (folded ? (*folded ? " shut" : " open") : " turned"); }; l.setFocus(true); + // With no fold to ask, left on the row chosen goes on to the list, + // which takes it as any list does. + l.selectFirstItem(); UNSCOPED_INFO("no fold: left is the list's"); - CHECK((!l.handleKeyHere(KEY_LEFT, MASK_NONE) || asked.empty())); + CHECK(l.handleKeyHere(KEY_LEFT, MASK_NONE)); l.setFold([&](const LLSD& value, std::optional folded) { asked.push_back(said(value, folded)); }, [](const LLScrollListItem*, S32 x) { return x < 20; }); l.deselectAllItems(); @@ -115,6 +118,7 @@ namespace { INFO("left shuts"); CHECK(l.handleKeyHere(KEY_LEFT, MASK_NONE)); + REQUIRE_FALSE(asked.empty()); CHECK(asked.back() == "0 shut"); } { @@ -126,17 +130,22 @@ namespace UNSCOPED_INFO("with a key held, the list's"); REQUIRE(asked.size() == size_t(2)); l.handleKeyHere(KEY_DOWN, MASK_NONE); - UNSCOPED_INFO("another key is the list's: it moved on"); - CHECK(l.getFirstSelected()->getColumn(0)->getValue().asString() == - std::string("two")); + { + INFO("another key is the list's: it moved on"); + REQUIRE(l.getFirstSelected()); + CHECK(l.getFirstSelected()->getColumn(0)->getValue().asString() == std::string("two")); + } const LLRect first = l.getCellRect(0, 0); { INFO("a press on the arrow turns its row"); CHECK(l.handleMouseDown(first.mLeft + 5, first.getCenterY(), MASK_NONE)); CHECK(asked.back() == "0 turned"); } - UNSCOPED_INFO("and chooses nothing"); - CHECK(l.getFirstSelected()->getColumn(0)->getValue().asString() == std::string("two")); + { + INFO("and chooses nothing"); + REQUIRE(l.getFirstSelected()); + CHECK(l.getFirstSelected()->getColumn(0)->getValue().asString() == std::string("two")); + } l.handleMouseDown(first.mLeft + 40, first.getCenterY(), MASK_NONE); l.handleMouseUp(first.mLeft + 40, first.getCenterY(), MASK_NONE); { @@ -168,8 +177,9 @@ namespace l.sortByColumnIndex(0, false); UNSCOPED_INFO("down, within each group, the groups as they were"); CHECK(order() == std::string("Zed b2 b1 Alpha a2 a1")); - // The rows' own comparison, where the owner gives one: by length, - // then by words. + // The rows' own comparison, where the owner gives one: here the + // words the other way round, so that a sort up comes out within + // each group as the words' own order would down. l.setComparison([](S32, const LLScrollListItem* a, const LLScrollListItem* b) { const std::string x = a->getColumn(0)->getValue().asString(); const std::string y = b->getColumn(0)->getValue().asString(); @@ -417,6 +427,7 @@ namespace value["columns"][3]["column"] = "empty"; value["columns"][3]["value"] = ""; LLScrollListItem* item = l.addElement(value); + REQUIRE(item); item->setEnabled(enabled); }; row("A heading", "", false); @@ -598,9 +609,12 @@ namespace }; l.setRows({ row("a", "alpha"), row("b", "beta"), row("c", "gamma") }); CHECK(order() == std::string("alpha beta gamma")); - LLScrollListItem* b = l.rowWithKey("b"); + LLScrollListItem* b = l.rowWithKey("b"); + LLScrollListItem* c = l.rowWithKey("c"); + REQUIRE(b); + REQUIRE(c); LLScrollListCell* b_cell = b->getColumn(0); - LLScrollListCell* c_cell = l.rowWithKey("c")->getColumn(0); + LLScrollListCell* c_cell = c->getColumn(0); l.selectNthItem(1); l.setRows({ row("a", "alpha"), row("b", "beta"), row("c", "gamma 2") }); { @@ -610,7 +624,9 @@ namespace } { INFO("the changed cell made again"); - CHECK(l.rowWithKey("c")->getColumn(0) != c_cell); + const LLScrollListItem* c_now = l.rowWithKey("c"); + REQUIRE(c_now); + CHECK(c_now->getColumn(0) != c_cell); CHECK(order() == "alpha beta gamma 2"); } UNSCOPED_INFO("still chosen"); diff --git a/indra/llui/tests/alpopover_test.cpp b/indra/llui/tests/alpopover_test.cpp index bd1fcf97f1..f3e1fbdbc2 100644 --- a/indra/llui/tests/alpopover_test.cpp +++ b/indra/llui/tests/alpopover_test.cpp @@ -83,7 +83,8 @@ namespace SKIP("no UI: LLUI_TEST_APP_DIR does not point at the source tree"); } LLPanel* over = anchor(); - ALPopover* popover = ALPopover::show(over, content(160, 90)); + LLPanel* body = content(160, 90); + ALPopover* popover = ALPopover::show(over, body); REQUIRE(popover != nullptr); CHECK(popover->getVisible()); @@ -100,7 +101,7 @@ namespace CHECK(where.mTop == screen.mBottom); UNSCOPED_INFO("the content is in it"); - REQUIRE(popover->getChild("content", true) != nullptr); + CHECK(popover->findChild("content", true) == body); popover->closeFloater(); over->die(); } @@ -136,6 +137,7 @@ namespace { LLPanel* over = anchor(); ALPopover* popover = ALPopover::show(over, content()); + REQUIRE(popover != nullptr); std::vector said; popover->onClosed([&said](bool escaped) { said.push_back(escaped); }); popover->settle(); @@ -149,6 +151,7 @@ namespace { LLPanel* over = anchor(); ALPopover* popover = ALPopover::show(over, content()); + REQUIRE(popover != nullptr); std::vector said; popover->onClosed([&said](bool escaped) { said.push_back(escaped); }); CHECK(popover->handleKeyHere(KEY_ESCAPE, MASK_NONE)); @@ -163,6 +166,7 @@ namespace { LLPanel* over = anchor(); ALPopover* popover = ALPopover::show(over, content()); + REQUIRE(popover != nullptr); std::vector said; popover->onClosed([&said](bool escaped) { said.push_back(escaped); }); popover->onFocusLost(); @@ -181,8 +185,12 @@ namespace { SKIP("no UI: LLUI_TEST_APP_DIR does not point at the source tree"); } + LLPanel* orphan = content(); + const LLHandle given = orphan->getHandle(); UNSCOPED_INFO("no anchor, no popover"); - CHECK(ALPopover::show(nullptr, content()) == nullptr); + CHECK(ALPopover::show(nullptr, orphan) == nullptr); + UNSCOPED_INFO("and the content given deleted, not leaked"); + CHECK(given.isDead()); LLPanel* over = anchor(); UNSCOPED_INFO("and no content, none either"); CHECK(ALPopover::show(over, nullptr) == nullptr); @@ -238,6 +246,7 @@ namespace home->addChild(over); ALPopover* popover = ALPopover::show(over, content()); + REQUIRE(popover != nullptr); UNSCOPED_INFO("it knows where home is"); CHECK(popover->home() == home); UNSCOPED_INFO("and asks before the viewer's own menu does"); @@ -259,6 +268,7 @@ namespace // menu's keys. LLPanel* loose = anchor(); ALPopover* alone = ALPopover::show(loose, content()); + REQUIRE(alone != nullptr); UNSCOPED_INFO("no home"); CHECK(alone->home() == nullptr); UNSCOPED_INFO("no claim"); @@ -337,6 +347,7 @@ namespace LLPanel* over = anchor(); window->addChild(over); ALPopover* popover = ALPopover::show(over, content()); + REQUIRE(popover != nullptr); std::vector said; popover->onClosed([&said](bool escaped) { said.push_back(escaped); }); UNSCOPED_INFO("its window's dependent"); @@ -353,6 +364,7 @@ namespace LLPanel* under = anchor(); again->addChild(under); ALPopover* first = ALPopover::show(under, content()); + REQUIRE(first != nullptr); first->escape(); CHECK(first->isDead()); again->closeFloater(); @@ -542,6 +554,7 @@ namespace ALPopoverSlot slot; std::vector said; ALPopover* popover = ALPopover::show(over, content()); + REQUIRE(popover != nullptr); slot.hold(popover, [&said](bool) { said.push_back(1); }); slot.hold(popover, [&said](bool) { said.push_back(2); }); { @@ -557,6 +570,7 @@ namespace const auto dead = [](const LLHandle& handle) { return !handle.get() || handle.get()->isDead(); }; said.clear(); ALPopover* dropped = ALPopover::show(over, content()); + REQUIRE(dropped != nullptr); const LLHandle handle = dropped->getDerivedHandle(); slot.hold(dropped, [&said](bool) { said.push_back(3); }); slot.drop(); @@ -575,6 +589,7 @@ namespace { ALPopoverSlot gone; ALPopover* held = ALPopover::show(over, content()); + REQUIRE(held != nullptr); scoped = held->getDerivedHandle(); gone.hold(held, [&said](bool) { said.push_back(4); }); } diff --git a/indra/llui/tests/alquickopen_test.cpp b/indra/llui/tests/alquickopen_test.cpp index 4c7f49fb72..9c379963e2 100644 --- a/indra/llui/tests/alquickopen_test.cpp +++ b/indra/llui/tests/alquickopen_test.cpp @@ -107,10 +107,13 @@ namespace UNSCOPED_INFO("and that is what comes back first"); CHECK(best("panel") == std::string("panel_people.xml")); - // `flbuy` is initials-and-more; the file that is only about buying - // wins over the one that is about buying currency. - UNSCOPED_INFO("the tighter of two"); + // `floater_buy` begins both, and `flbuy` is initials-and-more of + // both; either way the file that is only about buying wins over the + // one that is about buying currency. + UNSCOPED_INFO("the tighter of two prefixes"); CHECK(best("floater_buy") == std::string("floater_buy.xml")); + UNSCOPED_INFO("the tighter of two by initials"); + CHECK(best("flbuy") == std::string("floater_buy.xml")); } // Nothing typed answers everything, in the order it was given, because @@ -270,7 +273,9 @@ namespace REQUIRE(list->count() == 1); quick->setHint("Go to line 12."); CHECK(list->choices()[0].text == std::string("Go to line 12.")); - quick->findChild("query")->onCommit(); + LLLineEditor* field = quick->findChild("query"); + REQUIRE(field != nullptr); + field->onCommit(); REQUIRE(chosen.size() == 1); CHECK(chosen[0] == "12"); quick->die(); @@ -297,10 +302,11 @@ namespace } quick->setCandidates(many); const ALChoiceList* list = quick->findChild("matches"); + REQUIRE(list != nullptr); UNSCOPED_INFO("nothing typed: the first as many as were asked for"); REQUIRE(list->count() == 3); UNSCOPED_INFO("in the order given"); - CHECK(quick->listed().front() == "llFunction00"); + CHECK(quick->listed() == std::vector({ "llFunction00", "llFunction01", "llFunction02" })); quick->setQuery("func"); UNSCOPED_INFO("typed: as many as were asked for"); CHECK(list->count() == 3); @@ -310,7 +316,7 @@ namespace CHECK(quick->listed().size() == 3); } UNSCOPED_INFO("and nothing to say about it"); - CHECK(quick->findChild("matches")->placeholder().empty()); + CHECK(list->placeholder().empty()); quick->setQuery("zzz"); CHECK(list->count() == 0); UNSCOPED_INFO("and the list says so"); @@ -411,12 +417,15 @@ namespace p.name = "quick_open"; p.rect = LLRect(0, 60, 300, 0); ALQuickOpen* quick = LLUICtrlFactory::create(p); + // Found before the popover is up: one left up by a failed REQUIRE + // would be escaped after the case, into a callback holding `said`. + LLLineEditor* field = quick->findChild("query"); + REQUIRE(field != nullptr); ALPopover* popover = ALPopover::showOver(window, quick, "Go to line"); REQUIRE(popover != nullptr); std::vector said; popover->onClosed([&said](bool escaped) { said.push_back(escaped); }); quick->takeFocus(); - LLLineEditor* field = quick->findChild("query"); CHECK(gFocusMgr.getKeyboardFocus() == field); field->handleKey(KEY_ESCAPE, MASK_NONE, false); CHECK((popover->isDead() || !popover->getVisible())); @@ -462,7 +471,9 @@ namespace CHECK(quick->handleKeyHere(KEY_UP, MASK_NONE)); CHECK(quick->chosenRow() == 1); } - quick->findChild("query")->onCommit(); + LLLineEditor* field = quick->findChild("query"); + REQUIRE(field != nullptr); + field->onCommit(); { INFO("return takes the one chosen"); REQUIRE(chosen.size() == 1); @@ -470,6 +481,7 @@ namespace } ALChoiceList* list = quick->findChild("matches"); + REQUIRE(list != nullptr); const LLRect text = list->textRect(); const S32 y = text.mTop - list->layout().lineTop(2) - list->layout().rowHeightOf(2, 0) / 2 + list->scrollY(); CHECK(list->handleDoubleClick(text.mLeft + 4, y, MASK_NONE)); diff --git a/indra/llui/tests/alscopebar_test.cpp b/indra/llui/tests/alscopebar_test.cpp index c11829924a..d68a8a8ffc 100644 --- a/indra/llui/tests/alscopebar_test.cpp +++ b/indra/llui/tests/alscopebar_test.cpp @@ -143,12 +143,19 @@ namespace UNSCOPED_INFO("everything fits in the bar"); CHECK(scope->getRect().mRight <= 500); - // Narrower, and the field is what gives up the room. + // Narrower, and the field is what gives up the room: the choices keep + // theirs. + const LLComboBox* what = bar->getChild("field"); + const LLComboBox* how = bar->getChild("how"); const S32 was = field->getRect().getWidth(); + const S32 what_was = what->getRect().getWidth(); + const S32 how_was = how->getRect().getWidth(); + const S32 scope_was = scope->getRect().getWidth(); bar->reshape(360, 30); CHECK(field->getRect().getWidth() < was); - UNSCOPED_INFO("the last choice still has some width"); - CHECK(scope->getRect().getWidth() > 0); + CHECK(what->getRect().getWidth() == what_was); + CHECK(how->getRect().getWidth() == how_was); + CHECK(scope->getRect().getWidth() == scope_was); delete bar; } diff --git a/indra/llui/tests/alspecimenlist_test.cpp b/indra/llui/tests/alspecimenlist_test.cpp index 394454808d..9143977fd5 100644 --- a/indra/llui/tests/alspecimenlist_test.cpp +++ b/indra/llui/tests/alspecimenlist_test.cpp @@ -142,19 +142,28 @@ namespace } ALSpecimenList* list = make(); list->setSpecimens(three()); + // Filtering shows and hides; it builds nothing, so these stay. + const LLView* check_box = list->findChild("row_check_box", true); + const LLView* panel = list->findChild("row_panel", true); + const LLView* containers = list->findChild("heading_Containers", true); + const LLView* controls = list->findChild("heading_Controls", true); + REQUIRE(check_box != nullptr); + REQUIRE(panel != nullptr); + REQUIRE(containers != nullptr); + REQUIRE(controls != nullptr); list->filter("CHECK"); CHECK(list->shown() == 1u); - CHECK(list->findChild("row_check_box", true)->getVisible()); - CHECK_FALSE(list->findChild("row_panel", true)->getVisible()); - CHECK_FALSE(list->findChild("heading_Containers", true)->getVisible()); - CHECK(list->findChild("heading_Controls", true)->getVisible()); + CHECK(check_box->getVisible()); + CHECK_FALSE(panel->getVisible()); + CHECK_FALSE(containers->getVisible()); + CHECK(controls->getVisible()); // A heading is a word to filter by as well: everything under it. list->filter("containers"); UNSCOPED_INFO("the group matched"); CHECK(list->shown() == 1u); - CHECK(list->findChild("row_panel", true)->getVisible()); + CHECK(panel->getVisible()); list->filter(std::string()); CHECK(list->shown() == 3u); @@ -172,10 +181,15 @@ namespace ALSpecimenList* list = make(); list->setSpecimens(three()); + const LLView* scroller = list->findChild("scroller", true); + const LLView* empty = list->findChild("empty", true); + REQUIRE(scroller != nullptr); + REQUIRE(empty != nullptr); + list->filter("qqzzxw"); CHECK(list->shown() == 0u); - CHECK_FALSE(list->findChild("scroller", true)->getVisible()); - CHECK(list->findChild("empty", true)->getVisible()); + CHECK_FALSE(scroller->getVisible()); + CHECK(empty->getVisible()); delete list; } @@ -191,13 +205,15 @@ namespace ALSpecimenList* list = make(); list->setSpecimens(three()); LLView* row = list->findChild("row_check_box", true); + REQUIRE(row != nullptr); const LLRect r = row->getLocalRect(); const S32 x = r.getCenterX(); const S32 y = r.getCenterY(); - // Without a starter, moving the press carries nothing. + // Without a starter, moving the press carries nothing: there is no + // starter to call, and calling the empty one would throw. row->handleMouseDown(x, y, MASK_NONE); - row->handleHover(x + 20, y, MASK_NONE); + CHECK_NOTHROW(row->handleHover(x + 20, y, MASK_NONE)); row->handleMouseUp(x + 20, y, MASK_NONE); std::vector carried; @@ -286,8 +302,10 @@ namespace CHECK(list->shown() == size_t(1)); list->filter("zzz"); CHECK(list->shown() == size_t(0)); + const LLView* empty = list->findChild("empty", true); + REQUIRE(empty != nullptr); UNSCOPED_INFO("the pane says so"); - CHECK(list->findChild("empty", true)->getVisible()); + CHECK(empty->getVisible()); delete list; } @@ -404,6 +422,7 @@ namespace // The cells sit across under their heading: B is the second, at // the tiles' own height less the heading and half a cell. LLView* tiles = list->findChild("tiles", true); + REQUIRE(tiles != nullptr); const S32 x = 150; const S32 y = tiles->getRect().getHeight() - 20 - 30; list->cellPressed(0, MASK_NONE); @@ -455,6 +474,7 @@ namespace UNSCOPED_INFO("both sides are"); CHECK(list->cells()); LLView* tiles = list->findChild("tiles", true); + REQUIRE(tiles != nullptr); { INFO("the picture stayed with the row"); CHECK(tiles->findChild("picture", true) == nullptr); @@ -489,10 +509,15 @@ namespace CHECK(sample->getText() == "Aa Bb 0123"); } CHECK(sample->getFont() == LLFontGL::getFontSansSerif()); + const LLView* mono = list->findChild("row_Monospace", true); + const LLTextBox* mono_sample = mono ? mono->findChild("sample") : nullptr; + REQUIRE(mono_sample != nullptr); UNSCOPED_INFO("the other row in its own"); - CHECK(list->findChild("row_Monospace", true)->findChild("sample")->getFont() == LLFontGL::getFontMonospace()); + CHECK(mono_sample->getFont() == LLFontGL::getFontMonospace()); + const LLTextBox* label = sans->findChild("label"); + REQUIRE(label != nullptr); UNSCOPED_INFO("past the label"); - CHECK(sample->getRect().mLeft > sans->findChild("label")->getRect().mRight - 1); + CHECK(sample->getRect().mLeft > label->getRect().mRight - 1); list->filter("monospace"); UNSCOPED_INFO("found by its name, not only its label"); diff --git a/indra/llui/tests/alstudiofloater_test.cpp b/indra/llui/tests/alstudiofloater_test.cpp index 25bb59a45c..c1ef2d0e11 100644 --- a/indra/llui/tests/alstudiofloater_test.cpp +++ b/indra/llui/tests/alstudiofloater_test.cpp @@ -104,10 +104,20 @@ namespace return made; } + // Every popover still up escaped while what its callbacks count is still + // there: a REQUIRE that ends a case with one up would otherwise leave the + // harness to escape it after the case, into a frame that has gone. + struct EscapeOnExit + { + ~EscapeOnExit() { ll_test::escapePopovers(); } + }; + // Return, as the field gives it: the best answer taken. void choose(ALQuickOpen* quick) { - quick->findChild("query")->onCommit(); + LLLineEditor* query = quick->findChild("query"); + REQUIRE(query != nullptr); + query->onCommit(); } } @@ -139,11 +149,14 @@ namespace TestStudio* window = studio(); std::vector symbols, commands; S32 symbols_escaped = 0; + EscapeOnExit escape_on_exit; ALQuickOpen* first = window->quickOpen(candidates({ "state_entry", "touch_start" }), "Type a symbol", "Go to Symbol", [&](const std::string& v) { symbols.push_back(v); }, nullptr, 0, 0, [&]() { ++symbols_escaped; }); REQUIRE(first != nullptr); - const LLHandle first_up = first->getParentByType()->getHandle(); + LLFloater* first_popover = first->getParentByType(); + REQUIRE(first_popover != nullptr); + const LLHandle first_up = first_popover->getHandle(); ALQuickOpen* second = window->quickOpen(candidates({ "save", "compile" }), "Type a command", "Commands", [&](const std::string& v) { commands.push_back(v); }); @@ -178,10 +191,11 @@ namespace std::vector first_heard, second_heard; ALQuickOpen* first = window->quickOpen(candidates({ "alpha", "beta" }), "Type a name", "Tabs", [&](const std::string& v) { first_heard.push_back(v); }); + REQUIRE(first != nullptr); first->setQuery("be"); ALQuickOpen* again = window->quickOpen(candidates({ "alpha", "beta", "beetle" }), "Type a name", "Tabs", [&](const std::string& v) { second_heard.push_back(v); }); - CHECK(again == first); + REQUIRE(again == first); CHECK(again->query() == std::string("be")); choose(again); REQUIRE(second_heard.size() == 1); @@ -205,11 +219,15 @@ namespace S32 escaped = 0; S32 left = 0; std::vector chosen; + EscapeOnExit escape_on_exit; const auto ask = [&]() { return window->quickOpen(candidates({ "one" }), "Type", "Ways out", [&](const std::string& v) { chosen.push_back(v); }, nullptr, 0, 0, [&]() { ++escaped; }, {}, [&]() { ++left; }); }; - const auto popoverOf = [](ALQuickOpen* quick) { return ALViewType::as(quick->getParentByType()); }; + const auto popoverOf = [](ALQuickOpen* quick) + { + return quick ? ALViewType::as(quick->getParentByType()) : nullptr; + }; ALQuickOpen* quick = ask(); REQUIRE(popoverOf(quick) != nullptr); @@ -220,12 +238,14 @@ namespace CHECK(chosen.empty()); quick = ask(); + REQUIRE(popoverOf(quick) != nullptr); popoverOf(quick)->escape(); CHECK(escaped == 1); UNSCOPED_INFO("escaping is not leaving"); CHECK(left == 1); quick = ask(); + REQUIRE(quick != nullptr); choose(quick); CHECK(chosen.size() == (size_t)1); { @@ -380,6 +400,7 @@ namespace { INFO("a second key, not theirs to give, still does"); CHECK(window->runCommandKey('S', MASK_CONTROL | MASK_SHIFT)); + REQUIRE_FALSE(ran.empty()); CHECK(ran.back() == "save again"); } window->closeFloater(); @@ -452,7 +473,9 @@ namespace // A popover's field, whose keys come home to the window. ALQuickOpen* quick = window->quickOpen(candidates({ "one" }), "Type", "Popover", [](const std::string&) {}); + REQUIRE(quick != nullptr); LLLineEditor* query = quick->findChild("query"); + REQUIRE(query != nullptr); query->setFocus(true); quick->setQuery("world"); { diff --git a/indra/llui/tests/altabstrip_test.cpp b/indra/llui/tests/altabstrip_test.cpp index 586f2eaf9b..2bd970e21c 100644 --- a/indra/llui/tests/altabstrip_test.cpp +++ b/indra/llui/tests/altabstrip_test.cpp @@ -480,6 +480,7 @@ namespace const LLRect second = strip->rectOf(1); strip->handleMouseDown(first.getCenterX(), first.getCenterY(), MASK_NONE); strip->handleHover(second.getCenterX() + 4, second.getCenterY(), MASK_NONE); + REQUIRE(strip->tabs().size() == 3u); UNSCOPED_INFO("dragged past its neighbour, it moved"); CHECK(strip->tabs()[1].value == "a"); UNSCOPED_INFO("nothing said while it is held"); @@ -577,6 +578,7 @@ namespace strip->handleMouseUp(strip->rectOf(1).getCenterX() + 4, HEIGHT / 2, MASK_NONE); { INFO("nothing moved"); + REQUIRE(strip->tabs().size() == 2u); REQUIRE(strip->tabs()[0].value == "b"); CHECK(strip->tabs()[1].value == "c"); } @@ -706,6 +708,7 @@ namespace { INFO("which is still chosen, where it went"); REQUIRE(strip->chosen() == "a"); + REQUIRE(strip->tabs().size() == 3u); CHECK(strip->tabs()[1].value == "a"); } strip->handleKeyHere(KEY_LEFT, MASK_SHIFT); @@ -713,6 +716,7 @@ namespace { INFO("back, and no further than the first"); REQUIRE(orders.size() == 2); + REQUIRE(strip->tabs().size() == 3u); CHECK(strip->tabs()[0].value == "a"); } @@ -768,6 +772,7 @@ namespace CHECK(torn.front() == std::string("b")); S32 screen_x = 0, screen_y = 0; strip->localPointToScreen(second.getCenterX() + 5, -120, &screen_x, &screen_y); + REQUIRE(where.size() == size_t(1)); UNSCOPED_INFO("where, on the screen"); CHECK(where.front() == std::make_pair(screen_x, screen_y)); UNSCOPED_INFO("no longer tearing"); @@ -837,6 +842,7 @@ namespace dirty.badge = LLColor4::red; UNSCOPED_INFO("found"); CHECK(strip->setTab(dirty)); + REQUIRE(strip->tabs().size() == 3u); { INFO("changed in place"); REQUIRE(strip->tabs()[1].dirty); @@ -934,7 +940,7 @@ namespace REQUIRE(cut.find("\xE2\x80\xA6") != std::string::npos); } UNSCOPED_INFO("the extension kept: " << cut); - REQUIRE(cut.compare(cut.size() - 4, 4, ".lsl") == 0); + REQUIRE(cut.ends_with(".lsl")); UNSCOPED_INFO("no mark after the ellipsis, parted from its e: " << cut); REQUIRE(cut.find("\xE2\x80\xA6\xCC") == std::string::npos); UNSCOPED_INFO("the e kept with both its marks: " << cut); diff --git a/indra/llui/tests/alundostack_test.cpp b/indra/llui/tests/alundostack_test.cpp index fe5899102a..b02e72d6b4 100644 --- a/indra/llui/tests/alundostack_test.cpp +++ b/indra/llui/tests/alundostack_test.cpp @@ -54,6 +54,14 @@ namespace } } } + + // The step back taken and made the step forward, as an undo does. + static void undoOne(ALUndoStack& stack) + { + std::optional taken = stack.takeUndo(); + REQUIRE(taken); + stack.pushRedo(std::move(*taken)); + } }; // A change noted is a step back; taking it makes it a step forward @@ -128,12 +136,10 @@ namespace UNSCOPED_INFO("a frame later names nothing"); CHECK(stack.undoLabel() == std::string()); - std::optional back = stack.takeUndo(); - stack.pushRedo(*back); + undoOne(stack); UNSCOPED_INFO("the step forward keeps its name"); CHECK(stack.redoLabel() == std::string()); - back = stack.takeUndo(); - stack.pushRedo(*back); + undoOne(stack); UNSCOPED_INFO("and so does the next"); CHECK(stack.redoLabel() == std::string("Set the fill")); } @@ -161,8 +167,7 @@ namespace ALUndoStack stack; stack.note({ { "a" } }, "slider", 0.0, 1.0, join); stack.note({ { "b" } }, "other", 0.1, 1.0, join); - auto step = stack.takeUndo(); - stack.pushRedo(*step); + undoOne(stack); stack.note({ { "c" } }, "other", 0.2, 1.0, join); UNSCOPED_INFO("an edit after undo starts a new step"); REQUIRE(stack.inForce() == size_t(2)); @@ -258,7 +263,7 @@ namespace const auto back_to_start = [](const Step& step) { return step.mNames.size() >= 2 && step.mNames.back() == "start"; }; stack.note({ { "one" } }, "", 0.0, 1.0, join); stack.note({ { "two" } }, "", 5.0, 1.0, join); - stack.pushRedo(*stack.takeUndo()); + undoOne(stack); stack.note({ { "drag" } }, "slider", 10.0, 1.0, join, back_to_start); UNSCOPED_INFO("a drag is a step"); CHECK(stack.inForce() == size_t(2)); @@ -334,8 +339,9 @@ namespace CHECK_FALSE(stack.closeGroups()); stack.beginGroup(); stack.note({ { "6" } }, "", 40.0, 1.0, join); - stack.pushRedo(*stack.takeUndo()); + undoOne(stack); stack.note({ { "7" } }, "", 40.0, 1.0, join); + REQUIRE(stack.canUndo()); UNSCOPED_INFO("taken back while open: the next change a step of its own"); CHECK(stack.undone().back().mNames == std::vector{ "7" }); stack.clear(); @@ -405,10 +411,10 @@ namespace } stack.note({ { "d" } }, "", 20.0, 1.0, join); - stack.pushRedo(*stack.takeUndo()); + undoOne(stack); stack.note({ { "e" } }, "", 30.0, 1.0, join); - stack.pushRedo(*stack.takeUndo()); - stack.pushRedo(*stack.takeUndo()); + undoOne(stack); + undoOne(stack); std::vector forward = stack.takeForward(); { INFO("taken out"); diff --git a/indra/llui/tests/alviewtype_test.cpp b/indra/llui/tests/alviewtype_test.cpp index fe60143775..a8756c471c 100644 --- a/indra/llui/tests/alviewtype_test.cpp +++ b/indra/llui/tests/alviewtype_test.cpp @@ -134,7 +134,7 @@ namespace }; // Each class answers for itself and every base that is a kind, and for - // nothing else. A type with no kind is still answered, the slow way. + // nothing else; a class a test declares answers as the library's do. TEST_CASE_METHOD(alviewtype_data, "alviewtype: as<> finds a view's own class and each base kind but no other", "[alviewtype]") { if (!ui.ok()) @@ -207,7 +207,7 @@ namespace CHECK(inner->getParentByType() == f); UNSCOPED_INFO("the nearest floater view above it, through the floater"); CHECK(inner->getParentByType() == fv.get()); - UNSCOPED_INFO("no ancestor is a type with no kind"); + UNSCOPED_INFO("its ancestors are controls, but none a TestCtrl"); CHECK(inner->getParentByType() == nullptr); fv.reset(); @@ -361,6 +361,10 @@ namespace // Bound by on_check, a binding the item's own params do not name. TEST_CASE_METHOD(alviewtype_data, "alviewtype: a check item bound by on_check stays bound and shows the setting", "[alviewtype]") { + if (!ui.ok()) + { + SKIP("no UI: LLUI_TEST_APP_DIR does not point at the source tree"); + } LLControlGroup* config = LLUI::getInstance()->mSettingGroups["config"]; if (!config->controlExists("ViewTypeCheck")) { From ff37a3a6462b547ed6a2cfc53185493f12b058be Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 13 Oct 2026 23:21:56 -0400 Subject: [PATCH 6/8] llui's XUI tests check what they say they check Old test bugs TM23's idiom pass found (b9f739bcda), fixed. - alxuischema: "sentences from the notes" checked only that some note existed; it checks the tag's sentence is the one the notes hold. Tags are required before they are read. - alxuiservice: - a hover check was REQUIRE(... || true); it checks the hover is empty where there is no word; - "the open button is not a parent" checks the parents number two. - alxuitranslate: the second missed attribute's reason is checked as well as the first's. - alcanvasview: "a plain surface does not drag" read the flag it set and "until it says so" what a press does; both read what a press does, so the pair shows setDragPans is what changed it. - alxuicatalog, alxuilint, alxuisourcemap, alxuitreemodel: lookups, layers, findings, drag results and built views are required before they are read. - alxuiedit: a stray comment over another case's check is gone. Each new or corrected check fails under a small change to the test's own input or expectation, at the check and not by crashing. Co-Authored-By: Claude Opus 5.5 --- indra/llui/tests/alcanvasview_test.cpp | 5 +++- indra/llui/tests/alxuicatalog_test.cpp | 22 ++++++++++---- indra/llui/tests/alxuiedit_test.cpp | 1 - indra/llui/tests/alxuilint_test.cpp | 2 ++ indra/llui/tests/alxuischema_test.cpp | 21 +++++++++----- indra/llui/tests/alxuiservice_test.cpp | 9 ++++-- indra/llui/tests/alxuisourcemap_test.cpp | 37 +++++++++++++++++++----- indra/llui/tests/alxuitranslate_test.cpp | 15 +++++----- indra/llui/tests/alxuitreemodel_test.cpp | 12 ++++++-- 9 files changed, 89 insertions(+), 35 deletions(-) diff --git a/indra/llui/tests/alcanvasview_test.cpp b/indra/llui/tests/alcanvasview_test.cpp index 7e1c393010..14bf422030 100644 --- a/indra/llui/tests/alcanvasview_test.cpp +++ b/indra/llui/tests/alcanvasview_test.cpp @@ -1147,8 +1147,11 @@ namespace UNSCOPED_INFO("and down"); CHECK(area->getDocPosVertical() == 40); + // panGesture() is whether a left press pans, which the space bar + // held makes it too: asked before the surface says it drags and + // after, it shows the saying is what changed it. UNSCOPED_INFO("a plain surface does not drag by the left button"); - CHECK_FALSE(canvas->dragPans()); + CHECK_FALSE(canvas->panGesture()); canvas->setDragPans(true); UNSCOPED_INFO("until it says so"); CHECK(canvas->panGesture()); diff --git a/indra/llui/tests/alxuicatalog_test.cpp b/indra/llui/tests/alxuicatalog_test.cpp index 36cb81fb1a..74d1323d7c 100644 --- a/indra/llui/tests/alxuicatalog_test.cpp +++ b/indra/llui/tests/alxuicatalog_test.cpp @@ -138,8 +138,12 @@ namespace CHECK(a->layer("other", "en") != nullptr); CHECK(a->layer("other", "de") == nullptr); - CHECK((int)catalog.find("panel_b.xml")->kind == (int)ALXUICatalog::Kind::Panel); - CHECK((int)catalog.find("menu_c.xml")->kind == (int)ALXUICatalog::Kind::Menu); + const ALXUICatalog::Entry* panel = catalog.find("panel_b.xml"); + REQUIRE(panel != nullptr); + CHECK((int)panel->kind == (int)ALXUICatalog::Kind::Panel); + const ALXUICatalog::Entry* menu = catalog.find("menu_c.xml"); + REQUIRE(menu != nullptr); + CHECK((int)menu->kind == (int)ALXUICatalog::Kind::Menu); const ALXUICatalog::Entry* tmpl = catalog.find("widgets/button.xml"); UNSCOPED_INFO("template catalogued under its subdirectory"); REQUIRE(tmpl != nullptr); @@ -148,6 +152,7 @@ namespace const ALXUICatalog::Entry* broken = catalog.find("broken.xml"); UNSCOPED_INFO("a file that does not parse is still catalogued"); REQUIRE(broken != nullptr); + REQUIRE(broken->layers.size() == 1u); CHECK(broken->layers[0].doc == nullptr); UNSCOPED_INFO("and the reason"); CHECK_FALSE(broken->layers[0].error.empty()); @@ -162,7 +167,9 @@ namespace { ALXUICatalog catalog; catalog.scan(mSkins); - const ALXUICatalog::Entry& a = *catalog.find("floater_a.xml"); + const ALXUICatalog::Entry* found = catalog.find("floater_a.xml"); + REQUIRE(found != nullptr); + const ALXUICatalog::Entry& a = *found; std::vector layers = catalog.layersFor(a, "default", "en"); UNSCOPED_INFO("default/en: one file"); @@ -185,7 +192,9 @@ namespace UNSCOPED_INFO("default's language file"); CHECK(layers[1]->skin == std::string("default")); - layers = catalog.layersFor(*catalog.find("panel_b.xml"), "other", "de"); + const ALXUICatalog::Entry* b = catalog.find("panel_b.xml"); + REQUIRE(b != nullptr); + layers = catalog.layersFor(*b, "other", "de"); UNSCOPED_INFO("a file the skin lacks falls back to default"); REQUIRE(layers.size() == 1u); CHECK(layers[0]->skin == std::string("default")); @@ -242,7 +251,10 @@ namespace UNSCOPED_INFO("in the German layer"); CHECK(hits[0].layer->language == std::string("de")); - const ALXUICatalog::Layer* en = catalog.find("floater_a.xml")->layer("default", "en"); + const ALXUICatalog::Entry* floater = catalog.find("floater_a.xml"); + REQUIRE(floater != nullptr); + const ALXUICatalog::Layer* en = floater->layer("default", "en"); + REQUIRE(en != nullptr); pugi::xml_node second = ALXUICatalog::resolve(en->root(), ALXUISelection::fromString("body/ok#1")); UNSCOPED_INFO("the second button resolves"); REQUIRE(second); diff --git a/indra/llui/tests/alxuiedit_test.cpp b/indra/llui/tests/alxuiedit_test.cpp index 099a2f20b5..93ef626d4d 100644 --- a/indra/llui/tests/alxuiedit_test.cpp +++ b/indra/llui/tests/alxuiedit_test.cpp @@ -684,7 +684,6 @@ namespace CHECK(edit.text().find("top=\"40\"") != std::string::npos); } - // A move is a removal and an insertion; one undo is the whole move. { INFO("undo the redos"); CHECK(edit.undo()); diff --git a/indra/llui/tests/alxuilint_test.cpp b/indra/llui/tests/alxuilint_test.cpp index 76da274bf0..85efe235c1 100644 --- a/indra/llui/tests/alxuilint_test.cpp +++ b/indra/llui/tests/alxuilint_test.cpp @@ -332,6 +332,8 @@ namespace " \n" " \n" " ", /*decisive=*/false)); + UNSCOPED_INFO("callback not registered: " + run.describe()); + REQUIRE(run.count(ALXUILint::Rule::CallbackNotRegistered) == 1); UNSCOPED_INFO("advisory in a shell build"); CHECK((int)run.first(ALXUILint::Rule::CallbackNotRegistered)->severity == (int)ALXUILint::Severity::Note); diff --git a/indra/llui/tests/alxuischema_test.cpp b/indra/llui/tests/alxuischema_test.cpp index 542093e0cc..33cf79a66e 100644 --- a/indra/llui/tests/alxuischema_test.cpp +++ b/indra/llui/tests/alxuischema_test.cpp @@ -324,7 +324,9 @@ namespace UNSCOPED_INFO("a real is a type of its own"); CHECK(found); - const ALXUISchema::Attribute* alpha = find(*schema().tag("panel"), "bg_alpha_color.alpha"); + const ALXUISchema::Tag* panel = schema().tag("panel"); + REQUIRE(panel != nullptr); + const ALXUISchema::Attribute* alpha = find(*panel, "bg_alpha_color.alpha"); REQUIRE(alpha != nullptr); UNSCOPED_INFO("a colour component is a real"); CHECK((int)alpha->value == (int)ALParamType::REAL); @@ -374,8 +376,10 @@ namespace CHECK(find(*text, "font") != nullptr); UNSCOPED_INFO("and its parts"); CHECK(find(*text, "font.name") != nullptr); + const ALXUISchema::Tag* button = schema().tag("button"); + REQUIRE(button != nullptr); UNSCOPED_INFO("and an image"); - CHECK(find(*schema().tag("button"), "image_unselected") != nullptr); + CHECK(find(*button, "image_unselected") != nullptr); } // A registry knows that a button takes a label and cannot know what a @@ -577,15 +581,16 @@ namespace UNSCOPED_INFO("and nothing said about it"); CHECK_FALSE(name->deprecated); - // One sentence per tag, for the Library and for the XSD. + // One sentence per tag, for the Library and for the XSD, and it is + // the notes' own. A tag nobody has written one for falls back to what + // it did before there were sentences: nothing waits on prose. Every + // tag this binary registers has one, so the case above asks that of a + // name nobody wrote about. const ALXUISchema::Tag* button = schema().tag("button"); REQUIRE(button != nullptr); CHECK_FALSE(button->note.empty()); - - // A tag nobody has written one for falls back to what it did before - // there were sentences: nothing waits on prose. - UNSCOPED_INFO("and the notes do not have to be complete"); - CHECK(ALXUINotes::get().count() > 0); + UNSCOPED_INFO("the sentence is the one the notes hold"); + CHECK(button->note == ALXUINotes::get().note("button")); } // The kind a tag is looked for under, and whether a specimen of it can diff --git a/indra/llui/tests/alxuiservice_test.cpp b/indra/llui/tests/alxuiservice_test.cpp index 36ed602ad3..8a284bcca6 100644 --- a/indra/llui/tests/alxuiservice_test.cpp +++ b/indra/llui/tests/alxuiservice_test.cpp @@ -108,8 +108,11 @@ namespace CHECK(c.attribute == std::string("label")); c = ALXUIService::contextAt(doc, ALTextPos(5, 7)); // getChildList(); S32 seen = 0; for (auto it = children.rbegin(); it != children.rend(); ++it) @@ -173,16 +179,31 @@ namespace REQUIRE(same); } - CHECK(map.find(floater)->line == 1); - CHECK(map.find(floater)->tag == std::string("floater")); - CHECK(map.find(outer)->line == 3); - CHECK(map.find(inner1)->line == 4); + const ALXUISourceMap::Origin* from_floater = map.find(floater); + const ALXUISourceMap::Origin* from_outer = map.find(outer); + const ALXUISourceMap::Origin* from_inner1 = map.find(inner1); + const ALXUISourceMap::Origin* from_inner2 = map.find(inner2); + const ALXUISourceMap::Origin* from_leaf = map.find(leaf); + const ALXUISourceMap::Origin* from_other = map.find(other); + { + INFO("each of them came from the file"); + REQUIRE(from_floater); + REQUIRE(from_outer); + REQUIRE(from_inner1); + REQUIRE(from_inner2); + REQUIRE(from_leaf); + REQUIRE(from_other); + } + CHECK(from_floater->line == 1); + CHECK(from_floater->tag == std::string("floater")); + CHECK(from_outer->line == 3); + CHECK(from_inner1->line == 4); UNSCOPED_INFO("the second inner, not the first again"); - CHECK(map.find(inner2)->line == 5); + CHECK(from_inner2->line == 5); UNSCOPED_INFO("the leaf under the second inner"); - CHECK(map.find(leaf)->line == 7); + CHECK(from_leaf->line == 7); UNSCOPED_INFO("other, after a parameter element and a subtree"); - CHECK(map.find(other)->line == 10); + CHECK(from_other->line == 10); REQUIRE(dragHandle(floater) != nullptr); UNSCOPED_INFO("which came from no element"); diff --git a/indra/llui/tests/alxuitranslate_test.cpp b/indra/llui/tests/alxuitranslate_test.cpp index 0698f270a2..e2182922a8 100644 --- a/indra/llui/tests/alxuitranslate_test.cpp +++ b/indra/llui/tests/alxuitranslate_test.cpp @@ -32,6 +32,8 @@ #include +#include + namespace { struct alxuitranslate_data @@ -581,11 +583,7 @@ namespace // merge reads and anything else would be an override nobody asked for. const pugi::xml_node inner = overlay.resolve({ "inner" }); REQUIRE(inner); - S32 attributes = 0; - for (pugi::xml_attribute one : inner.attributes()) - { - ++attributes; - } + const S32 attributes = (S32)std::distance(inner.attributes_begin(), inner.attributes_end()); UNSCOPED_INFO("carrying only its name"); CHECK(attributes == 1); @@ -637,8 +635,11 @@ namespace CHECK((int)one->state == (int)ALXUITranslate::State::NotApplied); UNSCOPED_INFO("neither of them"); CHECK((int)two->state == (int)ALXUITranslate::State::NotApplied); - UNSCOPED_INFO("and both say the base has the name elsewhere"); - CHECK((int)one->miss == (int)ALXUITranslate::Miss::Moved); + { + INFO("and both say the base has the name elsewhere"); + CHECK((int)one->miss == (int)ALXUITranslate::Miss::Moved); + CHECK((int)two->miss == (int)ALXUITranslate::Miss::Moved); + } { INFO("so neither applies"); CHECK_FALSE(one->applies()); diff --git a/indra/llui/tests/alxuitreemodel_test.cpp b/indra/llui/tests/alxuitreemodel_test.cpp index dc574fcc94..a1da841823 100644 --- a/indra/llui/tests/alxuitreemodel_test.cpp +++ b/indra/llui/tests/alxuitreemodel_test.cpp @@ -283,8 +283,10 @@ namespace CHECK(hidden->isShown()); UNSCOPED_INFO("the authored state is remembered"); CHECK_FALSE(hidden->isAuthoredVisible()); + const ALXUISourceMap::Origin* hidden_origin = map.find(hidden->getView()); + REQUIRE(hidden_origin != nullptr); std::string authored; - map.find(hidden->getView())->node->getAttributeString("visible", authored); + hidden_origin->node->getAttributeString("visible", authored); UNSCOPED_INFO("and the document still says what it said"); CHECK(authored == std::string("false")); hidden->toggleShown(); @@ -303,6 +305,7 @@ namespace delete floater; LLXMLNodePtr root2; LLFloater* floater2 = build(fv.get(), root2); + REQUIRE(floater2 != nullptr); map.build(floater2, root2); model.build(floater2, map); ALXUITreeItem* outer2 = model.itemFor(ALXUISelection::fromString("outer")); @@ -483,6 +486,8 @@ namespace std::vector items { inner->getViewModelItem() }; UNSCOPED_INFO("and starting a drag says which"); CHECK(model.startDrag(items)); + UNSCOPED_INFO("to the starter, once"); + REQUIRE(picked.size() == 1u); UNSCOPED_INFO("by path"); CHECK(ALXUISelection::toString(picked.front()) == std::string("outer/inner")); @@ -588,10 +593,13 @@ namespace ll_test::FloaterViewScope floater_view(fv.get()); LLXMLNodePtr root; LLFloater* floater = build(fv.get(), root); + REQUIRE(floater != nullptr); ALXUISourceMap map; map.build(floater, root); // A label with a capital, which the words find all the same. - floater->findChild("inner", true)->setName("Inner"); + LLView* first_inner = floater->findChild("inner", true); + REQUIRE(first_inner != nullptr); + first_inner->setName("Inner"); ALXUITreeModel model; model.build(floater, map); ALXUITreeItem* top = model.rootItem(); From c0532cb4cb5be5d2f83eb77c1bfcb6d4ee55a358 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 13 Oct 2026 23:22:06 -0400 Subject: [PATCH 7/8] Linden's widget tests in llui check what they say they check Old test bugs TM23's idiom pass found (b9f739bcda), fixed. - llemojidictionary: "findVariant with no gender asked for picks the gender-neutral variant" passed by the variants' order: the two the fixture had scored alike. A variant that carries no gender is added after them and expected, and the case is renamed for it. - llcombobox: "choosing it settles it" followed setUnset(false), not a choice; that step is said as it is, and a choice in the control follows it. - lltabcontainer: "setTabBadge keeps a badge once the count is cleared" checks it is shown with the count and hidden after, and is renamed for it. Tab buttons are required before they are read. - llstatbar: an empty range's tick spacing is checked to be 0, and the case is renamed for it; a comment about denormals says the bounds are normal floats. - llurlentry: the "multicase" inputs were all lower case; they mix it. - lllayoutstack, lltextparser, lluicolortable: resize bars, highlights and table entries are required before they are read. Each new or corrected check fails under a small change to the test's own input or expectation, at the check and not by crashing. Co-Authored-By: Claude Opus 5.5 --- indra/llui/tests/llcombobox_test.cpp | 7 +++ indra/llui/tests/llemojidictionary_test.cpp | 26 ++++++---- indra/llui/tests/lllayoutstack_test.cpp | 3 ++ indra/llui/tests/llstatbar_test.cpp | 10 ++-- indra/llui/tests/lltabcontainer_test.cpp | 56 ++++++++++++++++++--- indra/llui/tests/lltextparser_test.cpp | 1 + indra/llui/tests/lluicolortable_test.cpp | 12 +++-- indra/llui/tests/llurlentry_test.cpp | 8 +-- 8 files changed, 96 insertions(+), 27 deletions(-) diff --git a/indra/llui/tests/llcombobox_test.cpp b/indra/llui/tests/llcombobox_test.cpp index c2d2dd4ee2..865420379f 100644 --- a/indra/llui/tests/llcombobox_test.cpp +++ b/indra/llui/tests/llcombobox_test.cpp @@ -194,6 +194,13 @@ namespace CHECK(text->getTentative()); combo->setUnset(false); + UNSCOPED_INFO("and saying it is set settles it"); + CHECK_FALSE(text->getTentative()); + + // A choice made in the control settles it too, without its owner + // saying so. + combo->setUnset(true); + combo->selectFirstItem(); UNSCOPED_INFO("and choosing it settles it"); CHECK_FALSE(text->getTentative()); combo->die(); diff --git a/indra/llui/tests/llemojidictionary_test.cpp b/indra/llui/tests/llemojidictionary_test.cpp index f0c610923c..8a15710972 100644 --- a/indra/llui/tests/llemojidictionary_test.cpp +++ b/indra/llui/tests/llemojidictionary_test.cpp @@ -156,19 +156,26 @@ namespace } // findVariant: prefer the cleanest match — gender=-1 user shouldn't - // get a gendered variant when a gender-neutral one of the same tone - // exists. - TEST_CASE_METHOD(llemojidictionary_data, "llemojidictionary: findVariant with no gender asked for picks the gender-neutral variant", "[llemojidictionary]") + // get a gendered variant when one of the same tone that carries no + // gender exists. + TEST_CASE_METHOD(llemojidictionary_data, "llemojidictionary: findVariant with no gender asked for picks the variant that carries none", "[llemojidictionary]") { + // The astronaut's tone-3 variants all carry a gender, person (2) + // as much as man (0), and score alike. One that carries none goes + // after them all, so that the +1 for not asserting a gender we + // didn't ask for is what picks it, and not the order. LLEmojiDescriptor d = make_astronaut_descriptor(); + LLEmojiVariant clean; + utf8str_append_cp(clean.Character, (llwchar)0x1F9D1); + utf8str_append_cp(clean.Character, (llwchar)(0x1F3FA + 3)); + clean.Tone = 3; + clean.Gender = -1; + d.Variants.push_back(clean); + const LLEmojiVariant* v = LLEmojiDictionary::instance().findVariant(d, 3, -1); REQUIRE(v != nullptr); CHECK(v->Tone == U8(3)); - // Gender preference unset — best match should have Gender=2 - // (person, gender-neutral) with the +1 bonus for not asserting a - // gender we didn't ask for. (Both 'person' and bare 'person' - // are gender=2 in this fixture.) - CHECK(v->Gender == S8(2)); + CHECK(v->Gender == S8(-1)); } // findVariant: partial-axis match still beats nothing — user asks @@ -203,7 +210,8 @@ namespace CHECK(out[0].Tone == U8(5)); UNSCOPED_INFO("no Gender given: unset"); CHECK(out[0].Gender == S8(-1)); - CHECK(out[0].ShortCodes.size() == size_t(1)); + REQUIRE(out[0].ShortCodes.size() == size_t(1)); + CHECK(out[0].ShortCodes.front() == std::string(":thumbs_up_dark_skin_tone:")); } TEST_CASE_METHOD(llemojidictionary_data, "llemojidictionary: loadVariants with no Variants key returns none", "[llemojidictionary]") diff --git a/indra/llui/tests/lllayoutstack_test.cpp b/indra/llui/tests/lllayoutstack_test.cpp index dbe7f0e7ca..14ab89399f 100644 --- a/indra/llui/tests/lllayoutstack_test.cpp +++ b/indra/llui/tests/lllayoutstack_test.cpp @@ -432,6 +432,7 @@ namespace s->addPanel(grows); s->addPanel(dragged); s->updateLayout(); + REQUIRE(grows->getResizeBar() != nullptr); UNSCOPED_INFO("the pair can resize, so there is somewhere to drag"); CHECK(grows->getResizeBar()->getVisible()); s->die(); @@ -446,6 +447,8 @@ namespace s->addPanel(between); s->addPanel(dragged); s->updateLayout(); + REQUIRE(between->getResizeBar() != nullptr); + REQUIRE(grows->getResizeBar() != nullptr); UNSCOPED_INFO("a band that cannot give room has no bar of its own"); CHECK_FALSE(between->getResizeBar()->getVisible()); UNSCOPED_INFO("and it takes the bar off the panel behind it too"); diff --git a/indra/llui/tests/llstatbar_test.cpp b/indra/llui/tests/llstatbar_test.cpp index 689ed1d961..74da51414f 100644 --- a/indra/llui/tests/llstatbar_test.cpp +++ b/indra/llui/tests/llstatbar_test.cpp @@ -113,7 +113,8 @@ namespace TEST_CASE_METHOD(llstatbar_data, "llstatbar: calcTickValue over a tiny range about zero is finite and not negative", "[llstatbar]") { - // Both bounds tiny enough that their sum rounds to zero. + // Bounds a tiny distance either side of zero: normal floats still, + // not denormal ones, which start below about 1.2e-38. const F32 tick = LLStatBar::calcTickValue(-1e-30f, 1e-30f); CHECK(finite(tick)); CHECK(tick >= 0.f); @@ -184,13 +185,14 @@ namespace } // A range with nothing in it must not produce a spacing the tick walk would - // step forever on, and must stay finite. - TEST_CASE_METHOD(llstatbar_data, "llstatbar: an empty range scales to finite bounds and spacing", "[llstatbar]") + // step forever on, and must stay finite. No spacing at all is what the bar + // walks no ticks for. + TEST_CASE_METHOD(llstatbar_data, "llstatbar: an empty range scales to finite bounds and no tick spacing", "[llstatbar]") { const Scaled empty = scale(0.f, 0.f); CHECK(finite(empty.mMin)); CHECK(finite(empty.mMax)); - CHECK((empty.mTick == 0.f || finite(empty.mTick))); + CHECK(empty.mTick == 0.f); } // Both bounds on the same side of zero and equal: the degenerate case the diff --git a/indra/llui/tests/lltabcontainer_test.cpp b/indra/llui/tests/lltabcontainer_test.cpp index 39100b553e..f5508fe310 100644 --- a/indra/llui/tests/lltabcontainer_test.cpp +++ b/indra/llui/tests/lltabcontainer_test.cpp @@ -26,6 +26,7 @@ #include "../lltabcontainer.h" +#include "../llbadge.h" #include "../llbutton.h" #include "../llfocusmgr.h" #include "../lliconctrl.h" @@ -83,6 +84,24 @@ namespace return tabs->findChild((vertical ? "vtab_" : "htab_") + name, true); } + // The badge on a tab. The strip holds it rather than the button, so + // that it is drawn over everything, and with one tab there is one. + static const LLBadge* badgeIn(const LLView* view) + { + for (const LLView* child : *view->getChildList()) + { + if (const LLBadge* badge = child->as()) + { + return badge; + } + if (const LLBadge* badge = badgeIn(child)) + { + return badge; + } + } + return nullptr; + } + // A texture with a size and nothing else: what a UI image needs to // exist here, where no image provider makes any. struct SizedTexture : public LLTexture @@ -133,7 +152,7 @@ namespace // A count on the tab, and off it again. A tab with nothing to say looks // like one that never had anything. - TEST_CASE_METHOD(lltabcontainer_data, "lltabcontainer: setTabBadge makes a badge and keeps it once the count is cleared", "[lltabcontainer]") + TEST_CASE_METHOD(lltabcontainer_data, "lltabcontainer: setTabBadge makes a badge and keeps it hidden once the count is cleared", "[lltabcontainer]") { if (!ui.ok()) { @@ -150,10 +169,19 @@ namespace tabs->setTabBadge(counted, "12"); CHECK(button->hasBadge()); + const LLBadge* badge = badgeIn(tabs); + REQUIRE(badge != nullptr); + { + INFO("showing the count"); + CHECK(badge->getVisible()); + CHECK(badge->getLabel() == std::string("12")); + } tabs->setTabBadge(counted, std::string()); UNSCOPED_INFO("the badge stays made once it is made"); CHECK(button->hasBadge()); + UNSCOPED_INFO("and is hidden, as a tab with no count has none showing"); + CHECK_FALSE(badge->getVisible()); tabs->die(); } @@ -684,7 +712,9 @@ namespace tabs->addTabPanel(page("g", std::string())); LLPanel* last = page("h", std::string()); tabs->addTabPanel(last); - CHECK(tabButton(tabs, "f")->getRect().getWidth() == tabs->getMaxTabWidth()); + const LLButton* wide_button = tabButton(tabs, "f"); + REQUIRE(wide_button != nullptr); + CHECK(wide_button->getRect().getWidth() == tabs->getMaxTabWidth()); tabs->setTabVisibility(wide, false); UNSCOPED_INFO("seven tabs of sixty in four hundred: one more than fit"); @@ -711,7 +741,9 @@ namespace tabs->addTabPanel(first); tabs->addTabPanel(second); tabs->selectTabPanel(first); - tabButton(tabs, "first")->setFocus(true); + LLButton* first_button = tabButton(tabs, "first"); + REQUIRE(first_button != nullptr); + first_button->setFocus(true); CHECK_FALSE(tabs->handleKeyHere(KEY_RIGHT, MASK_CONTROL)); CHECK(tabs->getCurrentPanel() == first); @@ -735,8 +767,11 @@ namespace LLPanel* indented = page("indented", std::string()); tabs->addTabPanel(plain); tabs->addTabPanel(LLTabContainer::TabPanelParams().panel(indented).indent(10)); - const S32 pad = tabButton(tabs, "plain")->getLeftHPad(); - CHECK(tabButton(tabs, "indented")->getLeftHPad() == pad + 10); + const LLButton* plain_button = tabButton(tabs, "plain"); + const LLButton* indented_button = tabButton(tabs, "indented"); + REQUIRE(plain_button != nullptr); + REQUIRE(indented_button != nullptr); + CHECK(indented_button->getLeftHPad() == plain_button->getLeftHPad() + 10); tabs->die(); } @@ -829,12 +864,16 @@ namespace // Four tabs of sixty in the three hundred and ninety-eight between // the page's sides: each gets its sixty and a fourth of the rest. + // Each tab's button is found here, and the strip keeps it as long as + // it keeps the tab, so the reads of them below have it to read. const S32 room = 398; S32 total = 0; for (const char* name : { "a", "b", "c", "d" }) { - const S32 width = tabButton(tabs, name)->getRect().getWidth(); CAPTURE(name); + const LLButton* button = tabButton(tabs, name); + REQUIRE(button != nullptr); + const S32 width = button->getRect().getWidth(); REQUIRE(width > 60); total += width; } @@ -907,7 +946,10 @@ namespace S32 total = 0; for (const char* name : { "a", "b", "c", "d" }) { - total += tabButton(tabs, name)->getRect().getWidth(); + CAPTURE(name); + const LLButton* tab = tabButton(tabs, name); + REQUIRE(tab != nullptr); + total += tab->getRect().getWidth(); } CHECK(total == 398 - KEPT); CHECK(1 + total <= button->getRect().mLeft); diff --git a/indra/llui/tests/lltextparser_test.cpp b/indra/llui/tests/lltextparser_test.cpp index cb3242993f..f0b4bbedae 100644 --- a/indra/llui/tests/lltextparser_test.cpp +++ b/indra/llui/tests/lltextparser_test.cpp @@ -129,6 +129,7 @@ namespace entry->mPattern = "carol"; CHECK(pieces("carol and alice") == std::string("[carol:carol]| and alice|")); entry = parser.getHighlightById(id); + REQUIRE(entry != nullptr); entry->mCaseSensitive = true; CHECK(pieces("Carol and carol") == std::string("Carol and |[carol:carol]||")); parser.removeHighlight(id); diff --git a/indra/llui/tests/lluicolortable_test.cpp b/indra/llui/tests/lluicolortable_test.cpp index 3eba0c6dd1..489b02a7a8 100644 --- a/indra/llui/tests/lluicolortable_test.cpp +++ b/indra/llui/tests/lluicolortable_test.cpp @@ -118,8 +118,10 @@ namespace CHECK(table.getColor("T1Nope", LLColor4::yellow).get() == LLColor4::yellow); CHECK_FALSE(table.colorExists("T1Nope")); CHECK(table.getLoadedSheet().files().size() == 2u); + const ALColorSheet::Declaration* base = table.getLoadedSheet().declarationOf("T1Base"); + REQUIRE(base != nullptr); UNSCOPED_INFO("T1Base was last declared in the second file"); - CHECK(table.getLoadedSheet().declarationOf("T1Base")->file == 1); + CHECK(base->file == 1); } // A handle taken before a reload reads the colour after it. A name the @@ -192,7 +194,9 @@ namespace table.setColor("ColorPaletteEntry99", LLColor4(1, 1, 1, 1)); CHECK(a.get() == LLColor4(0.5f, 0.25f, 0.f, 1.f)); CHECK_FALSE(table.isDefault("T4A")); - CHECK(table.getLoadedColors().find("T4A")->second.get() == LLColor4(1, 0, 0, 1)); + const auto loaded = table.getLoadedColors().find("T4A"); + REQUIRE(loaded != table.getLoadedColors().end()); + CHECK(loaded->second.get() == LLColor4(1, 0, 0, 1)); const std::string text = written(); CHECK_THAT(text, Catch::Matchers::ContainsSubstring("name=\"T4A\"")); @@ -214,7 +218,9 @@ namespace table.load(documents, round_trip, "user.xml"); UNSCOPED_INFO("the written document reproduces it"); CHECK(a.get() == LLColor4(0.5f, 0.25f, 0.f, 1.f)); - CHECK(table.getUserColors().find("T4A")->second.get() == LLColor4(0.5f, 0.25f, 0.f, 1.f)); + const auto user = table.getUserColors().find("T4A"); + REQUIRE(user != table.getUserColors().end()); + CHECK(user->second.get() == LLColor4(0.5f, 0.25f, 0.f, 1.f)); } // What the skins' files have to say reaches the loaded sheet, and a diff --git a/indra/llui/tests/llurlentry_test.cpp b/indra/llui/tests/llurlentry_test.cpp index f41ae39ca3..458773a8eb 100644 --- a/indra/llui/tests/llurlentry_test.cpp +++ b/indra/llui/tests/llurlentry_test.cpp @@ -467,8 +467,8 @@ namespace "secondlife:///app/teleport/Ahern/50/50/50"); require_url("Ahern (50,50,50) multicase", url, - "XXX secondlife:///app/teleport/Ahern/50/50/50/ XXX", - "secondlife:///app/teleport/Ahern/50/50/50/"); + "XXX SecondLife:///App/Teleport/Ahern/50/50/50/ XXX", + "SecondLife:///App/Teleport/Ahern/50/50/50/"); require_url("Ahern (50,50) [1]", url, "XXX secondlife:///app/teleport/Ahern/50/50/ XXX", @@ -638,8 +638,8 @@ namespace "secondlife:///app/region/Ahern/50/50/50"); require_url("Ahern (50,50,50) multicase", url, - "XXX secondlife:///app/region/Ahern/50/50/50/ XXX", - "secondlife:///app/region/Ahern/50/50/50/"); + "XXX SecondLife:///App/Region/Ahern/50/50/50/ XXX", + "SecondLife:///App/Region/Ahern/50/50/50/"); require_url("Ahern (50,50) [1]", url, "XXX secondlife:///app/region/Ahern/50/50/ XXX", From 5e910db7f73ad5de2e6738c0b876d0a87fcf250b Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 13 Oct 2026 23:22:06 -0400 Subject: [PATCH 8/8] A combo box's value chosen is no longer unset 386cb21909 let LLComboBox and LLSpinCtrl show a value nobody wrote: setUnset(true) marks it, and choosing one makes it the control's own. The spinner drops the mark when a number is stepped, dragged or typed. The combo box dropped only the look: an item chosen, or a value typed, left isUnset() true until its owner said otherwise, so the control said one thing and showed another. Nothing reads isUnset() outside the tests yet, so nothing showed it. The mark now goes where the look goes for an item chosen, in updateLabel(), which picking from the list, the arrow keys, selectFirstItem(), setCurrentByIndex() and setValue() all reach, and when a person commits an item or text of their own. alfieldeditors still marks a field nobody wrote after its setValue(), so such a field shows as it did. llcombobox's unset case checks both ways of choosing. Co-Authored-By: Claude Opus 5.5 --- indra/llui/llcombobox.cpp | 8 ++++++++ indra/llui/tests/llcombobox_test.cpp | 19 ++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/indra/llui/llcombobox.cpp b/indra/llui/llcombobox.cpp index c8320cc21f..29e93efa68 100644 --- a/indra/llui/llcombobox.cpp +++ b/indra/llui/llcombobox.cpp @@ -217,10 +217,14 @@ void LLComboBox::onCommit() mTextEntry->setValue(label); mTextEntry->setTentative(false); } + // Chosen or typed: the value is the control's own now, whoever had + // written it before. + mUnset = false; setControlValue(item->getValue()); } else if (mAllowTextEntry) { + mUnset = false; setControlValue(mTextEntry->getValue()); } else @@ -465,6 +469,10 @@ void LLComboBox::setUnset(bool unset) void LLComboBox::updateLabel() { + // An item chosen is shown as settled, and is: whoever had written the + // value before, it is the control's own now. + mUnset = false; + // Update the combo editor with the selected // item label. if (mTextEntry) diff --git a/indra/llui/tests/llcombobox_test.cpp b/indra/llui/tests/llcombobox_test.cpp index 865420379f..f4bd82bad4 100644 --- a/indra/llui/tests/llcombobox_test.cpp +++ b/indra/llui/tests/llcombobox_test.cpp @@ -198,11 +198,24 @@ namespace CHECK_FALSE(text->getTentative()); // A choice made in the control settles it too, without its owner - // saying so. + // saying so: an item chosen, combo->setUnset(true); combo->selectFirstItem(); - UNSCOPED_INFO("and choosing it settles it"); - CHECK_FALSE(text->getTentative()); + { + INFO("and choosing it settles it"); + CHECK_FALSE(text->getTentative()); + CHECK_FALSE(combo->isUnset()); + } + + // or a value typed that the list does not have. + combo->setUnset(true); + text->setText(std::string("up")); + text->onCommit(); + { + INFO("and typing it settles it"); + CHECK(combo->getValue().asString() == std::string("up")); + CHECK_FALSE(combo->isUnset()); + } combo->die(); } }