Something went wrong. Try again.
The agentic engineering control plane for the posthuman future
Something went wrong. Try again.
9.2 kB · 242 lines
C++
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243#include <QtTest>
#include <QDir>#include <QFile>#include <QPoint>#include <QStyleHints>#include <QTemporaryDir>
#include "EditorController.h"
namespace {
bool writeLines(const QString& path, const QStringList& lines){ QDir().mkpath(QFileInfo(path).absolutePath()); QFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return false; } file.write(lines.join(QLatin1Char('\n')).toUtf8()); file.write("\n"); file.close(); return true;}
// Long enough that the viewport has room to move.QStringList tallFile(){ QStringList lines; for (int i = 0; i < 400; ++i) { lines.append(QStringLiteral("line %1").arg(i)); } return lines;}
} // namespace
// The wheel path of the editor pane. The physics live in the shared// xeno::scroll header, and the terminal pane drives the same numbers, so these// cases pin the part that is the editor's own: which input class applies its// delta when.class EditorControllerTest : public QObject { Q_OBJECT
// The fixture the cases share. Both members and this helper are declared // before the slots section on purpose: the class's default access is private, // so writing `private:` here is the redundant specifier clang-tidy rejects, // and anything inside `private slots:` is a slot as far as moc is concerned. [[nodiscard]] QString filePath() const { return m_dir.filePath(QStringLiteral("wheel.txt")); }
QTemporaryDir m_dir; EditorController m_controller;
private slots: // NOLINTBEGIN(readability-convert-member-functions-to-static) void initTestCase() { QVERIFY(m_dir.isValid()); // Keep the developer's real frecency table out of reach: openFile records // every file it loads. qputenv("XENO_FRECENCY_FILE", m_dir.filePath(QStringLiteral("frecency.json")).toUtf8()); }
void init() { QVERIFY(m_dir.isValid()); QVERIFY(writeLines(filePath(), tallFile())); QVERIFY(m_controller.model()->openFile(filePath())); // The viewport is what the wheel moves, and it is only sized from a report. m_controller.model()->setViewport(0.0, 600.0); QCOMPARE(m_controller.model()->viewportTopPx(), 0.0); }
void cleanup() { QFile::remove(filePath()); }
// A regression case. The trackpad path used to add momentum impulse WITHOUT // applying the event's own delta, so the viewport moved only by the glide: // 35% of the accumulated delta per tick, decaying, and each tick's sub-line // remainder was dropped when the glide stopped. A 60 px drag therefore moved // the viewport 0 px while the finger moved and then animated past it. void aTrackpadDeltaLandsBeforeAnyGlide() { const qreal lineHeight = m_controller.model()->lineHeightPx(); QVERIFY(lineHeight > 1.0);
const qreal before = m_controller.model()->viewportTopPx(); m_controller.wheelScroll(QPoint(0, 0), QPoint(0, -60), Qt::NoModifier); // Pixel deltas are true pixel scrolling: 60 px of finger travel is 60 px of // viewport travel, in the same turn, with no tick in between. QVERIFY(std::abs(m_controller.model()->viewportTopPx() - (before + 60.0)) < 0.5); }
void aSubLineDeltaLandsAsWell() { const qreal before = m_controller.model()->viewportTopPx(); m_controller.wheelScroll(QPoint(0, 0), QPoint(0, -4), Qt::NoModifier); // Four pixels is under half a line for the default font, and it must still // move the viewport: the dropped remainder was what made a slow drag look // frozen. const qreal moved = m_controller.model()->viewportTopPx() - before; QVERIFY(moved > 3.0); QVERIFY(moved < 5.0); }
void aNotchStepsTheSystemLineCountAndKillsTheGlide() { const qreal lineHeight = m_controller.model()->lineHeightPx(); // One notch is 120 eighths of a degree, scaled by the system's lines per // notch. Read the hint rather than assuming three. const int perNotch = std::max(1, QGuiApplication::styleHints()->wheelScrollLines());
const qreal before = m_controller.model()->viewportTopPx(); m_controller.wheelScroll(QPoint(0, -120), QPoint(0, 0), Qt::NoModifier); const qreal stepped = m_controller.model()->viewportTopPx(); QVERIFY(std::abs(stepped - (before + (lineHeight * perNotch))) < 0.5);
// A notch cancels a glide in flight: the hand is back on the wheel, so the // viewport must not coast on after it. m_controller.wheelScroll(QPoint(0, 0), QPoint(0, -30), Qt::NoModifier); m_controller.wheelScroll(QPoint(0, -120), QPoint(0, 0), Qt::NoModifier); const qreal afterNotch = m_controller.model()->viewportTopPx(); QTest::qWait(120); QCOMPARE(m_controller.model()->viewportTopPx(), afterNotch); }
void controlFlipsTheDirection() { // Move down first: the viewport starts at the top, and a flip upward from // there would only clamp. m_controller.wheelScroll(QPoint(0, 0), QPoint(0, -200), Qt::NoModifier); const qreal before = m_controller.model()->viewportTopPx(); QVERIFY(before > 100.0); m_controller.wheelScroll(QPoint(0, 0), QPoint(0, -30), Qt::ControlModifier); // Ctrl scrolls the other way, shared with the terminal. QVERIFY(m_controller.model()->viewportTopPx() < before); }
void aHorizontalOnlyEventIsDropped() { const qreal before = m_controller.model()->viewportTopPx(); m_controller.wheelScroll(QPoint(120, 0), QPoint(0, 0), Qt::NoModifier); QCOMPARE(m_controller.model()->viewportTopPx(), before); }
// The seed is the first hit at or after the caret, not the file's first hit. void startSearchPicksTheFirstMatchAtOrAfterTheCaret() { m_controller.model()->requestCursor(5, 0); m_controller.startSearch(QStringLiteral("line")); QCOMPARE(m_controller.searchQuery(), QStringLiteral("line")); QCOMPARE(m_controller.matchCount(), 400); QCOMPARE(m_controller.currentMatch(), 5); // The match is selected: its start is the anchor and its end the caret. QCOMPARE(m_controller.model()->anchorLine(), 5); QCOMPARE(m_controller.model()->anchorColumn(), 0); QCOMPARE(m_controller.model()->caretLine(), 5); QCOMPARE(m_controller.model()->caretColumn(), 4); }
void searchNextAndPreviousStepAndWrap() { // openFile() does not reset the caret, and the fixture is reused across // cases, so seed it at the top for a known first match. m_controller.model()->requestCursor(0, 0); m_controller.startSearch(QStringLiteral("line")); QCOMPARE(m_controller.currentMatch(), 0);
m_controller.searchNext(); QCOMPARE(m_controller.currentMatch(), 1); QCOMPARE(m_controller.model()->caretLine(), 1);
m_controller.searchPrevious(); QCOMPARE(m_controller.currentMatch(), 0); // Stepping back past the first match wraps to the last. m_controller.searchPrevious(); QCOMPARE(m_controller.currentMatch(), m_controller.matchCount() - 1); QCOMPARE(m_controller.model()->caretLine(), 399);
// And forward past the last wraps to the first. m_controller.searchNext(); QCOMPARE(m_controller.currentMatch(), 0); }
void clearSearchEmptiesTheState() { m_controller.model()->requestCursor(0, 0); m_controller.startSearch(QStringLiteral("line")); QVERIFY(m_controller.matchCount() > 0); m_controller.clearSearch(); QCOMPARE(m_controller.searchQuery(), QString()); QCOMPARE(m_controller.matchCount(), 0); QCOMPARE(m_controller.currentMatch(), -1); }
// A store edit rebuilds the match set for a live query, but the rebuild must // not move the caret: only the edit does that. void aStoreEditRebuildsTheMatchSetWithoutMovingTheCaret() { m_controller.model()->requestCursor(0, 0); m_controller.startSearch(QStringLiteral("line")); QCOMPARE(m_controller.matchCount(), 400); QCOMPARE(m_controller.currentMatch(), 0);
// Replace a whole line, so the store text changes and storeChanged fires. m_controller.model()->requestSelection(10, 10); m_controller.model()->replaceSelection(QStringLiteral("X"));
// The count follows the new text, and the caret is where the edit left it // (just past the inserted glyph), not re-seeded onto a match. QCOMPARE(m_controller.matchCount(), 399); QCOMPARE(m_controller.currentMatch(), 0); QCOMPARE(m_controller.model()->caretLine(), 10); QCOMPARE(m_controller.model()->caretColumn(), 1); }
// Opening or closing the bar is not a clear: the live search survives. void setSearchBarOpenDoesNotClearTheSearch() { m_controller.model()->requestCursor(0, 0); m_controller.startSearch(QStringLiteral("line")); QCOMPARE(m_controller.matchCount(), 400); QVERIFY(!m_controller.searchBarOpen());
m_controller.setSearchBarOpen(true); QVERIFY(m_controller.searchBarOpen()); QCOMPARE(m_controller.searchQuery(), QStringLiteral("line")); QCOMPARE(m_controller.matchCount(), 400);
m_controller.setSearchBarOpen(false); QVERIFY(!m_controller.searchBarOpen()); QCOMPARE(m_controller.matchCount(), 400); } // NOLINTEND(readability-convert-member-functions-to-static)};
QTEST_MAIN(EditorControllerTest)
#include "tst_editorcontroller.moc"