diff --git a/plugins/itempinned/tests/itempinnedtests.cpp b/plugins/itempinned/tests/itempinnedtests.cpp index 23978df41f..f73df88617 100644 --- a/plugins/itempinned/tests/itempinnedtests.cpp +++ b/plugins/itempinned/tests/itempinnedtests.cpp @@ -4,6 +4,8 @@ #include "tests/test_utils.h" +#include "common/commandstatus.h" + ItemPinnedTests::ItemPinnedTests(const TestInterfacePtr &test, QObject *parent) : QObject(parent) , m_test(test) diff --git a/plugins/itemsync/tests/itemsynctests.cpp b/plugins/itemsync/tests/itemsynctests.cpp index 409468d82d..5c8efc5d21 100644 --- a/plugins/itemsync/tests/itemsynctests.cpp +++ b/plugins/itemsync/tests/itemsynctests.cpp @@ -2,6 +2,7 @@ #include "itemsynctests.h" +#include "common/commandstatus.h" #include "common/mimetypes.h" #include "common/sleeptimer.h" #include "tests/test_utils.h" @@ -17,7 +18,6 @@ using FilePtr = std::shared_ptr; const char sep[] = " ;; "; -const auto clipboardBrowserId = "focus:ClipboardBrowser"; const auto confirmRemoveDialogId = "focus::QPushButton in :QMessageBox"; class TestDir final { diff --git a/src/tests/test_utils.h b/src/tests/test_utils.h index 858ceda95e..2d21be8e78 100644 --- a/src/tests/test_utils.h +++ b/src/tests/test_utils.h @@ -1,17 +1,46 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#ifndef TEST_UTILS_H -#define TEST_UTILS_H - -#include "common/commandstatus.h" +#pragma once #include #include #include #include #include +#include #include +constexpr int maxReadLogSize = 1 * 1024 * 1024; + +constexpr auto clipboardTabName = "CLIPBOARD"; +constexpr auto defaultSessionColor = "#ff8800"; +constexpr auto defaultTagColor = "#000000"; + +constexpr auto clipboardBrowserId = "focus:ClipboardBrowser"; +constexpr auto clipboardBrowserRefreshButtonId = "focus:ClipboardBrowserRefreshButton"; +constexpr auto filterEditId = "focus:Utils::FilterLineEdit"; +constexpr auto trayMenuId = "focus:TrayMenu"; +constexpr auto menuId = "focus:Menu"; +constexpr auto customMenuId = "focus:CustomMenu"; +constexpr auto editorId = "focus::ItemEditorWidget"; +constexpr auto tabDialogLineEditId = "focus:lineEditTabName"; +constexpr auto commandDialogId = "focus:CommandDialog"; +constexpr auto commandDialogSaveButtonId = "focus::QPushButton in :QMessageBox"; +constexpr auto commandDialogListId = "focus:listWidgetItems"; +constexpr auto configurationDialogId = "focus:ConfigurationManager"; +constexpr auto shortcutButtonId = "focus::QToolButton in CommandDialog"; +constexpr auto shortcutDialogId = "focus::QKeySequenceEdit in ShortcutDialog"; +constexpr auto actionDialogId = "focus:ActionDialog"; +constexpr auto aboutDialogId = "focus:AboutDialog"; +constexpr auto logDialogId = "focus:LogDialog"; +constexpr auto actionHandlerDialogId = "focus:ActionHandlerDialog"; +constexpr auto actionHandlerFilterId = "focus:filterLineEdit"; +constexpr auto actionHandlerTableId = "focus:tableView"; +constexpr auto clipboardDialogId = "focus:ClipboardDialog"; +constexpr auto clipboardDialogFormatListId = "focus:listWidgetFormats"; +constexpr auto confirmExitDialogId = "focus::QPushButton in :QMessageBox"; +constexpr auto itemPreviewId = "focus:in dockWidgetItemPreviewContents"; + #define NO_ERRORS(ERRORS_OR_EMPTY) !m_test->writeOutErrors(ERRORS_OR_EMPTY) /** @@ -56,6 +85,8 @@ if ( qgetenv(ENV) == "1" ) \ SKIP("Unset " ENV " to run the tests") +#define WITH_TIMEOUT "afterMilliseconds(10000, fail); " + /// Interval to wait (in ms) before and after setting clipboard. #ifdef Q_OS_MAC // macOS seems to require larger delay before/after setting clipboard @@ -98,4 +129,13 @@ inline QString keyNameFor(QKeySequence::StandardKey standardKey) return QKeySequence(standardKey).toString(); } -#endif // TEST_UTILS_H +template +void runMultiple(Fn1 f1, Fn2 f2) +{ + QTimer timer; + timer.setSingleShot(true); + timer.setInterval(0); + QObject::connect(&timer, &QTimer::timeout, f2); + timer.start(); + f1(); +} diff --git a/src/tests/tests.cpp b/src/tests/tests.cpp index 3c37d62a01..37f9fe7bbc 100644 --- a/src/tests/tests.cpp +++ b/src/tests/tests.cpp @@ -2,11 +2,20 @@ #include "tests.h" #include "test_utils.h" +#include "tests_classes.h" +#include "tests_cli.h" +#include "tests_commands.h" +#include "tests_dialogs.h" +#include "tests_items.h" +#include "tests_script_commands.h" +#include "tests_scripts.h" +#include "tests_tray.h" #include "common/action.h" #include "common/appconfig.h" #include "common/client_server.h" #include "common/common.h" +#include "common/commandstatus.h" #include "common/config.h" #include "common/log.h" #include "common/mimetypes.h" @@ -14,7 +23,6 @@ #include "common/shortcuts.h" #include "common/sleeptimer.h" #include "common/textdata.h" -#include "common/version.h" #include "item/itemfactory.h" #include "item/itemwidget.h" #include "item/serialize.h" @@ -38,52 +46,12 @@ #include #include -#include #include -#define WITH_TIMEOUT "afterMilliseconds(10000, fail); " - #define KEEP_STDIN_OPEN "KEEP_STDIN_OPEN" -// WORKAROUND: Checking clipboard right after closing menu gets stuck on OS X. -#define ACTIVATE_MENU_ITEM(MENU_ID, WIDGET_ID, CONTENT) \ - RUN("keys" << MENU_ID << "ENTER", ""); \ - RUN("keys" << WIDGET_ID, ""); \ - WAIT_FOR_CLIPBOARD(CONTENT) - namespace { -const int maxReadLogSize = 1 * 1024 * 1024; - -const auto clipboardTabName = "CLIPBOARD"; -const auto defaultSessionColor = "#ff8800"; -const auto defaultTagColor = "#000000"; - -const auto clipboardBrowserId = "focus:ClipboardBrowser"; -const auto clipboardBrowserRefreshButtonId = "focus:ClipboardBrowserRefreshButton"; -const auto filterEditId = "focus:Utils::FilterLineEdit"; -const auto trayMenuId = "focus:TrayMenu"; -const auto menuId = "focus:Menu"; -const auto customMenuId = "focus:CustomMenu"; -const auto editorId = "focus::ItemEditorWidget"; -const auto tabDialogLineEditId = "focus:lineEditTabName"; -const auto commandDialogId = "focus:CommandDialog"; -const auto commandDialogSaveButtonId = "focus::QPushButton in :QMessageBox"; -const auto commandDialogListId = "focus:listWidgetItems"; -const auto configurationDialogId = "focus:ConfigurationManager"; -const auto shortcutButtonId = "focus::QToolButton in CommandDialog"; -const auto shortcutDialogId = "focus::QKeySequenceEdit in ShortcutDialog"; -const auto actionDialogId = "focus:ActionDialog"; -const auto aboutDialogId = "focus:AboutDialog"; -const auto logDialogId = "focus:LogDialog"; -const auto actionHandlerDialogId = "focus:ActionHandlerDialog"; -const auto actionHandlerFilterId = "focus:filterLineEdit"; -const auto actionHandlerTableId = "focus:tableView"; -const auto clipboardDialogId = "focus:ClipboardDialog"; -const auto clipboardDialogFormatListId = "focus:listWidgetFormats"; -const auto confirmExitDialogId = "focus::QPushButton in :QMessageBox"; -const auto itemPreviewId = "focus:in dockWidgetItemPreviewContents"; - class PerformanceTimer final { public: PerformanceTimer() { @@ -101,143 +69,6 @@ class PerformanceTimer final { QElapsedTimer m_timer; }; -// Similar to QTemporaryFile but allows removing from other process. -class TemporaryFile { -public: - TemporaryFile() - { - QTemporaryFile tmp; - tmp.setAutoRemove(false); - QVERIFY(tmp.open()); - m_fileName = tmp.fileName(); - tmp.close(); - } - - ~TemporaryFile() - { - QFile::remove(m_fileName); - } - - QString fileName() const - { - return m_fileName; - } - -private: - QString m_fileName; -}; - -template -void runMultiple(Fn1 f1, Fn2 f2) -{ - QTimer timer; - timer.setSingleShot(true); - timer.setInterval(0); - QObject::connect(&timer, &QTimer::timeout, f2); - timer.start(); - f1(); -} - -bool testStderr(const QByteArray &stderrData, TestInterface::ReadStderrFlag flag = TestInterface::ReadErrors) -{ - static const QRegularExpression reFailure("(Warning:|ERROR:|ASSERT|ScriptError:).*", QRegularExpression::CaseInsensitiveOption); - const QLatin1String scriptError("ScriptError:"); - - const auto plain = [](const char *str){ - return QRegularExpression(QRegularExpression::escape(QLatin1String(str))); - }; - const auto regex = [](const char *str){ - return QRegularExpression(QLatin1String(str)); - }; - // Ignore exceptions and errors from clients in application log - // (these are expected in some tests). - static const std::array ignoreList{ - regex(R"(CopyQ Note \[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\] ignoreLog; - - const QString output = QString::fromUtf8(stderrData); - QRegularExpressionMatchIterator it = reFailure.globalMatch(output); - while ( it.hasNext() ) { - const auto match = it.next(); - - const QString log = match.captured(); - - if ( flag == TestInterface::ReadErrorsWithoutScriptException - && log.contains(scriptError) ) - { - return false; - } - - if ( ignoreLog.contains(log) ) - return ignoreLog[log]; - - qDebug() << "Failure in logs:" << log; - - const bool ignore = std::any_of( - std::begin(ignoreList), std::end(ignoreList), - [&output](const QRegularExpression &reIgnore){ - return output.contains(reIgnore); - }); - - ignoreLog[log] = ignore; - if (!ignore) - return false; - } - - return true; -} - bool waitWhileFileExists(const QFile &file) { SleepTimer t(2000); @@ -245,15 +76,6 @@ bool waitWhileFileExists(const QFile &file) return !file.exists(); } -/// Generate unique data. -QByteArray generateData() -{ - static int i = 0; - const QByteArray id = "tests_" - + QByteArray::number(QDateTime::currentMSecsSinceEpoch() % 1000); - return id + '_' + QByteArray::number(++i); -} - QByteArray decorateOutput(const QByteArray &label, const QByteArray &stderrOutput) { QByteArray output = "\n" + stderrOutput; @@ -792,32 +614,6 @@ class TestInterfaceImpl final : public TestInterface { PlatformClipboardPtr m_clipboard; }; -int count(const QStringList &items, const QString &pattern) -{ - int from = -1; - int count = 0; - const QRegularExpression re(pattern); - while ( (from = items.indexOf(re, from + 1)) != -1 ) - ++count; - return count; -} - -QStringList splitLines(const QByteArray &nativeText) -{ - return QString::fromUtf8(nativeText).split(QRegularExpression("\r\n|\n|\r")); -} - -QString appWindowTitle(const QString &text) -{ -#ifdef Q_OS_MAC - return QStringLiteral("CopyQ - %1\n").arg(text); -#elif defined(Q_OS_WIN) - return QStringLiteral("%1 - CopyQ-TEST\n").arg(text); -#else - return QStringLiteral("%1 — CopyQ-TEST\n").arg(text); -#endif -} - QVariantMap secretData(const QByteArray &text) { #ifdef Q_OS_WIN @@ -874,3596 +670,630 @@ class SlowMimeData final : public QMimeData { } // namespace -Tests::Tests(const TestInterfacePtr &test, QObject *parent) - : QObject(parent) - , m_test(test) +void Tests::pipingCommands() { -} + const auto tab = testTab(1); + const Args args = Args("tab") << tab << "separator" << ","; -void Tests::initTestCase() -{ - TEST(m_test->initTestCase()); -} + RUN(args << "action" + << "copyq print HELLO | copyq print(str(input()).toLowerCase())", ""); + WAIT_ON_OUTPUT(args << "read" << "0" << "1", "hello,"); -void Tests::cleanupTestCase() -{ - TEST(m_test->cleanupTestCase()); + RUN(args << "action" + << "copyq print TEST" + " | copyq 'print(str(input()) + 1)'" + " | copyq 'print(str(input()) + 2)'" + " | copyq 'print(str(input()) + 3)'" + , ""); + WAIT_ON_OUTPUT(args << "read" << "0" << "1", "TEST123,hello"); } -void Tests::init() +void Tests::copyPasteCommands() { - TEST(m_test->init()); -} + const QByteArray commands = + "[Commands]\n" + "1\\Name=Test 1\n" + "2\\Name=Test 2\n" + "size=2"; -void Tests::cleanup() -{ - TEST( m_test->cleanup() ); + RUN("keys" << clipboardBrowserId << "F6", ""); + TEST( m_test->setClipboard(commands) ); + RUN("keys" << commandDialogListId << keyNameFor(QKeySequence::Paste), ""); + + TEST( m_test->setClipboard(QByteArray()) ); + RUN("keys" << commandDialogListId << keyNameFor(QKeySequence::Copy), ""); + WAIT_FOR_CLIPBOARD(commands); + + RUN("keys" << commandDialogListId << "Enter" << clipboardBrowserId, ""); + RUN("commands().length", "2\n"); } -void Tests::readLog() +void Tests::toggleClipboardMonitoring() { - QByteArray stdoutActual; - QByteArray stderrActual; - QCOMPARE( run(Args("info") << "log", &stdoutActual, &stderrActual), 0 ); - QVERIFY2( testStderr(stderrActual), stderrActual ); - QCOMPARE( logFileName() + "\n", QString::fromUtf8(stdoutActual) ); - QTRY_VERIFY( !readLogFile(maxReadLogSize).isEmpty() ); + const QByteArray data1 = generateData(); + TEST( m_test->setClipboard(data1) ); + RUN("clipboard", data1); + WAIT_ON_OUTPUT("read" << "0", data1); -#define LOGGED_ONCE(PATTERN) \ - QTRY_COMPARE( count(splitLines(readLogFile(maxReadLogSize)), PATTERN), 1 ) + RUN("disable", ""); + RUN("monitoring", "false\n"); + WAIT_ON_OUTPUT("isClipboardMonitorRunning", "false\n"); - LOGGED_ONCE( - R"(^CopyQ DEBUG \[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\] : Loading configuration$)"); + const QByteArray data2 = generateData(); + TEST( m_test->setClipboard(data2) ); + RUN("clipboard", data2); + WAIT_ON_OUTPUT("read" << "0", data1); - LOGGED_ONCE( - R"(^CopyQ DEBUG \[.*\] : Starting monitor$)"); + RUN("enable", ""); + RUN("monitoring", "true\n"); + WAIT_ON_OUTPUT("isClipboardMonitorRunning", "true\n"); - LOGGED_ONCE( - R"(^.*: Clipboard formats to save: .*$)"); + const QByteArray data3 = generateData(); + TEST( m_test->setClipboard(data3) ); + RUN("clipboard", data3); + WAIT_ON_OUTPUT("read" << "0", data3); } -void Tests::commandHelp() +void Tests::clipboardToItem() { - QByteArray stdoutActual; - QByteArray stderrActual; - QCOMPARE( run(Args("help"), &stdoutActual, &stderrActual), 0 ); - QVERIFY2( testStderr(stderrActual), stderrActual ); - QVERIFY(!stdoutActual.isEmpty()); - - const QStringList commands = QStringList() - << "show" - << "hide" - << "toggle" - << "menu" - << "exit" - << "help" - << "version" - << "clipboard" - << "copy" - << "paste" - << "action" - << "add" - << "remove"; - - for (const auto &command : commands) { - QCOMPARE( run(Args("help") << command, &stdoutActual, &stderrActual), 0 ); - QVERIFY2( testStderr(stderrActual), stderrActual ); - QVERIFY( !stdoutActual.isEmpty() ); - const QString help = QString::fromUtf8(stdoutActual); - QVERIFY( help.contains(QRegularExpression("\\b" + QRegularExpression::escape(command) + "\\b")) ); - } + TEST( m_test->setClipboard("TEXT1") ); + RUN("clipboard", "TEXT1"); + WAIT_ON_OUTPUT("read" << "0", "TEXT1"); + RUN("read" << "?" << "0", "text/plain\n"); + + TEST( m_test->setClipboard("DATA1", "DATA") ); + WAIT_ON_OUTPUT("clipboard" << "DATA", "DATA1"); - // Print error on unknown function name. - RUN_EXPECT_ERROR("help" << "xxx", CommandException); + // Unicode test. + const auto test = QString::fromUtf8(QByteArray("Zkouška s různými českými znaky!")); + const auto bytes = test.toUtf8(); + TEST( m_test->setClipboard(bytes) ); + RUN("clipboard", bytes); + WAIT_ON_OUTPUT("read" << "0", bytes); } -void Tests::commandVersion() +void Tests::itemToClipboard() { - QByteArray stdoutActual; - QByteArray stderrActual; - QCOMPARE( run(Args("version"), &stdoutActual, &stderrActual), 0 ); - QVERIFY2( testStderr(stderrActual), stderrActual ); - QVERIFY( !stdoutActual.isEmpty() ); + RUN("add" << "TESTING2" << "TESTING1", ""); + RUN("read" << "0" << "1", "TESTING1\nTESTING2"); - const QString version = QString::fromUtf8(stdoutActual); - // Version contains application name and version. - QVERIFY( version.contains(QRegularExpression("\\bCopyQ\\b.*" + QRegularExpression::escape(versionString))) ); - // Version contains Qt version. - QVERIFY( version.contains(QRegularExpression("\\bQt:\\s+\\d")) ); -} + RUN("select" << "0", ""); -void Tests::badCommand() -{ - RUN_EXPECT_ERROR_WITH_STDERR("xxx", CommandException, "xxx"); - RUN_EXPECT_ERROR_WITH_STDERR("tab" << testTab(1) << "yyy", CommandException, "yyy"); + WAIT_FOR_CLIPBOARD("TESTING1"); + RUN("clipboard", "TESTING1"); - // Bad command shouldn't create new tab. - QByteArray stdoutActual; - QByteArray stderrActual; - QCOMPARE( run(Args("tab"), &stdoutActual, &stderrActual), 0 ); - QVERIFY2( testStderr(stderrActual), stderrActual ); - QVERIFY( !QString::fromUtf8(stdoutActual) - .contains(QRegularExpression("^" + QRegularExpression::escape(testTab(1)) + "$")) ); -} + // select second item and move to top + RUN("config" << "move" << "true", "true\n"); + RUN("select" << "1", ""); + RUN("read" << "0" << "1", "TESTING2\nTESTING1"); -void Tests::badSessionName() -{ - RUN_EXPECT_ERROR("-s" << "max_16_characters_in_session_name_allowed" << "", CommandBadSyntax); - RUN_EXPECT_ERROR("-s" << "spaces disallowed" << "", CommandBadSyntax); -} + WAIT_FOR_CLIPBOARD("TESTING2"); + RUN("clipboard", "TESTING2"); -void Tests::commandCatchExceptions() -{ -#if QT_VERSION < QT_VERSION_CHECK(5,12,0) - SKIP("Internal exceptions are not handled well before Qt 5.12."); -#endif + // select without moving + RUN("config" << "move" << "0", "false\n"); + RUN("select" << "1", ""); + RUN("read" << "0" << "1", "TESTING2\nTESTING1"); - RUN("try { removeTab('MISSING') } catch(e) { print(e) }", - "Error: Tab with given name doesn't exist!"); + WAIT_FOR_CLIPBOARD("TESTING1"); + RUN("clipboard", "TESTING1"); } -void Tests::commandExit() +void Tests::tabAdd() { - RUN("exit", "Terminating server.\n"); + const QString tab = testTab(1); + const Args args = Args("tab") << tab << "separator" << " "; - TEST( m_test->waitForServerToStop() ); + QVERIFY( !hasTab(tab) ); + RUN(args, ""); + RUN(args << "size", "0\n"); + RUN(args << "add" << "ghi" << "def" << "abc", ""); + QVERIFY( hasTab(tab) ); - QCOMPARE( run(Args("exit")), 1 ); -} + // Restart server. + TEST( m_test->stopServer() ); + TEST( m_test->startServer() ); -void Tests::commandEval() -{ - RUN("eval" << "", ""); - RUN("eval" << "1", "1\n"); - RUN("eval" << "[1,2,3]", "1\n2\n3\n"); - RUN("eval" << "'123'", "123\n"); - RUN("eval" << "'123'", "123\n"); - - RUN("-e" << "", ""); - RUN("-e" << "1", "1\n"); + QVERIFY( hasTab(tab) ); + + RUN(args << "size", "3\n"); + RUN(args << "read" << "0" << "1" << "2", "abc def ghi"); } -void Tests::commandEvalThrows() +void Tests::tabRemove() { - RUN_EXPECT_ERROR_WITH_STDERR( - "throw Error('Some exception')", CommandException, - "ScriptError: Some exception\n" - "\n" - "--- backtrace ---\n" - ); + const QString tab = testTab(1); + const Args args = Args("tab") << tab << "separator" << " "; - RUN_EXPECT_ERROR_WITH_STDERR( - "throw 'Some exception'", CommandException, - "ScriptError: Some exception\n" - ); + RUN(args << "add" << "", ""); + QVERIFY( hasTab(tab) ); + RUN(Args() << "removetab" << tab, ""); + QVERIFY( !hasTab(tab) ); - RUN_EXPECT_ERROR("eval('throw Error(1)')", CommandException); - RUN_EXPECT_ERROR("eval('throw 1')", CommandException); - RUN_EXPECT_ERROR("eval" << "throw Error(1)", CommandException); - RUN_EXPECT_ERROR("eval" << "throw 1", CommandException); + RUN_EXPECT_ERROR("removetab" << tab, CommandException); } -void Tests::commandEvalSyntaxError() +void Tests::tabIcon() { - RUN_EXPECT_ERROR_WITH_STDERR("eval" << "(", CommandException, "SyntaxError"); -} + const QString tab = testTab(1); + const QString icon = ":/images/icon"; -void Tests::commandEvalArguments() -{ - RUN("eval" << "str(arguments[1]) + ', ' + str(arguments[2])" << "Test 1" << "Test 2", - "Test 1, Test 2\n"); + RUN("tab" << tab << "add" << "", ""); + RUN("tabIcon" << tab, "\n"); + RUN("tabicon" << tab << icon, ""); + RUN("tabIcon" << tab, icon + "\n"); + RUN("tabIcon" << tab << "", ""); + RUN("tabicon" << tab, "\n"); } -void Tests::commandEvalEndingWithComment() +void Tests::action() { - /* - With Qml scripts in Qt 5, it's not possible to get uncaught exceptions - from `QJSEngine::evaluate()`. - - Workaround is to wrap the script properly in an try/catch block: + const Args args = Args("tab") << testTab(1); + const Args argsAction = Args(args) << "action"; + const QString action = QString::fromLatin1("copyq %1 %2").arg(args.join(" ")); - try { - %1 - } catch(e) { - _store_exception_internally(e); - throw e; - } + // action with size + RUN(argsAction << action.arg("size") << "", ""); + WAIT_ON_OUTPUT(args << "size", "1\n"); + RUN(args << "read" << "0", "0\n"); - (Unfortunately, it's still possible to escape the function with a script injection.) - */ - RUN("eval" << "1 // TEST", "1\n"); -} + // action with size + RUN(argsAction << action.arg("size") << "", ""); + WAIT_ON_OUTPUT(args << "size", "2\n"); + RUN(args << "read" << "0", "1\n"); -void Tests::commandPrint() -{ - RUN("print" << "1", "1"); - RUN("print" << "TEST", "TEST"); -} + // action with eval print + RUN(argsAction << action.arg("eval 'print(\"A,B,C\")'") << "", ""); + WAIT_ON_OUTPUT(args << "size", "3\n"); + RUN(args << "read" << "0", "A,B,C"); -void Tests::commandAbort() -{ - RUN("eval" << "abort(); 1", ""); - RUN("eval" << "eval('abort(); print(1)'); 2", ""); - RUN("eval" << "execute('copyq', 'eval', '--', 'abort(); print(1)'); 2", "2\n"); + // action with read and comma separator for new items + RUN(argsAction << action.arg("read 0") << ",", ""); + WAIT_ON_OUTPUT(args << "size", "6\n"); + RUN(args << "read" << "0" << "1" << "2", "C\nB\nA"); } -void Tests::commandFail() +void Tests::renameTab() { - QByteArray stdoutActual; - QByteArray stderrActual; - QCOMPARE( run(Args("fail"), &stdoutActual, &stderrActual), 1 ); - QVERIFY2( testStderr(stderrActual), stderrActual ); - QCOMPARE( stdoutActual, QByteArray() ); -} + const QString tab1 = testTab(1); + const QString tab2 = testTab(2); -void Tests::commandSource() -{ - const auto script = - R"( - test = function() { return " TEST" } - print("SOURCED") - )"; - - QTemporaryFile scriptFile; - QVERIFY(scriptFile.open()); - scriptFile.write(script); - scriptFile.close(); - const auto scriptFileName = scriptFile.fileName(); - - RUN("source" << scriptFileName, "SOURCED"); - RUN("source" << scriptFileName << "test()", "SOURCED TEST\n"); -} + RUN("tab" << tab1 << "add" << "ghi" << "def" << "abc", ""); -void Tests::commandVisible() -{ - RUN("visible", "true\n"); -} + RUN("renametab" << tab1 << tab2, ""); + RUN("tab" << tab2 << "size", "3\n"); + RUN("tab" << tab2 << "read" << "0" << "1" << "2", "abc\ndef\nghi"); + QVERIFY( !hasTab(tab1) ); -void Tests::commandToggle() -{ - RUN("visible", "true\n"); - RUN("toggle", "false\n"); - WAIT_ON_OUTPUT("visible", "false\n"); - - RUN("toggle", "true\n"); - WAIT_ON_OUTPUT("visible", "true\n"); -} + // Rename non-existing tab. + RUN_EXPECT_ERROR("renametab" << tab1 << tab2, CommandException); -void Tests::commandHide() -{ - RUN("visible", "true\n"); - RUN("hide", ""); - WAIT_ON_OUTPUT("visible", "false\n"); -} + // Rename to same name. + RUN_EXPECT_ERROR("renametab" << tab2 << tab2, CommandException); -void Tests::commandShow() -{ - RUN("visible", "true\n"); - RUN("hide", ""); - WAIT_ON_OUTPUT("visible", "false\n"); + // Rename to empty name. + RUN_EXPECT_ERROR("renametab" << tab2 << "", CommandException); - RUN("show", ""); - WAIT_ON_OUTPUT("visible", "true\n"); -} + // Rename to existing tab. + RUN_EXPECT_ERROR("renametab" << tab2 << clipboardTabName, CommandException); -void Tests::commandShowAt() -{ - RUN("visible", "true\n"); - RUN("hide", ""); - WAIT_ON_OUTPUT("visible", "false\n"); + QVERIFY( !hasTab(tab1) ); + QVERIFY( hasTab(tab2) ); - RUN("showAt", ""); - WAIT_ON_OUTPUT("visible", "true\n"); -} + RUN("renametab" << tab2 << tab1, ""); + RUN("tab" << tab1 << "read" << "0" << "1" << "2", "abc\ndef\nghi"); -void Tests::commandFocused() -{ - RUN("focused", "true\n"); - RUN("hide", ""); - RUN("focused", "false\n"); + QVERIFY( hasTab(tab1) ); + QVERIFY( !hasTab(tab2) ); } -void Tests::commandsUnicode() +void Tests::renameClipboardTab() { - const auto text = QString::fromUtf8(QByteArray("Zkouška s různými českými znaky!")); - RUN_WITH_INPUT("eval" << "input()", text, text); - RUN_WITH_INPUT("eval" << "str(input())", text, text + "\n"); - - RUN_WITH_INPUT("eval" << "fromUnicode(str(input()), 'utf8')", text, text); - RUN_WITH_INPUT("eval" << "toUnicode(fromUnicode(str(input()), 'utf16'), 'utf16')", text, text + "\n"); - RUN_WITH_INPUT("eval" << "toUnicode(fromUnicode(str(input()), 'utf32le'), 'utf32le')", text, text + "\n"); - RUN_WITH_INPUT("eval" << "toUnicode( fromUnicode(str(input()), 'utf16le') )", text, text + "\n"); -} + const QString newClipboardTabName = clipboardTabName + QStringLiteral("2"); + RUN("config" << "tray_tab" << clipboardTabName, clipboardTabName + QStringLiteral("\n")); + const QString icon = ":/images/icon"; + RUN("tabicon" << clipboardTabName << icon, ""); -void Tests::commandsAddRead() -{ - RUN("add" << "A", ""); - RUN("read" << "0", "A"); - - RUN("add" << "B", ""); - RUN("read" << "0", "B"); - RUN("read" << "1", "A"); - - RUN("add" << "C" << "D", ""); - RUN("read" << "0", "D"); - RUN("read" << "1", "C"); - RUN("read" << "2", "B"); - RUN("read" << "3", "A"); -} + RUN("renametab" << clipboardTabName << newClipboardTabName, ""); + RUN("tab", newClipboardTabName + "\n"); + RUN("config" << "clipboard_tab", newClipboardTabName + QStringLiteral("\n")); + RUN("config" << "tray_tab", newClipboardTabName + QStringLiteral("\n")); + RUN("tabicon" << newClipboardTabName, icon + QStringLiteral("\n")); -void Tests::commandsWriteRead() -{ - const QByteArray input("\x00\x01\x02\x03\x04", 5); - const auto arg1 = QString::fromLatin1("\x01\x02\x03\x04"); - const auto arg2 = QString::fromLatin1("\x7f\x6f\x5f\x4f"); - TEST( m_test->runClient( - Args() << "write" - << COPYQ_MIME_PREFIX "test1" << arg1 - << COPYQ_MIME_PREFIX "test2" << "-" - << COPYQ_MIME_PREFIX "test3" << arg2, "", - input) ); - RUN("read" << COPYQ_MIME_PREFIX "test1" << "0", arg1.toLatin1()); - RUN("read" << COPYQ_MIME_PREFIX "test2" << "0", input); - RUN("read" << COPYQ_MIME_PREFIX "test3" << "0", arg2.toLatin1()); - - RUN("write(1, {'text/plain': 'A'}, {'text/plain': 'B'})", ""); - RUN("read(mimeText, 0, 1, 2, 3)", "\nB\nA\n"); - - RUN("write(0, [{'text/plain': 'C'}, {'text/plain': 'D'}])", ""); - RUN("read(mimeText, 0, 1, 2, 3)", "D\nC\n\nB"); - - RUN("write(0, ['E', 'F'])", ""); - RUN("read(mimeText, 0, 1, 2, 3)", "F\nE\nD\nC"); + TEST( m_test->setClipboard("test1") ); + WAIT_ON_OUTPUT("tab" << newClipboardTabName << "read" << "0", "test1"); + RUN("tab", newClipboardTabName + "\n"); - RUN_EXPECT_ERROR_WITH_STDERR( - "write(0, [{}], [{}])", - CommandException, "Unexpected multiple item list arguments"); - RUN_EXPECT_ERROR_WITH_STDERR( - "write(0)", - CommandException, "Expected item arguments"); - RUN_EXPECT_ERROR_WITH_STDERR( - "write(0, '1', '2', '3')", - CommandException, "Unexpected uneven number of mimeType/data arguments"); + WAIT_ON_OUTPUT("read" << "0", "test1"); + RUN("tab", newClipboardTabName + "\n"); } -void Tests::commandChange() +void Tests::importExportTab() { - RUN("add" << "C" << "B" << "A", ""); - RUN("change" << "1" << "text/plain" << "b", ""); - RUN("separator" << " " << "read" << "0" << "1" << "2", "A b C"); - - RUN("change" << "1" << "text/plain" << "B" << "text/html" << "B", ""); - RUN("read" << "text/html" << "1", "B"); - RUN("separator" << " " << "read" << "0" << "1" << "2", "A B C"); - - RUN("change(1, 'text/html', undefined)", ""); - RUN("read" << "?" << "1", "text/plain\n"); -} + const QString tab = testTab(1); + const Args args = Args("tab") << tab << "separator" << " "; -void Tests::commandSetCurrentTab() -{ - const auto tab = testTab(1); - RUN("setCurrentTab" << tab, ""); - RUN("testSelected", tab + "\n"); -} + RUN(args << "add" << "ghi" << "def" << "abc", ""); -void Tests::commandConfig() -{ - QByteArray stdoutActual; - QByteArray stderrActual; - QCOMPARE( run(Args("config"), &stdoutActual, &stderrActual), 0 ); - QVERIFY2( testStderr(stderrActual), stderrActual ); - QVERIFY( !stdoutActual.isEmpty() ); + TemporaryFile tmp; + RUN(args << "exporttab" << tmp.fileName(), ""); - // invalid option - RUN_EXPECT_ERROR_WITH_STDERR("config" << "xxx", CommandException, "xxx"); + RUN("removetab" << tab, ""); + QVERIFY( !hasTab(tab) ); - QCOMPARE( run(Args("config") << "tab_tree", &stdoutActual, &stderrActual), 0 ); - QVERIFY2( testStderr(stderrActual), stderrActual ); - QVERIFY2( stdoutActual == "true\n" || stdoutActual == "false\n", stdoutActual); + RUN(args << "importtab" << tmp.fileName(), ""); + RUN(args << "read" << "0" << "1" << "2", "abc def ghi"); + RUN(args << "size", "3\n"); - RUN("config" << "tab_tree" << "true", "true\n"); - RUN("config" << "tab_tree", "true\n"); + // Export with relative path. + TemporaryFile tmp2; - RUN("config" << "tab_tree" << "false", "false\n"); - RUN("config" << "tab_tree", "false\n"); + // Change back to original working directory once finished. + struct CurrentDirectoryGuard { + CurrentDirectoryGuard() : oldDir(QDir::currentPath()) {} + ~CurrentDirectoryGuard() { QDir::setCurrent(oldDir); } + const QString oldDir; + } currentDirectoryGuard; - RUN("config" << "tab_tree" << "1", "true\n"); - RUN("config" << "tab_tree", "true\n"); + QDir::setCurrent( QDir::cleanPath(tmp2.fileName() + "/..") ); - RUN("config" << "tab_tree" << "0", "false\n"); - RUN("config" << "tab_tree", "false\n"); + const QString fileName = QFileInfo( tmp2.fileName() ).fileName(); - // Set multiple options. - RUN("config" << "tab_tree" << "0" << "text_wrap" << "1", - "tab_tree=false\n" - "text_wrap=true\n"); + RUN(args << "add" << "012", ""); + RUN(args << "exporttab" << fileName, ""); - // Don't set any options if there is an invalid one. - RUN_EXPECT_ERROR_WITH_STDERR("config" << "tab_tree" << "1" << "xxx" << "0", CommandException, "xxx"); - RUN("config" << "tab_tree", "false\n"); -} + RUN("removetab" << tab, ""); + QVERIFY( !hasTab(tab) ); -void Tests::commandToggleConfig() -{ - RUN("toggleConfig" << "check_clipboard", "false\n"); - RUN("config" << "check_clipboard", "false\n"); - RUN("toggleConfig" << "check_clipboard", "true\n"); - RUN("config" << "check_clipboard", "true\n"); - - RUN_EXPECT_ERROR("toggleConfig", CommandException); - RUN_EXPECT_ERROR_WITH_STDERR("toggleConfig" << "xxx", CommandException, "xxx"); - RUN_EXPECT_ERROR_WITH_STDERR("toggleConfig" << "clipboard_tab", CommandException, "clipboard_tab"); + RUN(args << "importtab" << fileName, ""); + RUN(args << "read" << "0" << "1" << "2" << "3", "012 abc def ghi"); } -void Tests::commandDialog() +void Tests::removeAllFoundItems() { - RUN(Args() << "keys" << clipboardBrowserId, ""); - runMultiple( - [&]() { RUN(WITH_TIMEOUT "dialog('text')", "TEST\n"); }, - [&]() { RUN(Args() << "keys" << "focus::QLineEdit in :QDialog" << ":TEST" << "ENTER", ""); } - ); - - RUN(Args() << "keys" << clipboardBrowserId, ""); - runMultiple( - [&]() { RUN(WITH_TIMEOUT "dialog('text') === undefined", "true\n"); }, - [&]() { RUN(Args() << "keys" << "focus::QLineEdit in :QDialog" << "ESCAPE", ""); } - ); - - RUN(Args() << "keys" << clipboardBrowserId, ""); - runMultiple( - [&]() { RUN(WITH_TIMEOUT "dialog('.defaultChoice', 2, 'list', [1, 2, 3])", "2\n"); }, - [&]() { RUN(Args() << "keys" << "focus::QComboBox in :QDialog" << "ENTER", ""); } - ); - - RUN(Args() << "keys" << clipboardBrowserId, ""); - runMultiple( - [&]() { RUN(WITH_TIMEOUT "dialog('.defaultChoice', '', 'list', [1, 2, 3])", "\n"); }, - [&]() { RUN(Args() << "keys" << "focus::QComboBox in :QDialog" << "ENTER", ""); } - ); - - RUN(Args() << "keys" << clipboardBrowserId, ""); - runMultiple( - [&]() { RUN(WITH_TIMEOUT "dialog('list', [0, 1, 2])", "0\n"); }, - [&]() { RUN(Args() << "keys" << "focus::QComboBox in :QDialog" << "ENTER", ""); } - ); - - // Can't focus configuration checkboxes on OS X -#ifndef Q_OS_MAC - RUN(Args() << "keys" << clipboardBrowserId, ""); - runMultiple( - [&]() { RUN(WITH_TIMEOUT "dialog('boolean', true) === true", "true\n"); }, - [&]() { RUN(Args() << "keys" << "focus::QCheckBox in :QDialog" << "ENTER", ""); } - ); -#endif - - // Verify that special argument ".title" changes dialog's object name - // so that geometry can be stored. - RUN(Args() << "keys" << clipboardBrowserId, ""); - runMultiple( - [&]() { RUN(WITH_TIMEOUT "dialog('.title', 'test', 'text')", ""); }, - [&]() { RUN(Args() << "keys" << "focus::QLineEdit in dialog_test:QDialog" << "ESCAPE", ""); } - ); + auto args = Args("add"); + for (int i = 0; i < 50; ++i) { + args << QString::fromLatin1("a%1").arg(i); + args << QString::fromLatin1("b%1").arg(i); + } - RUN(Args() << "keys" << clipboardBrowserId, ""); - const QByteArray script = R"( - dialog( - '.width', 100, - '.height', 100, - '.x', 10, - '.y', 10, - '.style', 'background: red', - '.icon', '', - '.label', 'TEST', - 'text', 'DEFAULT', - ) - )"; - runMultiple( - [&]() { RUN(WITH_TIMEOUT + script, "DEFAULT\n"); }, - [&]() { RUN(Args() << "keys" << "focus::QLineEdit in :QDialog" << "ENTER", ""); } - ); + RUN(args, ""); + RUN("size", "100\n"); - RUN(Args() << "keys" << clipboardBrowserId, ""); - runMultiple( - [&]() { RUN(WITH_TIMEOUT "dialog('.title', 'Remove Items', '.label', 'Remove all items?') === true", "true\n"); }, - [&]() { RUN(Args() << "keys" << "focus::QPushButton in dialog_Remove Items:QDialog" << "ENTER", ""); } - ); -} + RUN("filter" << "a", ""); + RUN("keys" << "CTRL+A" << m_test->shortcutToRemove(), ""); -void Tests::commandDialogCloseOnDisconnect() -{ - RUN("afterMilliseconds(0, abort); dialog()", ""); + RUN("size", "50\n"); + RUN("read" << "49" << "48" << "47", "b0\nb1\nb2"); + RUN("read" << "0" << "1" << "2", "b49\nb48\nb47"); } -void Tests::commandMenuItems() +void Tests::nextPrevious() { - RUN(Args() << "keys" << clipboardBrowserId, ""); - runMultiple( - [&]() { RUN(WITH_TIMEOUT "menuItems('a', 'b', 'c')", "a\n"); }, - [&]() { RUN(Args() << "keys" << customMenuId << "ENTER", ""); } - ); + const QString tab = testTab(1); + const Args args = Args("tab") << tab; + RUN(args << "add" << "C" << "B" << "A", ""); + RUN("setCurrentTab" << tab, ""); - RUN(Args() << "keys" << clipboardBrowserId, ""); - runMultiple( - [&]() { RUN(WITH_TIMEOUT "menuItems([{'text/plain': 'a'}, {'text/plain': 'b'}])", "0\n"); }, - [&]() { RUN(Args() << "keys" << customMenuId << "ENTER", ""); } - ); + RUN(args << "next", ""); + WAIT_FOR_CLIPBOARD("B"); - RUN(Args() << "keys" << clipboardBrowserId, ""); - runMultiple( - [&]() { RUN(WITH_TIMEOUT "menuItems('a', 'b', 'c')", "\n"); }, - [&]() { RUN(Args() << "keys" << customMenuId << "ESCAPE", ""); } - ); + RUN(args << "next", ""); + WAIT_FOR_CLIPBOARD("C"); - RUN(Args() << "keys" << clipboardBrowserId, ""); - runMultiple( - [&]() { RUN(WITH_TIMEOUT "menuItems([{'text/plain': 'a'}, {'text/plain': 'b'}])", "-1\n"); }, - [&]() { RUN(Args() << "keys" << customMenuId << "ESCAPE", ""); } - ); + RUN(args << "next", ""); + WAIT_FOR_CLIPBOARD("C"); - RUN(Args() << "keys" << clipboardBrowserId, ""); - runMultiple( - [&]() { RUN(WITH_TIMEOUT "menuItems('a', 'b', 'c')", "b\n"); }, - [&]() { RUN(Args() << "keys" << customMenuId << ":b" << "ENTER", ""); } - ); + RUN(args << "previous", ""); + WAIT_FOR_CLIPBOARD("B"); - RUN(Args() << "keys" << clipboardBrowserId, ""); - runMultiple( - [&]() { RUN(WITH_TIMEOUT "menuItems([{'text/plain': 'a'}, {'text/plain': 'b'}])", "1\n"); }, - [&]() { RUN(Args() << "keys" << customMenuId << ":b" << "ENTER", ""); } - ); + RUN(args << "previous", ""); + WAIT_FOR_CLIPBOARD("A"); - RUN("afterMilliseconds(0, abort); menuItems('a', 'b', 'c')", ""); + RUN(args << "previous", ""); + WAIT_FOR_CLIPBOARD("A"); } -void Tests::commandsPackUnpack() +void Tests::externalEditor() { - QMap data; - data[mimeText] = "plain text"; - data[mimeHtml] = "HTML text"; - data[QLatin1String(COPYQ_MIME_PREFIX "test1")] = "test1 data"; - data[QLatin1String(COPYQ_MIME_PREFIX "test2")] = "test2 data"; - - auto args = Args() << "write"; - for (const auto &mime : data.keys()) - args << mime << data[mime]; - RUN(args, ""); - - const QByteArray script1 = - "var data = read('" + toByteArray(mimeItems) + "', 0); var item = unpack(data);"; - - // Unpack item read from list. - for (const auto &mime : data.keys()) { - RUN("eval" - << script1 + "var mime = '" + mime + "'; print(mime + ':' + str(item[mime]))", - mime + ':' + data[mime]); - } + const QString tab = testTab(1); + const Args args = Args("tab") << tab; + const QString editorTab = testTab(2); + const Args editorArgs = Args("tab") << editorTab; + const Args editorFileNameArgs = Args(editorArgs) << "read" << "0"; + const Args editorEndArgs = Args(editorArgs) << "remove" << "0"; - // Test pack and unpack consistency. - const QByteArray script2 = "data = pack(item); item = unpack(data);"; - for (const auto &mime : data.keys()) { - RUN("eval" - << script1 + script2 + "var mime = '" + mime + "'; print(mime + ':' + str(item[mime]))", - mime + ':' + data[mime]); - } -} + // Set editor command which add file name to edit to special editor tab. + // The command finishes when the special tab is emptied by this test. + // File to edit is removed by application when the command finished. + const auto cmd = QString( + R"(copyq tab "%1" eval "add(arguments[1]); while(length()) sleep(100);" --)" + ).arg(editorTab) + " %1"; + RUN("config" << "editor" << cmd, cmd + "\n"); -void Tests::commandsBase64() -{ - const QByteArray data = "0123456789\001\002\003\004\005\006\007abcdefghijklmnopqrstuvwxyz!"; - const QByteArray base64 = data.toBase64(); + // Set clipboard. + const QByteArray data1 = generateData(); + TEST( m_test->setClipboard(data1) ); + RUN("clipboard", data1); - TEST( m_test->runClient(Args() << "eval" << "print(input())", data, data) ); - TEST( m_test->runClient(Args() << "eval" << "print(tobase64(input()))", base64, data) ); +#define EDIT(DATA1, DATA2) \ + do { \ + WAIT_ON_OUTPUT(editorArgs << "size", "1\n"); \ + QByteArray out; \ + QByteArray err; \ + run(editorFileNameArgs, &out, &err); \ + QVERIFY2( testStderr(err), err ); \ + QFile file(out); \ + QVERIFY( file.exists() ); \ + QVERIFY( file.open(QIODevice::ReadWrite) ); \ + QVERIFY( file.readAll() == (DATA1) ); \ + file.write(DATA2); \ + file.close(); \ + RUN(editorEndArgs, ""); \ + waitWhileFileExists(file); \ + } while(false) - // Line break is added only if return value is string; - // tobase64() returns string, frombase64() returns byte array. - RUN("tobase64" << data, base64 + '\n'); - RUN("frombase64" << base64, data); + // Edit clipboard. + RUN("edit" << "-1", ""); + const QByteArray data2 = generateData(); + EDIT(data1, data2); - TEST( m_test->runClient(Args() << "eval" << "print(frombase64(tobase64(input())))", data, data) ); + // Check if clipboard changed. + WAIT_ON_OUTPUT("read" << "0", data1 + data2); + WAIT_FOR_CLIPBOARD(data1 + data2); - // Test Base64 encoding and decoding consistency. - TEST( m_test->runClient( - Args() << "eval" - << "var base64 = tobase64(input()); if (str(input()) === str(frombase64(base64))) print('OK')", - "OK", data) ); -} + // Edit existing item. + const QString text = + "Some text to edit,\n" + "with second line!\n" + + generateData(); + RUN(args << "add" << text, ""); -void Tests::commandsGetSetItem() -{ - QMap data; - data["text/plain"] = "plain text"; - data["text/html"] = "HTML text"; - data[COPYQ_MIME_PREFIX "test1"] = "test1 data"; - data[COPYQ_MIME_PREFIX "test2"] = "test2 data"; + // Modify first item. + RUN(args << "edit" << "0", ""); + const QByteArray data3 = generateData(); + EDIT(text.toUtf8(), data3); - const QString tab = testTab(1); - const Args args = Args("tab") << tab; + // Check first item. + WAIT_ON_OUTPUT(args << "read" << "0", text.toUtf8() + data3); - Args args2 = args; - args2 << "write"; - for (const auto &mime : data.keys()) - args2 << mime << data[mime]; - RUN(args2, ""); - - // Get item from list. - for (const auto &mime : data.keys()) { - RUN(args << "eval" - << "var mime = '" + mime + "'; print(mime + ':' + str(getitem(0)[mime]))", - mime + ':' + data[mime]); - } + // Edit new item. + RUN(args << "edit", ""); + const QByteArray data4 = generateData(); + EDIT(QByteArray(), data4); - // Set item. - RUN(args << "eval" - << "setitem(1, { 'text/plain': 'plain text 2', 'text/html': 'HTML text 2' })", - ""); + // Check first item. + WAIT_ON_OUTPUT(args << "read" << "0", data4); - RUN(args << "eval" << "print(getitem(1)['text/plain'])", "plain text 2"); - RUN(args << "eval" << "print(getitem(1)['text/html'])", "HTML text 2"); +#undef EDIT } -void Tests::commandsChecksums() +void Tests::nextPreviousTab() { - RUN("md5sum" << "TEST", "033bd94b1168d7e4f0d644c3c95e35bf\n"); - RUN("sha1sum" << "TEST", "984816fd329622876e14907634264e6f332e9fb3\n"); - RUN("sha256sum" << "TEST", "94ee059335e587e501cc4bf90613e0814f00a7b08bc7c648fd865a2af6a22cc2\n"); - RUN("sha512sum" << "TEST", "7bfa95a688924c47c7d22381f20cc926f524beacb13f84e203d4bd8cb6ba2fce81c57a5f059bf3d509926487bde925b3bcee0635e4f7baeba054e5dba696b2bf\n"); -} + const auto tab1 = testTab(1); + RUN("tab" << tab1 << "size", "0\n"); -void Tests::commandEscapeHTML() -{ - RUN("escapeHTML" << "&\n<\n>", "&
<
>\n"); -} + const auto tab2 = testTab(2); + RUN("tab" << tab2 << "size", "0\n"); -void Tests::commandExecute() -{ - const QByteArray script = - "function test(c, expected_stdout, expected_exit_code) {" - " if (str(c.stdout) !== expected_stdout) print('Unexpected stdout: \"' + str(c.stdout) + '\"');" - " if (c.exit_code !== expected_exit_code) print('Unexpected exit_code: ' + str(c.exit_code));" - "}"; - - RUN("eval" << script + - "c = execute('copyq', 'write', 'text/plain', 'plain text', 'text/html', 'test HTML');" - "test(c, '', 0);" - , ""); - - RUN("eval" << script + - "c = execute('copyq', 'read', 'text/plain', 0);" - "test(c, 'plain text', 0);" - , ""); - - RUN("eval" << script + - "c = execute('copyq', 'read', 'text/html', 0);" - "test(c, 'test HTML', 0);" - , ""); - - RUN("eval" << script + - "c = execute('copyq', 'read', 0, function(lines) { print(lines); });" - "test(c, 'plain text', 0);" - , "plain text"); -} - -void Tests::commandSettings() -{ - RUN("config" << "clipboard_tab" << "TEST", "TEST\n"); - - RUN("settings" << "test_variable", ""); - RUN("settings" << "test_variable" << "TEST VALUE", ""); - RUN("settings" << "test_variable", "TEST VALUE"); - RUN("settings" << "test_variable" << "TEST VALUE 2", ""); - RUN("settings" << "test_variable", "TEST VALUE 2"); - - RUN("config" << "clipboard_tab", "TEST\n"); -} - -void Tests::commandsEnvSetEnv() -{ - RUN("eval" << - "\n var name = 'COPYQ_ENV_TEST'" - "\n if (setEnv(name, 'OK'))" - "\n print(env(name))" - "\n else" - "\n print('FAILED')" - , "OK" - ); -} - -void Tests::commandSleep() -{ - QElapsedTimer t; - - t.start(); - RUN("sleep" << "100", ""); - const auto afterElapsed100Ms = t.elapsed(); - QVERIFY(afterElapsed100Ms > 100); - - t.start(); - RUN("sleep" << "1000", ""); - const auto afterElapsed1000Ms = t.elapsed(); - QVERIFY(afterElapsed1000Ms > 1000); -} - -void Tests::commandsData() -{ - RUN("eval" << "setData('x', 'X'); data('x')", "X"); - RUN("eval" << "setData('x', 'X'); setData('y', 'Y'); str(data('x')) + str(data('y'))", "XY\n"); - - RUN("dataFormats", ""); - RUN("eval" << "setData('x'); dataFormats()", "x\n"); - RUN("eval" << "setData('x'); setData('y'); dataFormats()", "x\ny\n"); - - RUN("eval" << "setData('x'); setData('y'); removeData('x'); dataFormats()", "y\n"); - RUN("eval" << "setData('x'); setData('y'); removeData('y'); dataFormats()", "x\n"); -} - -void Tests::commandCurrentWindowTitle() -{ - RUN("disable", ""); - WAIT_ON_OUTPUT("currentWindowTitle", appWindowTitle("*Clipboard Storing Disabled*")); - RUN("enable", ""); -} - -void Tests::commandCopy() -{ - RUN("copy" << "A", "true\n"); - WAIT_FOR_CLIPBOARD("A"); - - RUN("copy" << "DATA" << "B", "true\n"); - WAIT_FOR_CLIPBOARD2("B", "DATA"); - - RUN( Args() << "copy" - << "DATA3" << "C" - << "DATA4" << "D" - , "true\n" ); - WAIT_FOR_CLIPBOARD2("C", "DATA3"); - WAIT_FOR_CLIPBOARD2("D", "DATA4"); - - RUN( "copy({'DATA1': 1, 'DATA2': 2})", "true\n" ); - WAIT_FOR_CLIPBOARD2("1", "DATA1"); - WAIT_FOR_CLIPBOARD2("2", "DATA2"); + using KeyPair = QPair; + const QList keyPairs = QList() + << KeyPair(keyNameFor(QKeySequence::NextChild), keyNameFor(QKeySequence::PreviousChild)) + << KeyPair("RIGHT", "LEFT"); - RUN_EXPECT_ERROR_WITH_STDERR( - "copy({}, {})", - CommandException, "Expected single item"); - RUN_EXPECT_ERROR_WITH_STDERR( - "copy([{}, {}])", - CommandException, "Expected single item"); -} + for (const auto &keyPair : keyPairs) { + for (const auto &optionValue : {"false", "true"}) { + RUN("config" << "tab_tree" << optionValue, QString(optionValue) + "\n"); -void Tests::commandClipboard() -{ - TEST( m_test->setClipboard("A") ); - WAIT_FOR_CLIPBOARD("A"); - RUN("clipboard", "A"); + RUN("keys" << keyPair.first, ""); + RUN("testSelected", tab1 + "\n"); + RUN("keys" << keyPair.first, ""); + RUN("testSelected", tab2 + "\n"); + RUN("keys" << keyPair.first, ""); - TEST( m_test->setClipboard("B", "DATA") ); - WAIT_FOR_CLIPBOARD2("B", "DATA"); - RUN("clipboard" << "DATA", "B"); + RUN("keys" << keyPair.second, ""); + RUN("testSelected", tab2 + "\n"); + RUN("keys" << keyPair.second, ""); + RUN("testSelected", tab1 + "\n"); + RUN("keys" << keyPair.second, ""); + } + } } -void Tests::commandHasClipboardFormat() +void Tests::itemPreview() { - TEST( m_test->setClipboard("B", "DATA") ); - WAIT_FOR_CLIPBOARD2("B", "DATA"); - WAIT_ON_OUTPUT("hasClipboardFormat('DATA')", "true\n"); - WAIT_ON_OUTPUT("hasClipboardFormat('text/plain')", "false\n"); -} + const auto tab1 = testTab(1); + RUN("tab" << tab1 << "add" << "def" << "abc", ""); + RUN("setCurrentTab" << tab1, ""); -void Tests::commandEdit() -{ - SKIP_ON_ENV("COPYQ_TESTS_SKIP_COMMAND_EDIT"); + RUN("preview", "false\n"); + RUN("keys" << clipboardBrowserId << "F7", ""); + RUN("preview", "true\n"); - RUN("config" << "editor" << "", "\n"); + RUN("keys" << clipboardBrowserId << "TAB" << itemPreviewId, ""); + RUN("keys" << itemPreviewId << "HOME", ""); + RUN("keys" << itemPreviewId << "RIGHT", ""); + RUN("keys" << itemPreviewId << "SHIFT+RIGHT", ""); + RUN("keys" << itemPreviewId << keyNameFor(QKeySequence::Copy), ""); + WAIT_FOR_CLIPBOARD("b"); - // Edit clipboard and new item. - TEST( m_test->setClipboard("TEST") ); - RUN("edit" << "-1", ""); - RUN("keys" << "END" << ":LINE 1" << "F2", ""); - RUN("read" << "0", "TESTLINE 1"); - WAIT_FOR_CLIPBOARD("TESTLINE 1"); + RUN("keys" << itemPreviewId << "F7" << clipboardBrowserId, ""); - // Edit existing item. - RUN("edit" << "0", ""); - RUN("keys" << "END" << "ENTER" << ":LINE 2" << "F2", ""); - RUN("read" << "0", "TESTLINE 1\nLINE 2"); - WAIT_FOR_CLIPBOARD("TESTLINE 1"); - - // Edit clipboard (ignore existing data) and new item. - RUN("edit", ""); - RUN("keys" << "END" << ":LINE 1" << "F2", ""); - RUN("read" << "0", "LINE 1"); - WAIT_FOR_CLIPBOARD("LINE 1"); + RUN("preview" << "true", "false\n"); + RUN("preview" << "false", "true\n"); + RUN("preview" << "1", "false\n"); + RUN("preview" << "0", "true\n"); + RUN("preview(true)", "false\n"); + RUN("preview(false)", "true\n"); + RUN("preview", "false\n"); } -void Tests::commandEditItem() +void Tests::openAndSavePreferences() { - SKIP_ON_ENV("COPYQ_TESTS_SKIP_COMMAND_EDIT"); - - RUN("config" << "editor" << "", "\n"); - - // Edit clipboard and new item. - TEST( m_test->setClipboard("TEST", mimeHtml) ); - RUN("editItem" << "-1" << mimeHtml, ""); - RUN("keys" << "END" << ":LINE 1" << "F2", ""); -#ifdef Q_OS_WIN -# define FRAG_START "" -# define FRAG_END "" - const auto expected = QByteArrayLiteral(FRAG_START "TEST" FRAG_END "LINE 1"); -#else - const auto expected = QByteArrayLiteral("TESTLINE 1"); -#endif - RUN("read" << mimeHtml << "0", expected); - RUN("read" << "0", ""); - WAIT_FOR_CLIPBOARD2(expected, mimeHtml); - WAIT_FOR_CLIPBOARD(""); - - // Edit existing item. - RUN("editItem" << "0" << mimeHtml, ""); - RUN("keys" << "END" << "ENTER" << ":LINE 2" << "F2", ""); - RUN("read" << mimeHtml << "0", expected + "\nLINE 2"); - RUN("read" << "0", ""); - WAIT_FOR_CLIPBOARD2(expected, mimeHtml); - WAIT_FOR_CLIPBOARD(""); - - // Edit clipboard (ignore existing data) and new item. - RUN("editItem" << "-1" << mimeHtml << "TEST", ""); - RUN("keys" << "END" << ":LINE 1" << "F2", ""); - RUN("read" << mimeHtml << "0", "TESTLINE 1"); - RUN("read" << "0", ""); -#ifdef Q_OS_WIN - WAIT_FOR_CLIPBOARD2(FRAG_START "TESTLINE 1" FRAG_END, mimeHtml); -#else - WAIT_FOR_CLIPBOARD2("TESTLINE 1", mimeHtml); +#ifdef Q_OS_MAC + SKIP("Can't focus configuration checkboxes on OS X"); #endif - WAIT_FOR_CLIPBOARD(""); -} - -void Tests::commandGetSetCurrentPath() -{ - RUN("currentPath", QDir::currentPath() + "\n"); - const auto newPath = QDir::homePath().toUtf8(); + RUN("config" << "check_clipboard" << "false", "false\n"); - TEST( m_test->runClient( - Args("eval") << "currentPath(input()); print(currentPath())", - newPath, newPath) ); + // Open preferences dialog. + RUN("keys" << "Ctrl+P" << configurationDialogId, ""); - TEST( m_test->runClient( - Args("eval") << "currentPath(input()); print(Dir().absolutePath())", - newPath, newPath) ); + // Focus and set wrap text option. + // This behavior could differ on some systems and in other languages. + RUN("keys" << configurationDialogId << "ALT+1", ""); + // Wait for any checkbox animation or delay + waitFor(1000); + RUN("keys" << configurationDialogId << "ENTER" << clipboardBrowserId, ""); + WAIT_ON_OUTPUT("config" << "check_clipboard", "true\n"); } -void Tests::commandSelectItems() +void Tests::pasteFromMainWindow() { - RUN("add" << "C" << "B" << "A", ""); + RUN("config" + << "activate_closes" << "true" + << "activate_focuses" << "true" + << "activate_pastes" << "true" + , + "activate_closes=true\n" + "activate_focuses=true\n" + "activate_pastes=true\n" + ); - RUN("selectItems" << "1", "true\n"); - RUN("testSelected", QString(clipboardTabName) + " 1 1\n"); + RUN("add" << "TEST", ""); + RUN("hide", ""); + runMultiple( + [&]() { RUN(WITH_TIMEOUT "dialog('text')", "TEST\n"); }, + [&]() { + RUN("keys" << "focus::QLineEdit in :QDialog", ""); + RUN("show", ""); + RUN("keys" << clipboardBrowserId << "ENTER", ""); - RUN("selectItems" << "0" << "2", "true\n"); - RUN("testSelected", QString(clipboardTabName) + " 2 0 2\n"); + WAIT_FOR_CLIPBOARD("TEST"); + waitFor(waitMsPasteClipboard); - const auto tab = testTab(1); - const auto args = Args("tab") << tab; - RUN(args << "add" << "C" << "B" << "A", ""); - RUN(args << "selectItems" << "1" << "2", "true\n"); - RUN("testSelected", QString(clipboardTabName) + " 2 0 2\n"); - RUN("setCurrentTab" << tab, ""); - RUN("testSelected", tab + " 2 1 2\n"); + RUN("keys" << "focus::QLineEdit in :QDialog" << "ENTER", ""); + } + ); } -void Tests::commandsExportImport() +void Tests::pasteNext() { const auto tab1 = testTab(1); - RUN("tab" << tab1 << "add" << "C" << "B" << "A", ""); + RUN("setCurrentTab" << tab1, ""); + RUN("keys" + << clipboardBrowserId << "CTRL+N" + << editorId << ":NEW ", ""); const auto tab2 = testTab(2); - RUN("tab" << tab2 << "add" << "3" << "2", ""); - - RUN("config" << "maxitems" << "3", "3\n"); - RUN("config" << "editor" << "EDITOR1 %1", "EDITOR1 %1\n"); - - TemporaryFile tmp; - const auto fileName = tmp.fileName(); - - RUN("exportData" << fileName, ""); - - RUN("config" << "maxitems" << "1", "1\n"); - RUN("config" << "editor" << "EDITOR2 %1", "EDITOR2 %1\n"); - RUN("removetab" << tab1, ""); - RUN("tab" << tab2 << "add" << "1", ""); - - RUN("importData" << fileName, ""); - - RUN("config" << "maxitems", "3\n"); - RUN("config" << "editor", "EDITOR1 %1\n"); - - const auto suffix = " (1)"; - RUN("tab", - QString(clipboardTabName) + "\n" - + tab2 + "\n" - + clipboardTabName + suffix + "\n" - + tab1 + "\n" - + tab2 + suffix + "\n"); - - RUN("tab" << tab1 << "read" << "0" << "1" << "2", "A\nB\nC"); - RUN("tab" << tab2 + suffix << "read" << "0" << "1", "2\n3"); - RUN("tab" << tab2 << "read" << "0", "1"); -} - -void Tests::commandsGetSetCommands() -{ - RUN("commands().length", "0\n"); - - RUN("setCommands([{name: 'test', cmd: 'copyq help'}])", ""); - RUN("commands().length", "1\n"); - RUN("commands()[0].name", "test\n"); - RUN("commands()[0].cmd", "copyq help\n"); - RUN("commands()[0].enable", "true\n"); - - RUN("setCommands(commands())", ""); - RUN("commands().length", "1\n"); - RUN("commands()[0].name", "test\n"); - RUN("commands()[0].enable", "true\n"); -} - -void Tests::commandsImportExportCommands() -{ - const QString commands = - R"(' - [Commands] - 1\Name=Test 1 - 2\Name=Test 2 - size=2 - ')"; - RUN("eval" << "importCommands(arguments[1]).length" << "--" << commands, "2\n"); - RUN("eval" << "importCommands(arguments[1])[0].name" << "--" << commands, "Test 1\n"); - RUN("eval" << "importCommands(arguments[1])[1].name" << "--" << commands, "Test 2\n"); - - RUN("importCommands(exportCommands([{},{}])).length", "2\n"); - RUN("importCommands(exportCommands([{},{name: 'Test 2'}]))[1].name", "Test 2\n"); -} - -void Tests::commandsImportExportCommandsFixIndentation() -{ - { - const QString commands = - "[Command]\n" - "Command=\"\n 1\n 2\n 3\""; - RUN("eval" << "importCommands(arguments[1])[0].cmd" << "--" << commands, "1\n2\n3\n"); - } - - { - const QString commands = - "[Command]\n" - "Command=\"\r\n 1\r\n 2\r\n 3\""; - RUN("eval" << "importCommands(arguments[1])[0].cmd" << "--" << commands, "1\n2\n3\n"); - } -} - -void Tests::commandsAddCommandsRegExp() -{ - const QString commands = - "[Command]\n" - "Match=^(https?|ftps?)://\\\\$\n"; - - // Ensure there is a basic RegExp support. - RUN("/test/", "/test/\n"); - RUN("/test/.source", "test\n"); - - RUN("eval" << "exportCommands(importCommands(arguments[1]))" << "--" << commands, commands); - RUN("eval" << "Object.prototype.toString.call(importCommands(arguments[1])[0].re)" << "--" << commands, "[object RegExp]\n"); - RUN("eval" << "Object.prototype.toString.call(importCommands(arguments[1])[0].wndre)" << "--" << commands, "[object RegExp]\n"); - RUN("eval" << "importCommands(arguments[1])[0].re" << "--" << commands, "/^(https?|ftps?):\\/\\/\\$/\n"); - RUN("eval" << "importCommands(arguments[1])[0].wndre" << "--" << commands, "/(?:)/\n"); - - RUN("eval" << "addCommands(importCommands(arguments[1]))" << "--" << commands, ""); - RUN("keys" << commandDialogListId << "Enter" << clipboardBrowserId, ""); - - RUN("exportCommands(commands())", commands); - RUN("commands()[0].name", "\n"); - RUN("commands()[0].re", "/^(https?|ftps?):\\/\\/\\$/\n"); - RUN("commands()[0].wndre", "/(?:)/\n"); -} - -void Tests::commandScreenshot() -{ - RUN("screenshot().size() > 0", "true\n"); -} - -void Tests::commandNotification() -{ - const auto script = R"( - notification( - '.title', 'title', - '.message', 'message', - '.time', 1000, - '.id', 'test', - '.icon', 'copyq', - '.button', 'OK', '', '', - '.button', 'CANCEL', '', '' - ) - )"; - RUN(script, ""); - - RUN_EXPECT_ERROR_WITH_STDERR( - "notification('.message', 'message', 'BAD')", CommandException, "Unknown argument: BAD"); -} - -void Tests::commandIcon() -{ - RUN("iconColor", QByteArray(defaultSessionColor) + "\n"); - - RUN("iconColor" << "red", ""); - RUN("iconColor", "#ff0000\n"); - - RUN_EXPECT_ERROR("iconColor" << "BAD_COLOR_NAME", CommandException); - RUN("iconColor", "#ff0000\n"); - - RUN("iconColor" << "", ""); - RUN("iconColor", QByteArray(defaultSessionColor) + "\n"); - - RUN("iconColor" << defaultSessionColor, ""); - RUN("iconColor", QByteArray(defaultSessionColor) + "\n"); -} - -void Tests::commandIconTag() -{ - RUN("iconTag", "\n"); - - RUN("iconTag" << "TEST", ""); - RUN("iconTag", "TEST\n"); - - RUN("iconTag" << "", ""); - RUN("iconTag", "\n"); -} - -void Tests::commandIconTagColor() -{ - RUN("iconTagColor", QByteArray(defaultTagColor) + "\n"); - - RUN("iconTagColor" << "red", ""); - RUN("iconTagColor", "#ff0000\n"); - - RUN_EXPECT_ERROR("iconTagColor" << "BAD_COLOR_NAME", CommandException); - RUN("iconTagColor", "#ff0000\n"); + RUN("tab" << tab2 << "add" << "test3" << "test2" << "test1", ""); + RUN("tab" << tab2 << "next(); paste(); next()", ""); + waitFor(waitMsPasteClipboard); - RUN("iconTagColor" << defaultTagColor, ""); - RUN("iconTagColor", QByteArray(defaultTagColor) + "\n"); + RUN("keys" << editorId, ""); + WAIT_FOR_CLIPBOARD("test3"); + RUN("keys" << editorId << "F2", ""); + RUN("tab" << tab1 << "read" << "0", "NEW test2"); } -void Tests::commandLoadTheme() +void Tests::configAutostart() { - RUN_EXPECT_ERROR_WITH_STDERR( - "loadTheme" << "a non-existent file", CommandException, "ScriptError: Failed to read theme"); - RUN_EXPECT_ERROR_WITH_STDERR( - "loadTheme" << ".", CommandException, "ScriptError: Failed to read theme"); - - { - QTemporaryFile tmp; - QVERIFY(tmp.open()); - tmp.write("INVALID INI FILE"); - tmp.close(); - RUN_EXPECT_ERROR_WITH_STDERR( - "loadTheme" << tmp.fileName(), CommandException, "ScriptError: Failed to parse theme"); - } - - { - QTemporaryFile tmp; - QVERIFY(tmp.open()); - tmp.write("[General]"); - tmp.close(); - RUN("loadTheme" << tmp.fileName(), ""); - } - - // Verify default stylesheets - if there is a syntax error, - // application prints a warning which should be captured by tests. - { - QTemporaryFile tmp; - QVERIFY(tmp.open()); - tmp.write("[General]\nstyle_main_window=true"); - tmp.close(); - RUN("loadTheme" << tmp.fileName(), ""); + if ( !platformNativeInterface()->canAutostart() ) { + SKIP("Autostart is unsupported on this platform"); + return; } + + RUN("config" << "autostart" << "true", "true\n"); + RUN("config" << "autostart", "true\n"); + RUN("config" << "autostart" << "false", "false\n"); + RUN("config" << "autostart", "false\n"); } -void Tests::commandDateString() -{ - const auto dateFormat = "TEST:yyyy-MM-dd"; - const auto dateTime = QDateTime::currentDateTime(); - const auto today = dateTime.toString(dateFormat); - RUN("dateString" << dateFormat, today + "\n"); -} - -void Tests::commandAfterMilliseconds() -{ - const QString script = "afterMilliseconds(100, function(){ print('TEST'); abort(); });"; - RUN(script, ""); - RUN(script + "sleep(1)", ""); - RUN(script + "sleep(200)", "TEST"); -} - -void Tests::commandAsync() -{ - RUN("afterMilliseconds(0, function() { print(currentItem()); abort(); }); dialog()", "-1"); -} - -void Tests::commandFilter() -{ - RUN("filter", "\n"); - RUN("filter" << "test", ""); - RUN("filter", "test\n"); - RUN("filter" << "", ""); - RUN("filter", "\n"); - - // Empty filter() after ESC. - RUN("filter" << "test", ""); - RUN("filter", "test\n"); - RUN("keys" << "ESC", ""); - RUN("filter", "\n"); -} - -void Tests::commandMimeTypes() -{ - RUN("print(mimeText)", mimeText); - RUN("print(mimeHtml)", mimeHtml); - RUN("print(mimeUriList)", mimeUriList); - RUN("print(mimeWindowTitle)", mimeWindowTitle); - RUN("print(mimeItems)", mimeItems); - RUN("print(mimeItemNotes)", mimeItemNotes); - RUN("print(mimeOwner)", mimeOwner); - RUN("print(mimeClipboardMode)", mimeClipboardMode); - RUN("print(mimeCurrentTab)", mimeCurrentTab); - RUN("print(mimeSelectedItems)", mimeSelectedItems); - RUN("print(mimeCurrentItem)", mimeCurrentItem); - RUN("print(mimeHidden)", mimeHidden); - RUN("print(mimeShortcut)", mimeShortcut); - RUN("print(mimeColor)", mimeColor); - RUN("print(mimeOutputTab)", mimeOutputTab); -} - -void Tests::commandUnload() -{ - // Failure if tab is visible. - RUN("unload", ""); - - const auto tab = testTab(1); - - // Success if tab doesn't exist. - RUN("unload" << tab, tab + "\n"); - - RUN("tab" << tab << "add" << "A", ""); - // Success if tab is not visible and editor is not open. - RUN("unload" << tab, tab + "\n"); - - RUN("tab" << tab << "add" << "B", ""); - RUN("unload", tab + "\n"); - // Success if tab is not loaded. - RUN("unload", tab + "\n"); - - // Success if tab does not exist. - RUN("unload" << "missing-tab", "missing-tab\n"); -} - -void Tests::commandForceUnload() -{ - RUN("forceUnload", ""); - RUN_EXPECT_ERROR_WITH_STDERR("add" << "A", CommandException, "ScriptError: Invalid tab"); - - RUN("keys" << clipboardBrowserRefreshButtonId << "Space", ""); - RUN("add" << "A", ""); - - const auto tab = testTab(1); - RUN("tab" << tab << "add" << "A", ""); - - RUN("forceUnload" << tab, ""); - - RUN("setCurrentTab" << tab, ""); - RUN_EXPECT_ERROR_WITH_STDERR( - "tab" << tab << "add" << "B", CommandException, "ScriptError: Invalid tab"); - - RUN("keys" << clipboardBrowserRefreshButtonId << "Space", ""); - RUN("add" << "B", ""); -} - -void Tests::commandServerLogAndLogs() -{ - const QByteArray data1 = generateData(); - QRegularExpression re("CopyQ Note \\[[^]]+\\] : " + QRegularExpression::escape(data1)); - - QByteArray stdoutActual; - QByteArray stderrActual; - - QCOMPARE( run(Args("logs"), &stdoutActual, &stderrActual), 0 ); - QVERIFY2( testStderr(stderrActual), stderrActual ); - QVERIFY( !stdoutActual.isEmpty() ); - QVERIFY( !QString::fromUtf8(stdoutActual).contains(re) ); - - RUN("serverLog" << data1, ""); - - QCOMPARE( run(Args("logs"), &stdoutActual, &stderrActual), 0 ); - QVERIFY2( testStderr(stderrActual), stderrActual ); - QVERIFY( !stdoutActual.isEmpty() ); - QVERIFY( QString::fromUtf8(stdoutActual).contains(re) ); -} - -void Tests::classByteArray() -{ - RUN("ByteArray", ""); - RUN("ByteArray('test')", "test"); - RUN("ByteArray(ByteArray('test'))", "test"); - RUN("typeof(ByteArray('test'))", "object\n"); - RUN("ByteArray('test') instanceof ByteArray", "true\n"); - RUN("b = ByteArray('0123'); b.chop(2); b", "01"); - RUN("ByteArray('0123').equals(ByteArray('0123'))", "true\n"); - RUN("ByteArray('0123').left(3)", "012"); - RUN("ByteArray('0123').mid(1, 2)", "12"); - RUN("ByteArray('0123').mid(1)", "123"); - RUN("ByteArray('0123').right(3)", "123"); - RUN("ByteArray('0123').remove(1, 2)", "03"); - RUN("ByteArray(' 01 23 ').simplified()", "01 23"); - RUN("ByteArray('0123').toBase64()", "MDEyMw=="); - RUN("ByteArray('ABCd').toLower()", "abcd"); - RUN("ByteArray('abcD').toUpper()", "ABCD"); - RUN("ByteArray(' 01 23 ').trimmed()", "01 23"); - RUN("b = ByteArray('0123'); b.truncate(2); b", "01"); - RUN("ByteArray('0123').toLatin1String() == '0123'", "true\n"); - RUN("ByteArray('0123').valueOf() == '0123'", "true\n"); - RUN("ByteArray(8).size()", "8\n"); - RUN("b = ByteArray(); b.length = 10; b.length", "10\n"); - - // ByteArray implicitly converts to str. - RUN("ByteArray('test') == 'test'", "true\n"); - RUN("ByteArray('test1') == 'test2'", "false\n"); - RUN("ByteArray('test') === 'test'", "false\n"); - RUN("ByteArray('a') + 'b'", "ab\n"); -} - -void Tests::classFile() -{ - RUN("var f = new File('/copyq_missing_file'); f.exists()", "false\n"); -} - -void Tests::classDir() -{ - RUN("var d = new Dir('/missing_directory/')" - "; d.exists()" - , "false\n" - ); - - const auto home = QDir::homePath(); - RUN("Dir().homePath()", home + "\n"); - RUN("Dir().home().path()", home + "\n"); - - const auto root = QDir::rootPath(); - RUN("Dir().rootPath()", root + "\n"); - RUN("Dir().root().path()", root + "\n"); - - const auto temp = QDir::tempPath(); - RUN("Dir().tempPath()", temp + "\n"); - RUN("Dir().temp().path()", temp + "\n"); - - RUN("Dir().separator()", QString(QDir::separator()) + "\n"); - - QTemporaryDir tmpDir; - QVERIFY(tmpDir.isValid()); - QDir dir(tmpDir.path()); - const auto path = dir.path(); - const auto args = QString::fromLatin1("var d = new Dir('%1')").arg(path); - - RUN(args << "d.exists()", "true\n"); - RUN(args << "d.isReadable()", "true\n"); - RUN(args << "d.isAbsolute()", "true\n"); - RUN(args << "d.isRelative()", "false\n"); - RUN(args << "d.absolutePath()", path + "\n"); - RUN(args << "d.path()", path + "\n"); - RUN(args << "d.makeAbsolute()", "true\n"); - - RUN(args << "d.mkdir('test')", "true\n"); - QVERIFY( QDir(dir.filePath("test")).exists() ); - RUN(args << "d.exists('test')", "true\n"); - - RUN(args << "d.mkpath('a/b/c')", "true\n"); - QVERIFY( QDir(dir.filePath("a/b/c")).exists() ); - RUN(args << "d.exists('a/b/c')", "true\n"); - RUN(args << "d.filePath('a/b/c')", dir.filePath("a/b/c") + "\n"); - RUN(args << "d.relativeFilePath('" + path + "/a/b/c')", "a/b/c\n"); - RUN("Dir('" + path + "/test/../a//b/c/..').canonicalPath()", dir.filePath("a/b") + "\n"); - RUN(args << "d.setPath('" + path + "/a/b/c')" << "d.path()", dir.filePath("a/b/c") + "\n"); - - RUN(args << "d.cd('a')" << "d.cd('b')", "true\n"); - RUN(args << "d.cd('x')", "false\n"); - RUN(args << "d.cd('a')" << "d.cd('b')" << "d.path()", dir.filePath("a/b") + "\n"); - RUN(args << "d.cd('a')" << "d.cd('..')" << "d.path()", path + "\n"); - RUN(args << "d.cd('a')" << "d.cdUp()" << "d.path()", path + "\n"); - - RUN(args << "d.count()", QString::fromLatin1("%1\n").arg(dir.count())); - RUN(args << "d.dirName()", QString::fromLatin1("%1\n").arg(dir.dirName())); - - RUN(args << "d.match(['a*'], 'test')", "false\n"); - RUN(args << "d.match(['t*'], 'test')", "true\n"); - RUN(args << "d.entryList()", ".\n..\na\ntest\n"); - RUN(args << "d.entryList(['t*'])", "test\n"); - RUN(args << "d.entryList(['t?st', 'a*'])", "a\ntest\n"); - - RUN(args << "d.setNameFilters(['t?st', 'a*'])" << "d.nameFilters()", "t?st\na*\n"); - - QFile f(dir.filePath("test.txt")); - QVERIFY( f.open(QIODevice::WriteOnly) ); - f.close(); - RUN(args << "d.exists('test.txt')", "true\n"); - RUN(args << "d.absoluteFilePath('test.txt')", dir.filePath("test.txt") + "\n"); - RUN(args << "d.rename('test.txt', 'test2.txt')", "true\n"); - RUN(args << "d.exists('test2.txt')", "true\n"); - RUN(args << "d.remove('test2.txt')", "true\n"); - RUN(args << "d.exists('test2.txt')", "false\n"); - - RUN(args << "d.rmdir('test')", "true\n"); - RUN(args << "d.exists('test')", "false\n"); - - RUN("Dir().cleanPath('/a//b/../c/')", QDir::cleanPath("/a//b/../c/") + "\n"); -} - -void Tests::classTemporaryFile() -{ - RUN("var f = new TemporaryFile(); f.open()", "true\n"); - - QByteArray err; - - for ( const auto autoRemove : {true, false} ) { - QByteArray fileName; - const auto script = - QString(R"( - var f = new TemporaryFile() - if (!f.open()) - throw 'Failed to open temporary file' - - f.setAutoRemove(%1) - print(f.fileName()) - )").arg(autoRemove); - run(Args() << script, &fileName, &err); - QVERIFY2( testStderr(err), err ); - - QFile f( QString::fromUtf8(fileName) ); - QVERIFY( f.exists() != autoRemove ); - - if (!autoRemove) - f.remove(); - } - - QByteArray fileName; - const auto scriptWrite = R"( - var f = new TemporaryFile() - if (!f.open()) - throw 'Failed to open temporary file' - - if (!f.write('LINE')) - throw 'Failed to write to temporary file' - - f.setAutoRemove(false) - print(f.fileName()) - )"; - run(Args() << scriptWrite, &fileName); - QVERIFY2( testStderr(err), err ); - QVERIFY( QFile::exists(QString::fromUtf8(fileName)) ); - - QByteArray out; - const auto scriptRead = R"( - var f = new File(str(input())) - if (!f.openReadOnly()) - throw 'Failed to open file' - - print('' - + ' exists()=' + str(f.exists()) - + ' isOpen()=' + str(f.isOpen()) - + ' isReadable()=' + str(f.isReadable()) - + ' isWritable()=' + str(f.isWritable()) - + ' size()=' + str(f.size()) - + ' readAll()=' + str(f.readAll()) - + ' atEnd()=' + str(f.atEnd()) - + ' seek(0)=' + str(f.seek(0)) - + ' read(1)=' + str(f.read(1)) - + ' pos()=' + str(f.pos()) - + ' peek(1)=' + str(f.peek(1)) - + ' readLine()=' + str(f.readLine()) - ) - )"; - const QByteArray expectedOut = - " exists()=true" - " isOpen()=true" - " isReadable()=true" - " isWritable()=false" - " size()=4" - " readAll()=LINE" - " atEnd()=true" - " seek(0)=true" - " read(1)=L" - " pos()=1" - " peek(1)=I" - " readLine()=INE"; - run(Args() << scriptRead, &out, &err, fileName); - QVERIFY2( testStderr(err), err ); - QCOMPARE(out, expectedOut); - - const auto scriptRemove = R"( - var f = new File(str(input())) - if (!f.remove()) - throw 'Failed to remove file' - )"; - run(Args() << scriptRemove, &out, &err, fileName); - QVERIFY2( testStderr(err), err ); - QCOMPARE(QByteArray(), out); - QVERIFY( !QFile::exists(QString::fromUtf8(fileName)) ); - - RUN("TemporaryFile().autoRemove()", "true\n"); - RUN("TemporaryFile().fileTemplate()", QDir::temp().filePath("copyq.test.XXXXXX") + "\n"); -} - -void Tests::classItemSelection() -{ - const auto tab1 = testTab(1); - const Args args = Args("tab") << tab1 << "separator" << ","; - const QString outRows("ItemSelection(tab=\"" + tab1 + "\", rows=[%1])\n"); - - RUN(args << "add" << "C" << "B" << "A", ""); - RUN(args << "ItemSelection().length", "0\n"); - RUN("ItemSelection('" + tab1 + "').length", "0\n"); - RUN(args << "ItemSelection().selectAll().length", "3\n"); - RUN("ItemSelection('" + tab1 + "').selectAll().length", "3\n"); - - RUN(args << "a = ItemSelection(); b = a; a === b", "true\n"); - RUN(args << "a = ItemSelection(); b = a.selectAll(); a === b", "true\n"); - - RUN(args << "ItemSelection().selectAll().str()", outRows.arg("0..2")); - RUN(args << "ItemSelection().selectRemovable().str()", outRows.arg("0..2")); - RUN(args << "ItemSelection().selectRemovable().removeAll().str()", outRows.arg("")); - RUN(args << "read(0,1,2)", ",,"); - - RUN(args << "add" << "C" << "B" << "A", ""); - RUN(args << "ItemSelection().select(/A|C/).str()", outRows.arg("0,2")); - RUN(args << "ItemSelection().select(/a|c/i).str()", outRows.arg("0,2")); - RUN(args << "ItemSelection().select(/A/).select(/C/).str()", outRows.arg("0,2")); - RUN(args << "ItemSelection().select(/C/).select(/A/).str()", outRows.arg("2,0")); - RUN(args << "ItemSelection().select(/A|C/).invert().str()", outRows.arg("1")); - - RUN(args << "ItemSelection().select(/A|C/).deselectIndexes([0]).str()", outRows.arg("2")); - RUN(args << "ItemSelection().select(/A|C/).deselectIndexes([1]).str()", outRows.arg("0")); - RUN(args << "ItemSelection().select(/A|C/).deselectIndexes([0,1]).str()", outRows.arg("")); - RUN(args << "ItemSelection().select(/A|C/).deselectSelection(ItemSelection().select(/A/)).str()", outRows.arg("2")); - RUN(args << "ItemSelection().select(/A|C/).deselectSelection(ItemSelection().select(/C/)).str()", outRows.arg("0")); - RUN(args << "ItemSelection().select(/A|C/).deselectSelection(ItemSelection().selectAll()).str()", outRows.arg("")); - - RUN(args << "a = ItemSelection().select(/a/i); b = a.copy(); a !== b", "true\n"); - RUN(args << "a = ItemSelection().select(/a/i); b = a.copy(); a.str() == b.str()", "true\n"); - RUN(args << "a = ItemSelection().select(/a|b/i); b = a.copy(); b.select(/C/); [a.rows(), '', b.rows()]", "0\n1\n\n0\n1\n2\n"); - - RUN(args << "s = ItemSelection().selectAll(); insert(1, 'X'); insert(3, 'Y'); s.invert().str()", outRows.arg("1,3")); - - RUN(args << "ItemSelection().select(/a/i).invert().removeAll().str()", outRows.arg("")); - RUN(args << "read(0,1,2)", "A,,"); - - RUN(args << "ItemSelection().selectAll().removeAll().str()", outRows.arg("")); - RUN(args << "read(0,1,2)", ",,"); - - RUN(args << "write('application/x-tst', 'ghi')", ""); - RUN(args << "write('application/x-tst', 'def')", ""); - RUN(args << "write('application/x-tst', 'abc')", ""); - RUN(args << "read('application/x-tst',0,1,2)", "abc,def,ghi"); - RUN(args << "ItemSelection().select(/e/, 'application/x-tst').str()", outRows.arg("1")); - RUN(args << "ItemSelection().select(/e/, 'application/x-tst').removeAll().str()", outRows.arg("")); - RUN(args << "read('application/x-tst',0,1,2)", "abc,ghi,"); - - RUN(args << "ItemSelection().selectAll().itemAtIndex(0)['application/x-tst']", "abc"); - RUN(args << "ItemSelection().selectAll().itemAtIndex(1)['application/x-tst']", "ghi"); - RUN(args << "ItemSelection().select(/h/, 'application/x-tst').itemAtIndex(0)['application/x-tst']", "ghi"); - RUN(args << "ItemSelection().select(/h/, 'application/x-tst').itemAtIndex(1)['application/x-tst'] == undefined", "true\n"); - - RUN(args << "ItemSelection().select(/ghi/, 'application/x-tst').setItemAtIndex(0, {'application/x-tst': 'def'}).str()", - outRows.arg("1")); - RUN(args << "read('application/x-tst',0,1,2)", "abc,def,"); - - RUN(args << "d = ItemSelection().selectAll().items(); [d.length, str(d[0]['application/x-tst']), str(d[1]['application/x-tst'])]", "2\nabc\ndef\n"); - RUN(args << "ItemSelection().selectAll().setItems([{'application/x-tst': 'xyz'}]).str()", outRows.arg("0,1")); - RUN(args << "read('application/x-tst',0,1,2)", "xyz,def,"); - - RUN(args << "ItemSelection().selectAll().setItemsFormat(mimeItemNotes, 'test1').str()", outRows.arg("0,1")); - RUN(args << "read(mimeItemNotes,0,1,2)", "test1,test1,"); - RUN(args << "read('application/x-tst',0,1,2)", "xyz,def,"); - - RUN(args << "ItemSelection().selectAll().setItems([{'application/x-tst': ByteArray('abc')}]).str()", outRows.arg("0,1")); - RUN(args << "read('application/x-tst',0,1,2)", "abc,def,"); - - RUN(args << "ItemSelection().selectAll().setItemsFormat(mimeItemNotes, ByteArray('test2')).str()", outRows.arg("0,1")); - RUN(args << "read(mimeItemNotes,0,1,2)", "test2,test2,"); - RUN(args << "read('application/x-tst',0,1,2)", "abc,def,"); - - RUN(args << "ItemSelection().selectAll().itemsFormat(mimeItemNotes).map(str)", "test2\ntest2\n"); - RUN(args << "ItemSelection().selectAll().itemsFormat('application/x-tst').map(str)", "abc\ndef\n"); - RUN(args << "ItemSelection().selectAll().itemsFormat(ByteArray('application/x-tst')).map(str)", "abc\ndef\n"); - - RUN(args << "ItemSelection().selectAll().setItemsFormat(mimeItemNotes, undefined).str()", outRows.arg("0,1")); - RUN(args << "read(mimeItemNotes,0,1,2)", ",,"); - RUN(args << "read('application/x-tst',0,1,2)", "abc,def,"); - - RUN(args << "ItemSelection().selectAll().removeAll().str()", outRows.arg("")); - RUN(args << "add" << "C" << "B" << "A", ""); - RUN(args << "ItemSelection().select(/C/).move(1).str()", outRows.arg("1")); - RUN(args << "read(0,1,2)", "A,C,B"); - RUN(args << "ItemSelection().select(/B/).select(/C/).move(1).str()", outRows.arg("2,1")); - RUN(args << "read(0,1,2)", "A,C,B"); - RUN(args << "ItemSelection().select(/A/).move(2).str()", outRows.arg("1")); - RUN(args << "read(0,1,2)", "C,A,B"); - RUN(args << "ItemSelection().select(/C/).select(/B/).move(2).str()", outRows.arg("1,2")); - RUN(args << "read(0,1,2)", "A,C,B"); - - RUN(args << "change(1, mimeItemNotes, 'NOTE'); read(mimeItemNotes,0,1,2)", ",NOTE,"); - RUN(args << "ItemSelection().select(/.*/, mimeItemNotes).str()", outRows.arg("1")); - RUN(args << "ItemSelection().select(undefined, mimeItemNotes).str()", outRows.arg("0,2")); - - // Match nothing if select() argument is not a regular expression. - RUN(args << "add" << "", ""); - RUN(args << "ItemSelection().select('A').str()", outRows.arg("")); -} - -void Tests::classItemSelectionGetCurrent() -{ - const auto tab1 = testTab(1); - const Args args = Args("tab") << tab1 << "separator" << ","; - - RUN("ItemSelection().tab", "CLIPBOARD\n"); - RUN(args << "ItemSelection().tab", tab1 + "\n"); - - RUN(args << "ItemSelection().current().tab", "CLIPBOARD\n"); - RUN(args << "ItemSelection().current().str()", "ItemSelection(tab=\"CLIPBOARD\", rows=[])\n"); - RUN("setCurrentTab" << tab1, ""); - RUN(args << "ItemSelection().current().tab", tab1 + "\n"); - RUN(args << "ItemSelection().current().str()", "ItemSelection(tab=\"" + tab1 + "\", rows=[])\n"); - - RUN(args << "add" << "C" << "B" << "A", ""); - RUN(args << "ItemSelection().current().str()", "ItemSelection(tab=\"" + tab1 + "\", rows=[0])\n"); - - RUN("setCommands([{name: 'test', inMenu: true, shortcuts: ['Ctrl+F1'], cmd: 'copyq: add(ItemSelection().current().str())'}])", ""); - RUN("keys" << "CTRL+F1", ""); - WAIT_ON_OUTPUT(args << "read(0)", "ItemSelection(tab=\"" + tab1 + "\", rows=[0])"); - RUN("keys" << "END" << "SHIFT+UP" << "CTRL+F1", ""); - WAIT_ON_OUTPUT(args << "read(0)", "ItemSelection(tab=\"" + tab1 + "\", rows=[2,3])"); -} - -void Tests::classItemSelectionByteArray() -{ - const auto tab1 = testTab(1); - const Args args = Args("tab") << tab1 << "separator" << ","; - RUN("setCurrentTab" << tab1, ""); - - RUN(args << "add" << "C" << "B" << "A", ""); - RUN(args << "ByteArray(ItemSelection().selectAll().itemAtIndex(0)[mimeText])", "A"); - RUN(args << "str(ItemSelection().selectAll().itemAtIndex(0)[mimeText])", "A\n"); - RUN(args << "write(0, [ItemSelection().selectAll().itemAtIndex(2)])" - << "read(mimeText, 0)", "C"); -} - -void Tests::classItemSelectionSort() -{ - const auto tab1 = testTab(1); - const Args args = Args("tab") << tab1 << "separator" << ","; - const QString outRows("ItemSelection(tab=\"" + tab1 + "\", rows=[%1])\n"); - RUN("setCurrentTab" << tab1, ""); - - RUN(args << "add(2,5,1,3,4)", ""); - RUN(args << "read(0,1,2,3,4)", "4,3,1,5,2"); - - const auto script = R"( - var sel = ItemSelection().selectAll(); - const texts = sel.itemsFormat(mimeText); - sel.sort(function(i,j){ - return texts[i] < texts[j]; - }); - sel.str(); - )"; - RUN(args << script, outRows.arg("3,2,0,4,1")); - RUN(args << "read(0,1,2,3,4)", "1,2,3,4,5"); - RUN(args << "size", "5\n"); -} - -void Tests::classSettings() -{ - TemporaryFile configFile; - const QString fileName = configFile.fileName(); - - RUN("eval" << "s=Settings(str(arguments[1])); print(s.fileName())" << fileName, fileName); - RUN("eval" << "s=Settings(str(arguments[1])); s.isWritable() === true" << fileName, "true\n"); - - RUN("eval" << "s=Settings(str(arguments[1])); s.contains('o1')" << fileName, "false\n"); - RUN("eval" << "s=Settings(str(arguments[1])); s.setValue('o1', 1); s.sync(); s.contains('o1')" << fileName, "true\n"); - RUN("eval" << "s=Settings(str(arguments[1])); s.value('o1')" << fileName, "1\n"); - - RUN("eval" << "s=Settings(str(arguments[1])); s.setValue('o2', 2)" << fileName, ""); - RUN("eval" << "s=Settings(str(arguments[1])); s.value('o2')" << fileName, "2\n"); - - RUN("eval" << "s=Settings(str(arguments[1])); s.setValue('o2', [1,2,3])" << fileName, ""); - RUN("eval" << "s=Settings(str(arguments[1])); s.value('o2')[0]" << fileName, "1\n"); - RUN("eval" << "s=Settings(str(arguments[1])); s.value('o2')[1]" << fileName, "2\n"); - RUN("eval" << "s=Settings(str(arguments[1])); s.value('o2')[2]" << fileName, "3\n"); - - RUN("eval" << "s=Settings(str(arguments[1])); s.setValue('g1/o3', true)" << fileName, ""); - RUN("eval" << "s=Settings(str(arguments[1])); s.value('g1/o3')" << fileName, "true\n"); - - RUN("eval" << "s=Settings(str(arguments[1])); s.childKeys()" << fileName, "o1\no2\n"); - RUN("eval" << "s=Settings(str(arguments[1])); s.allKeys()" << fileName, "g1/o3\no1\no2\n"); - - RUN("eval" << "s=Settings(str(arguments[1])); s.beginGroup('g1'); s.group()" << fileName, "g1\n"); - RUN("eval" << "s=Settings(str(arguments[1])); s.beginGroup('g1'); s.setValue('g1.2/o4', 'test')" << fileName, ""); - RUN("eval" << "s=Settings(str(arguments[1])); s.beginGroup('g1'); s.childGroups()" << fileName, "g1.2\n"); - RUN("eval" << "s=Settings(str(arguments[1])); s.beginGroup('g1'); s.endGroup(); s.childGroups()" << fileName, "g1\n"); - RUN("eval" << "s=Settings(str(arguments[1])); s.value('g1/g1.2/o4')" << fileName, "test\n"); - RUN("eval" << "s=Settings(str(arguments[1])); s.allKeys()" << fileName, "g1/g1.2/o4\ng1/o3\no1\no2\n"); - RUN("eval" << "s=Settings(str(arguments[1])); s.remove('g1/g1.2/o4'); s.allKeys()" << fileName, "g1/o3\no1\no2\n"); - - RUN("eval" << "s=Settings(str(arguments[1])); s.beginWriteArray('a1', 3); s.setArrayIndex(1); s.setValue('o1', 'v1'); s.endArray()" << fileName, ""); - RUN("eval" << "s=Settings(str(arguments[1])); s.beginReadArray('a1')" << fileName, "3\n"); - RUN("eval" << "s=Settings(str(arguments[1])); s.beginReadArray('a1'); s.setArrayIndex(1); s.value('o1');" << fileName, "v1\n"); - - RUN("eval" << "s=Settings(str(arguments[1])); s.clear(); s.allKeys()" << fileName, ""); - - QVERIFY(QFile::remove(fileName)); - QVERIFY(!QFile::exists(fileName)); - RUN("eval" << "s=Settings(str(arguments[1])); s.setValue('o1', 1); s.sync(); File(str(arguments[1])).exists()" << fileName, "true\n"); - QVERIFY(QFile::exists(fileName)); - - QVERIFY(QFile::remove(fileName)); - QVERIFY(!QFile::exists(fileName)); - RUN("eval" << "s=Settings(str(arguments[1])); s.setValue('o1', 1)" << fileName, ""); - QVERIFY(QFile::exists(fileName)); - - const QString appConfigFileName = AppConfig().settings().fileName(); - RUN("Settings().fileName()", QStringLiteral("%1\n").arg(appConfigFileName)); - RUN("Settings().value('Options/tabs')", QStringLiteral("%1\n").arg(clipboardTabName)); -} - -void Tests::calledWithInstance() -{ - // These would fail with the old deprecated Qt Script module. - RUN("f=ByteArray().size; f()", "0\n"); - RUN("f=Dir().path; f()", ".\n"); - RUN("f=File('test').fileName; f()", "test\n"); - RUN("f=TemporaryFile().autoRemove; f()", "true\n"); -} - -void Tests::pipingCommands() -{ - const auto tab = testTab(1); - const Args args = Args("tab") << tab << "separator" << ","; - - RUN(args << "action" - << "copyq print HELLO | copyq print(str(input()).toLowerCase())", ""); - WAIT_ON_OUTPUT(args << "read" << "0" << "1", "hello,"); - - RUN(args << "action" - << "copyq print TEST" - " | copyq 'print(str(input()) + 1)'" - " | copyq 'print(str(input()) + 2)'" - " | copyq 'print(str(input()) + 3)'" - , ""); - WAIT_ON_OUTPUT(args << "read" << "0" << "1", "TEST123,hello"); -} - -void Tests::chainingCommands() -{ - const auto tab1 = testTab(1); - RUN("tab" << tab1 << "add" << "C" << "B" << "A", ""); - RUN("tab" << tab1 << "separator" << " " << "read" << "0" << "1" << "2", "A B C"); - RUN("tab" << tab1 << "separator" << "\\t" << "showAt" << "read" << "0" << "1" << "2", "A\tB\tC"); - - // Chain functions without arguments. - RUN("enable" << "disable" << "monitoring", "false\n"); - RUN("if (!monitoring()) enable" << "monitoring", "true\n"); - - // Don't treat arguments after "eval" as functions. - RUN("eval" << "arguments[1]" << "TEST", "TEST"); - RUN("eval" << "arguments[1]" << "--" << "TEST", "TEST"); -} - -void Tests::configMaxitems() -{ - RUN("config" << "maxitems" << "3", "3\n"); - RUN("add" << "A" << "B" << "C", ""); - RUN("add" << "D", ""); - RUN("separator" << " " << "read" << "0" << "1" << "2", "D C B"); - RUN("size", "3\n"); - - RUN("add" << "E" << "F", ""); - RUN("separator" << " " << "read" << "0" << "1" << "2", "F E D"); - RUN("size", "3\n"); - - RUN("config" << "maxitems" << "2", "2\n"); - RUN("separator" << " " << "read" << "0" << "1", "F E"); - RUN("size", "2\n"); - - // Adding too many items fails. - RUN_EXPECT_ERROR("add" << "1" << "2" << "3", CommandException); - RUN("separator" << " " << "read" << "0" << "1", "F E"); - RUN("size", "2\n"); - - // Single item in tabs. - RUN("config" << "maxitems" << "1", "1\n"); - RUN("separator" << " " << "read" << "0", "F"); - RUN("size", "1\n"); - - RUN("add" << "G", ""); - RUN("separator" << " " << "read" << "0", "G"); - RUN("size", "1\n"); - - RUN("write" << "1" << "text/plain" << "H", ""); - RUN("separator" << " " << "read" << "0", "H"); - RUN("size", "1\n"); - - // No items in tabs. - RUN("config" << "maxitems" << "0", "0\n"); - RUN("size", "0\n"); - - RUN_EXPECT_ERROR("add" << "1", CommandException); - RUN_EXPECT_ERROR("write" << "1", CommandException); - RUN("size", "0\n"); - - // Invalid value. - RUN("config" << "maxitems" << "-99", "0\n"); - RUN("size", "0\n"); -} - -void Tests::keysAndFocusing() -{ - RUN("disable", ""); - RUN("keys" << clipboardBrowserId << "CTRL+T", ""); - WAIT_ON_OUTPUT("currentWindowTitle", appWindowTitle("New Tab")); - - RUN("keys" << tabDialogLineEditId << "ESC", ""); - WAIT_ON_OUTPUT("currentWindowTitle", appWindowTitle("*Clipboard Storing Disabled*")); - - RUN("enable", ""); -} - -void Tests::selectItems() -{ - const auto tab = QString(clipboardTabName); - RUN("add" << "C" << "B" << "A", ""); - - RUN("keys" << "RIGHT" << "SHIFT+DOWN" << "SHIFT+DOWN", ""); - RUN("testSelected", tab + " 2 0 1 2\n"); - - RUN("keys" << "SHIFT+UP", ""); - RUN("testSelected", tab + " 1 0 1\n"); - - RUN("keys" << "END", ""); - RUN("testSelected", tab + " 2 2\n"); - - RUN("keys" << "SHIFT+UP", ""); - RUN("testSelected", tab + " 1 1 2\n"); - - RUN("keys" << "CTRL+A", ""); - RUN("testSelected", tab + " 1 0 1 2\n"); - - // CTRL+SPACE toggles current item selection - RUN("add" << "D", ""); - RUN("keys" << "PGUP" << "CTRL+SHIFT+DOWN" << "CTRL+SHIFT+DOWN", ""); - RUN("testSelected", tab + " 2 0\n"); - RUN("keys" << "CTRL+SPACE", ""); - RUN("testSelected", tab + " 2 0 2\n"); - RUN("keys" << "SHIFT+DOWN", ""); - RUN("testSelected", tab + " 3 0 2 3\n"); - RUN("keys" << "CTRL+SPACE", ""); - RUN("testSelected", tab + " 3 0 2\n"); -} - -void Tests::moveItems() -{ - const auto tab = QString(clipboardTabName); - const auto args = Args() << "separator" << " "; - RUN(args << "add" << "C" << "B" << "A", ""); - - // move item one down - RUN(args << "keys" << "RIGHT" << "CTRL+DOWN", ""); - RUN(args << "read" << "0" << "1" << "2", "B A C"); - RUN(args << "testSelected", tab + " 1 1\n"); - - // move items to top - RUN(args << "keys" << "SHIFT+DOWN" << "CTRL+HOME", ""); - RUN(args << "read" << "0" << "1" << "2", "A C B"); - RUN(args << "testSelected", tab + " 1 0 1\n"); -} - -void Tests::deleteItems() -{ - const auto tab = QString(clipboardTabName); - const auto args = Args() << "separator" << ","; - RUN(args << "add" << "C" << "B" << "A", ""); - - // delete first item - RUN(args << "keys" << "RIGHT" << m_test->shortcutToRemove(), ""); - RUN(args << "read" << "0" << "1" << "2", "B,C,"); - RUN(args << "testSelected", tab + " 0 0\n"); - - // select all and delete - RUN(args << "keys" << "CTRL+A" << m_test->shortcutToRemove(), ""); - RUN(args << "size", "0\n"); -} - -void Tests::searchItems() -{ - RUN("add" << "a" << "b" << "c", ""); - RUN("keys" << ":b" << "TAB", ""); - RUN("testSelected", QString(clipboardTabName) + " 1 1\n"); -} - -void Tests::searchItemsAndSelect() -{ - RUN("add" << "xx1" << "a" << "xx2" << "c" << "xx3" << "d", ""); - RUN("keys" << ":xx" << filterEditId, ""); - RUN("testSelected", QString(clipboardTabName) + " 1 1\n"); - - RUN("keys" << filterEditId << "DOWN" << filterEditId, ""); - RUN("testSelected", QString(clipboardTabName) + " 3 3\n"); - - RUN("keys" << filterEditId << "DOWN" << filterEditId, ""); - RUN("testSelected", QString(clipboardTabName) + " 5 5\n"); - - RUN("keys" << filterEditId << "TAB" << clipboardBrowserId, ""); -} - -void Tests::searchItemsAndCopy() -{ - RUN("add" << "TEST_ITEM", ""); - RUN("keys" << ":test" << "CTRL+C" << filterEditId, ""); - WAIT_FOR_CLIPBOARD("TEST_ITEM"); -} - -void Tests::searchRowNumber() -{ - RUN("add" << "d2" << "c" << "b2" << "a", ""); - - RUN("keys" << ":2" << "TAB", ""); - RUN("testSelected", QString(clipboardTabName) + " 1 1\n"); - RUN("keys" << "CTRL+A", ""); - RUN("testSelected", QString(clipboardTabName) + " 1 1 3\n"); - - RUN("config" << "row_index_from_one" << "false", "false\n"); - RUN("keys" << ":2" << "TAB", ""); - RUN("testSelected", QString(clipboardTabName) + " 2 2\n"); - RUN("keys" << "CTRL+A", ""); - RUN("testSelected", QString(clipboardTabName) + " 2 1 2 3\n"); -} - -void Tests::searchAccented() -{ - RUN("add" << "a" << "väčšina" << "a", ""); - RUN("filter" << "vacsina", ""); - WAIT_ON_OUTPUT("testSelected", QByteArray(clipboardTabName) + " 1 1\n"); -} - -void Tests::copyItems() -{ - const auto tab = QString(clipboardTabName); - RUN("add" << "C" << "B" << "A", ""); - - // Select and copy all items. - RUN("keys" << "CTRL+A" << keyNameFor(QKeySequence::Copy), ""); - - // This seems to be required on Windows. - WAIT_ON_OUTPUT("clipboard", "A\nB\nC"); - - // Paste all items. - RUN("keys" << keyNameFor(QKeySequence::Paste), ""); - RUN("separator" << " " << "read" << "0" << "1" << "2" << "3" << "4" << "5", "A B C A B C"); - RUN("size", "6\n"); -} - -void Tests::selectAndCopyOrder() -{ - const auto tab = testTab(1); - const Args args = Args("tab") << tab << "separator" << " "; - RUN(args << "add" << "D" << "C" << "B" << "A", ""); - RUN("setCurrentTab" << tab, ""); - - RUN("keys" << "END" << "SHIFT+UP" << "SHIFT+UP" << "SHIFT+UP", ""); - RUN(args << "testSelected", tab + " 0 0 1 2 3\n"); - - RUN("keys" << keyNameFor(QKeySequence::Copy), ""); - WAIT_ON_OUTPUT("clipboard", "D\nC\nB\nA"); -} - -void Tests::sortAndReverse() -{ - const auto tab = testTab(1); - const Args args = Args("tab") << tab << "separator" << " "; - RUN(args << "add" << "D" << "A" << "C" << "B", ""); - RUN("setCurrentTab" << tab, ""); - - RUN("keys" << "CTRL+A", ""); - RUN(args << "testSelected", tab + " 0 0 1 2 3\n"); - - RUN("keys" << "CTRL+SHIFT+S", ""); - RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "A B C D "); - RUN(args << "testSelected", tab + " 1 0 1 2 3\n"); - RUN("keys" << keyNameFor(QKeySequence::Copy), ""); - WAIT_ON_OUTPUT("clipboard", "A\nB\nC\nD"); - - RUN("keys" << "CTRL+SHIFT+R", ""); - RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "D C B A "); - RUN(args << "testSelected", tab + " 2 0 1 2 3\n"); - RUN("keys" << keyNameFor(QKeySequence::Copy), ""); - WAIT_ON_OUTPUT("clipboard", "D\nC\nB\nA"); - - RUN("keys" << "CTRL+SHIFT+R", ""); - RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "A B C D "); - RUN(args << "testSelected", tab + " 1 0 1 2 3\n"); -} - -void Tests::createTabDialog() -{ - const auto tab1 = testTab(1); - RUN("keys" - << clipboardBrowserId << "CTRL+T" - << tabDialogLineEditId << ":" + tab1 << "ENTER", ""); - RUN("testSelected", tab1 + "\n"); -} - -void Tests::copyPasteCommands() -{ - const QByteArray commands = - "[Commands]\n" - "1\\Name=Test 1\n" - "2\\Name=Test 2\n" - "size=2"; - - RUN("keys" << clipboardBrowserId << "F6", ""); - TEST( m_test->setClipboard(commands) ); - RUN("keys" << commandDialogListId << keyNameFor(QKeySequence::Paste), ""); - - TEST( m_test->setClipboard(QByteArray()) ); - RUN("keys" << commandDialogListId << keyNameFor(QKeySequence::Copy), ""); - WAIT_FOR_CLIPBOARD(commands); - - RUN("keys" << commandDialogListId << "Enter" << clipboardBrowserId, ""); - RUN("commands().length", "2\n"); -} - -void Tests::editItems() -{ - RUN("config" << "edit_ctrl_return" << "true", "true\n"); - - RUN("add" << "Line 4" << "Line 1", ""); - - RUN("keys" - << clipboardBrowserId << "F2" - << editorId << "END" << "ENTER" << ":Line 2" << "F2", ""); - RUN("read" << "0", "Line 1\nLine 2"); - - RUN("keys" - << clipboardBrowserId << "DOWN" << "F2" - << editorId << "HOME" << ":Line 3" << "ENTER" << "F2", ""); - RUN("read" << "1", "Line 3\nLine 4"); - RUN("read" << "0", "Line 1\nLine 2"); - - // Edit multiple items - RUN("keys" - << clipboardBrowserId << "SHIFT+UP" << "F2" - << editorId << "END" << "ENTER" << ":Line 5" << "F2", ""); - RUN("read" << "0", "Line 3\nLine 4\nLine 1\nLine 2\nLine 5"); - RUN("read" << "1", "Line 1\nLine 2"); - RUN("read" << "2", "Line 3\nLine 4"); -} - -void Tests::createNewItem() -{ - RUN("config" << "edit_ctrl_return" << "true", "true\n"); - - RUN("keys" << "CTRL+N" << editorId << ":Line 1" << "ENTER" << ":Line 2" << "F2", ""); - RUN("read" << "0", "Line 1\nLine 2"); - - RUN("keys" << "CTRL+N" << editorId << ":Line 3" << "ENTER" << ":Line 4" << "F2", ""); - RUN("read" << "0", "Line 3\nLine 4"); -} - -void Tests::editNotes() -{ - RUN("add" << "B" << "A", ""); - - RUN("config" << "editor" << "", "\n"); - RUN("keys" << "SHIFT+F2" << ":A Note" << "F2", ""); - RUN("read" << mimeText << "0" << mimeItemNotes << "0" << "F2", "A\nA Note"); - RUN("read" << mimeText << "1" << mimeItemNotes << "1" << "F2", "B\n"); - - RUN("keys" << "DOWN", ""); - - RUN("keys" << "SHIFT+F2" << ":B Note" << "F2", ""); - RUN("read" << mimeText << "1" << mimeItemNotes << "1" << "F2", "B\nB Note"); - RUN("read" << mimeText << "0" << mimeItemNotes << "0" << "F2", "A\nA Note"); -} - -void Tests::toggleClipboardMonitoring() -{ - const QByteArray data1 = generateData(); - TEST( m_test->setClipboard(data1) ); - RUN("clipboard", data1); - WAIT_ON_OUTPUT("read" << "0", data1); - - RUN("disable", ""); - RUN("monitoring", "false\n"); - WAIT_ON_OUTPUT("isClipboardMonitorRunning", "false\n"); - - const QByteArray data2 = generateData(); - TEST( m_test->setClipboard(data2) ); - RUN("clipboard", data2); - WAIT_ON_OUTPUT("read" << "0", data1); - - RUN("enable", ""); - RUN("monitoring", "true\n"); - WAIT_ON_OUTPUT("isClipboardMonitorRunning", "true\n"); - - const QByteArray data3 = generateData(); - TEST( m_test->setClipboard(data3) ); - RUN("clipboard", data3); - WAIT_ON_OUTPUT("read" << "0", data3); -} - -void Tests::clipboardToItem() -{ - TEST( m_test->setClipboard("TEXT1") ); - RUN("clipboard", "TEXT1"); - WAIT_ON_OUTPUT("read" << "0", "TEXT1"); - RUN("read" << "?" << "0", "text/plain\n"); - - TEST( m_test->setClipboard("DATA1", "DATA") ); - WAIT_ON_OUTPUT("clipboard" << "DATA", "DATA1"); - - // Unicode test. - const auto test = QString::fromUtf8(QByteArray("Zkouška s různými českými znaky!")); - const auto bytes = test.toUtf8(); - TEST( m_test->setClipboard(bytes) ); - RUN("clipboard", bytes); - WAIT_ON_OUTPUT("read" << "0", bytes); -} - -void Tests::itemToClipboard() -{ - RUN("add" << "TESTING2" << "TESTING1", ""); - RUN("read" << "0" << "1", "TESTING1\nTESTING2"); - - RUN("select" << "0", ""); - - WAIT_FOR_CLIPBOARD("TESTING1"); - RUN("clipboard", "TESTING1"); - - // select second item and move to top - RUN("config" << "move" << "true", "true\n"); - RUN("select" << "1", ""); - RUN("read" << "0" << "1", "TESTING2\nTESTING1"); - - WAIT_FOR_CLIPBOARD("TESTING2"); - RUN("clipboard", "TESTING2"); - - // select without moving - RUN("config" << "move" << "0", "false\n"); - RUN("select" << "1", ""); - RUN("read" << "0" << "1", "TESTING2\nTESTING1"); - - WAIT_FOR_CLIPBOARD("TESTING1"); - RUN("clipboard", "TESTING1"); -} - -void Tests::tabAdd() -{ - const QString tab = testTab(1); - const Args args = Args("tab") << tab << "separator" << " "; - - QVERIFY( !hasTab(tab) ); - RUN(args, ""); - RUN(args << "size", "0\n"); - RUN(args << "add" << "ghi" << "def" << "abc", ""); - QVERIFY( hasTab(tab) ); - - // Restart server. - TEST( m_test->stopServer() ); - TEST( m_test->startServer() ); - - QVERIFY( hasTab(tab) ); - - RUN(args << "size", "3\n"); - RUN(args << "read" << "0" << "1" << "2", "abc def ghi"); -} - -void Tests::tabRemove() -{ - const QString tab = testTab(1); - const Args args = Args("tab") << tab << "separator" << " "; - - RUN(args << "add" << "", ""); - QVERIFY( hasTab(tab) ); - RUN(Args() << "removetab" << tab, ""); - QVERIFY( !hasTab(tab) ); - - RUN_EXPECT_ERROR("removetab" << tab, CommandException); -} - -void Tests::tabIcon() -{ - const QString tab = testTab(1); - const QString icon = ":/images/icon"; - - RUN("tab" << tab << "add" << "", ""); - RUN("tabIcon" << tab, "\n"); - RUN("tabicon" << tab << icon, ""); - RUN("tabIcon" << tab, icon + "\n"); - RUN("tabIcon" << tab << "", ""); - RUN("tabicon" << tab, "\n"); -} - -void Tests::action() -{ - const Args args = Args("tab") << testTab(1); - const Args argsAction = Args(args) << "action"; - const QString action = QString::fromLatin1("copyq %1 %2").arg(args.join(" ")); - - // action with size - RUN(argsAction << action.arg("size") << "", ""); - WAIT_ON_OUTPUT(args << "size", "1\n"); - RUN(args << "read" << "0", "0\n"); - - // action with size - RUN(argsAction << action.arg("size") << "", ""); - WAIT_ON_OUTPUT(args << "size", "2\n"); - RUN(args << "read" << "0", "1\n"); - - // action with eval print - RUN(argsAction << action.arg("eval 'print(\"A,B,C\")'") << "", ""); - WAIT_ON_OUTPUT(args << "size", "3\n"); - RUN(args << "read" << "0", "A,B,C"); - - // action with read and comma separator for new items - RUN(argsAction << action.arg("read 0") << ",", ""); - WAIT_ON_OUTPUT(args << "size", "6\n"); - RUN(args << "read" << "0" << "1" << "2", "C\nB\nA"); -} - -void Tests::insertRemoveItems() -{ - const Args args = Args("tab") << testTab(1) << "separator" << ","; - - RUN(args << "add" << "ghi" << "abc", ""); - RUN(args << "insert" << "1" << "def", ""); - RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "abc,def,ghi,,"); - - RUN(args << "insert" << "0" << "012", ""); - RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "012,abc,def,ghi,"); - - RUN(args << "remove" << "0" << "2", ""); - RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "abc,ghi,,,"); - - QByteArray in("ABC"); - QCOMPARE( run(Args(args) << "insert" << "1" << "-", nullptr, nullptr, in), 0); - RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "abc,ABC,ghi,,"); -} - -void Tests::renameTab() -{ - const QString tab1 = testTab(1); - const QString tab2 = testTab(2); - - RUN("tab" << tab1 << "add" << "ghi" << "def" << "abc", ""); - - RUN("renametab" << tab1 << tab2, ""); - RUN("tab" << tab2 << "size", "3\n"); - RUN("tab" << tab2 << "read" << "0" << "1" << "2", "abc\ndef\nghi"); - QVERIFY( !hasTab(tab1) ); - - // Rename non-existing tab. - RUN_EXPECT_ERROR("renametab" << tab1 << tab2, CommandException); - - // Rename to same name. - RUN_EXPECT_ERROR("renametab" << tab2 << tab2, CommandException); - - // Rename to empty name. - RUN_EXPECT_ERROR("renametab" << tab2 << "", CommandException); - - // Rename to existing tab. - RUN_EXPECT_ERROR("renametab" << tab2 << clipboardTabName, CommandException); - - QVERIFY( !hasTab(tab1) ); - QVERIFY( hasTab(tab2) ); - - RUN("renametab" << tab2 << tab1, ""); - RUN("tab" << tab1 << "read" << "0" << "1" << "2", "abc\ndef\nghi"); - - QVERIFY( hasTab(tab1) ); - QVERIFY( !hasTab(tab2) ); -} - -void Tests::renameClipboardTab() -{ - const QString newClipboardTabName = clipboardTabName + QStringLiteral("2"); - RUN("config" << "tray_tab" << clipboardTabName, clipboardTabName + QStringLiteral("\n")); - const QString icon = ":/images/icon"; - RUN("tabicon" << clipboardTabName << icon, ""); - - RUN("renametab" << clipboardTabName << newClipboardTabName, ""); - RUN("tab", newClipboardTabName + "\n"); - RUN("config" << "clipboard_tab", newClipboardTabName + QStringLiteral("\n")); - RUN("config" << "tray_tab", newClipboardTabName + QStringLiteral("\n")); - RUN("tabicon" << newClipboardTabName, icon + QStringLiteral("\n")); - - TEST( m_test->setClipboard("test1") ); - WAIT_ON_OUTPUT("tab" << newClipboardTabName << "read" << "0", "test1"); - RUN("tab", newClipboardTabName + "\n"); - - WAIT_ON_OUTPUT("read" << "0", "test1"); - RUN("tab", newClipboardTabName + "\n"); -} - -void Tests::importExportTab() -{ - const QString tab = testTab(1); - const Args args = Args("tab") << tab << "separator" << " "; - - RUN(args << "add" << "ghi" << "def" << "abc", ""); - - TemporaryFile tmp; - RUN(args << "exporttab" << tmp.fileName(), ""); - - RUN("removetab" << tab, ""); - QVERIFY( !hasTab(tab) ); - - RUN(args << "importtab" << tmp.fileName(), ""); - RUN(args << "read" << "0" << "1" << "2", "abc def ghi"); - RUN(args << "size", "3\n"); - - // Export with relative path. - TemporaryFile tmp2; - - // Change back to original working directory once finished. - struct CurrentDirectoryGuard { - CurrentDirectoryGuard() : oldDir(QDir::currentPath()) {} - ~CurrentDirectoryGuard() { QDir::setCurrent(oldDir); } - const QString oldDir; - } currentDirectoryGuard; - - QDir::setCurrent( QDir::cleanPath(tmp2.fileName() + "/..") ); - - const QString fileName = QFileInfo( tmp2.fileName() ).fileName(); - - RUN(args << "add" << "012", ""); - RUN(args << "exporttab" << fileName, ""); - - RUN("removetab" << tab, ""); - QVERIFY( !hasTab(tab) ); - - RUN(args << "importtab" << fileName, ""); - RUN(args << "read" << "0" << "1" << "2" << "3", "012 abc def ghi"); -} - -void Tests::removeAllFoundItems() -{ - auto args = Args("add"); - for (int i = 0; i < 50; ++i) { - args << QString::fromLatin1("a%1").arg(i); - args << QString::fromLatin1("b%1").arg(i); - } - - RUN(args, ""); - RUN("size", "100\n"); - - RUN("filter" << "a", ""); - RUN("keys" << "CTRL+A" << m_test->shortcutToRemove(), ""); - - RUN("size", "50\n"); - RUN("read" << "49" << "48" << "47", "b0\nb1\nb2"); - RUN("read" << "0" << "1" << "2", "b49\nb48\nb47"); -} - -void Tests::nextPrevious() -{ - const QString tab = testTab(1); - const Args args = Args("tab") << tab; - RUN(args << "add" << "C" << "B" << "A", ""); - RUN("setCurrentTab" << tab, ""); - - RUN(args << "next", ""); - WAIT_FOR_CLIPBOARD("B"); - - RUN(args << "next", ""); - WAIT_FOR_CLIPBOARD("C"); - - RUN(args << "next", ""); - WAIT_FOR_CLIPBOARD("C"); - - RUN(args << "previous", ""); - WAIT_FOR_CLIPBOARD("B"); - - RUN(args << "previous", ""); - WAIT_FOR_CLIPBOARD("A"); - - RUN(args << "previous", ""); - WAIT_FOR_CLIPBOARD("A"); -} - -void Tests::externalEditor() -{ - const QString tab = testTab(1); - const Args args = Args("tab") << tab; - const QString editorTab = testTab(2); - const Args editorArgs = Args("tab") << editorTab; - const Args editorFileNameArgs = Args(editorArgs) << "read" << "0"; - const Args editorEndArgs = Args(editorArgs) << "remove" << "0"; - - // Set editor command which add file name to edit to special editor tab. - // The command finishes when the special tab is emptied by this test. - // File to edit is removed by application when the command finished. - const auto cmd = QString( - R"(copyq tab "%1" eval "add(arguments[1]); while(length()) sleep(100);" --)" - ).arg(editorTab) + " %1"; - RUN("config" << "editor" << cmd, cmd + "\n"); - - // Set clipboard. - const QByteArray data1 = generateData(); - TEST( m_test->setClipboard(data1) ); - RUN("clipboard", data1); - -#define EDIT(DATA1, DATA2) \ - do { \ - WAIT_ON_OUTPUT(editorArgs << "size", "1\n"); \ - QByteArray out; \ - QByteArray err; \ - run(editorFileNameArgs, &out, &err); \ - QVERIFY2( testStderr(err), err ); \ - QFile file(out); \ - QVERIFY( file.exists() ); \ - QVERIFY( file.open(QIODevice::ReadWrite) ); \ - QVERIFY( file.readAll() == (DATA1) ); \ - file.write(DATA2); \ - file.close(); \ - RUN(editorEndArgs, ""); \ - waitWhileFileExists(file); \ - } while(false) - - // Edit clipboard. - RUN("edit" << "-1", ""); - const QByteArray data2 = generateData(); - EDIT(data1, data2); - - // Check if clipboard changed. - WAIT_ON_OUTPUT("read" << "0", data1 + data2); - WAIT_FOR_CLIPBOARD(data1 + data2); - - // Edit existing item. - const QString text = - "Some text to edit,\n" - "with second line!\n" - + generateData(); - RUN(args << "add" << text, ""); - - // Modify first item. - RUN(args << "edit" << "0", ""); - const QByteArray data3 = generateData(); - EDIT(text.toUtf8(), data3); - - // Check first item. - WAIT_ON_OUTPUT(args << "read" << "0", text.toUtf8() + data3); - - // Edit new item. - RUN(args << "edit", ""); - const QByteArray data4 = generateData(); - EDIT(QByteArray(), data4); - - // Check first item. - WAIT_ON_OUTPUT(args << "read" << "0", data4); - -#undef EDIT -} - -void Tests::nextPreviousTab() -{ - const auto tab1 = testTab(1); - RUN("tab" << tab1 << "size", "0\n"); - - const auto tab2 = testTab(2); - RUN("tab" << tab2 << "size", "0\n"); - - using KeyPair = QPair; - const QList keyPairs = QList() - << KeyPair(keyNameFor(QKeySequence::NextChild), keyNameFor(QKeySequence::PreviousChild)) - << KeyPair("RIGHT", "LEFT"); - - for (const auto &keyPair : keyPairs) { - for (const auto &optionValue : {"false", "true"}) { - RUN("config" << "tab_tree" << optionValue, QString(optionValue) + "\n"); - - RUN("keys" << keyPair.first, ""); - RUN("testSelected", tab1 + "\n"); - RUN("keys" << keyPair.first, ""); - RUN("testSelected", tab2 + "\n"); - RUN("keys" << keyPair.first, ""); - - RUN("keys" << keyPair.second, ""); - RUN("testSelected", tab2 + "\n"); - RUN("keys" << keyPair.second, ""); - RUN("testSelected", tab1 + "\n"); - RUN("keys" << keyPair.second, ""); - } - } -} - -void Tests::itemPreview() -{ - const auto tab1 = testTab(1); - RUN("tab" << tab1 << "add" << "def" << "abc", ""); - RUN("setCurrentTab" << tab1, ""); - - RUN("preview", "false\n"); - RUN("keys" << clipboardBrowserId << "F7", ""); - RUN("preview", "true\n"); - - RUN("keys" << clipboardBrowserId << "TAB" << itemPreviewId, ""); - RUN("keys" << itemPreviewId << "HOME", ""); - RUN("keys" << itemPreviewId << "RIGHT", ""); - RUN("keys" << itemPreviewId << "SHIFT+RIGHT", ""); - RUN("keys" << itemPreviewId << keyNameFor(QKeySequence::Copy), ""); - WAIT_FOR_CLIPBOARD("b"); - - RUN("keys" << itemPreviewId << "F7" << clipboardBrowserId, ""); - - RUN("preview" << "true", "false\n"); - RUN("preview" << "false", "true\n"); - RUN("preview" << "1", "false\n"); - RUN("preview" << "0", "true\n"); - RUN("preview(true)", "false\n"); - RUN("preview(false)", "true\n"); - RUN("preview", "false\n"); -} - -void Tests::openAndSavePreferences() -{ -#ifdef Q_OS_MAC - SKIP("Can't focus configuration checkboxes on OS X"); -#endif - - RUN("config" << "check_clipboard" << "false", "false\n"); - - // Open preferences dialog. - RUN("keys" << "Ctrl+P" << configurationDialogId, ""); - - // Focus and set wrap text option. - // This behavior could differ on some systems and in other languages. - RUN("keys" << configurationDialogId << "ALT+1", ""); - // Wait for any checkbox animation or delay - waitFor(1000); - RUN("keys" << configurationDialogId << "ENTER" << clipboardBrowserId, ""); - WAIT_ON_OUTPUT("config" << "check_clipboard", "true\n"); -} - -void Tests::pasteFromMainWindow() -{ - RUN("config" - << "activate_closes" << "true" - << "activate_focuses" << "true" - << "activate_pastes" << "true" - , - "activate_closes=true\n" - "activate_focuses=true\n" - "activate_pastes=true\n" - ); - - RUN("add" << "TEST", ""); - RUN("hide", ""); - runMultiple( - [&]() { RUN(WITH_TIMEOUT "dialog('text')", "TEST\n"); }, - [&]() { - RUN("keys" << "focus::QLineEdit in :QDialog", ""); - RUN("show", ""); - RUN("keys" << clipboardBrowserId << "ENTER", ""); - - WAIT_FOR_CLIPBOARD("TEST"); - waitFor(waitMsPasteClipboard); - - RUN("keys" << "focus::QLineEdit in :QDialog" << "ENTER", ""); - } - ); -} - -void Tests::tray() -{ - RUN("add" << "A", ""); - RUN("keys" << clipboardBrowserId, ""); - RUN("menu", ""); - ACTIVATE_MENU_ITEM(trayMenuId, clipboardBrowserId, "A"); -} - -void Tests::menu() -{ - const auto tab = testTab(1); - - RUN("tab" << tab << "add" << "D" << "C" << "B" << "A", ""); - - RUN("keys" << clipboardBrowserId, ""); - RUN("menu" << tab, ""); - ACTIVATE_MENU_ITEM(menuId, clipboardBrowserId, "A"); - - // Show menu with 2 items from the tab and select last one. - RUN("keys" << clipboardBrowserId, ""); - RUN("menu" << tab << "2", ""); - RUN("keys" << menuId << "END", ""); - ACTIVATE_MENU_ITEM(menuId, clipboardBrowserId, "B"); - -#ifdef Q_OS_MAC - SKIP("Number keys don't seem to work in the tray menu on macOS."); -#endif - - // Select item by row number. - RUN("tab" << tab << "add(3,2,1,0)", ""); - RUN("menu" << tab, ""); - RUN("keys" << menuId << "3" << clipboardBrowserId, ""); - WAIT_FOR_CLIPBOARD("2"); - - // Select item by index. - RUN("config" << "row_index_from_one" << "false", "false\n"); - RUN("tab" << tab << "add(3,2,1,0)", ""); - RUN("menu" << tab, ""); - RUN("keys" << menuId << "3" << clipboardBrowserId, ""); - WAIT_FOR_CLIPBOARD("3"); -} - -void Tests::traySearch() -{ - RUN("add" << "C" << "B" << "A", ""); - - RUN("keys" << clipboardBrowserId, ""); - RUN("menu", ""); - RUN("keys" << trayMenuId << "B", ""); - ACTIVATE_MENU_ITEM(trayMenuId, clipboardBrowserId, "B"); -} - -void Tests::trayPaste() -{ - RUN("config" << "tray_tab_is_current" << "false", "false\n"); - - const auto tab1 = testTab(1); - RUN("setCurrentTab" << tab1, ""); - RUN("keys" - << clipboardBrowserId << "CTRL+N" - << editorId << ":NEW ", ""); - - RUN("add" << "TEST", ""); - RUN("keys" << editorId, ""); - RUN("menu", ""); - ACTIVATE_MENU_ITEM(trayMenuId, editorId, "TEST"); - waitFor(waitMsPasteClipboard); - - RUN("keys" << editorId << "F2", ""); - RUN("tab" << tab1 << "read" << "0", "NEW TEST"); - - RUN("keys" - << clipboardBrowserId << "CTRL+N" - << editorId << ":NEW ", ""); - - RUN("config" << "tray_item_paste" << "false", "false\n"); - RUN("add" << "TEST2", ""); - RUN("keys" << editorId, ""); - RUN("menu", ""); - ACTIVATE_MENU_ITEM(trayMenuId, editorId, "TEST2"); - - RUN("keys" << editorId << "F2", ""); - RUN("tab" << tab1 << "read" << "0", "NEW "); -} - -void Tests::pasteNext() -{ - const auto tab1 = testTab(1); - RUN("setCurrentTab" << tab1, ""); - RUN("keys" - << clipboardBrowserId << "CTRL+N" - << editorId << ":NEW ", ""); - - const auto tab2 = testTab(2); - RUN("tab" << tab2 << "add" << "test3" << "test2" << "test1", ""); - RUN("tab" << tab2 << "next(); paste(); next()", ""); - waitFor(waitMsPasteClipboard); - - RUN("keys" << editorId, ""); - WAIT_FOR_CLIPBOARD("test3"); - RUN("keys" << editorId << "F2", ""); - RUN("tab" << tab1 << "read" << "0", "NEW test2"); -} - -void Tests::configTrayTab() -{ - const auto tab1 = testTab(1); - RUN("tab" << tab1 << "add" << "A", ""); - - const auto tab2 = testTab(2); - RUN("tab" << tab2 << "add" << "B", ""); - - RUN("config" << "tray_tab_is_current" << "false", "false\n"); - - RUN("config" << "tray_item_paste" << "false", "false\n"); - - RUN("config" << "tray_tab" << tab1, tab1 + "\n"); - - RUN("keys" << clipboardBrowserId, ""); - RUN("menu", ""); - ACTIVATE_MENU_ITEM(trayMenuId, clipboardBrowserId, "A"); - - RUN("config" << "tray_tab" << tab2, tab2 + "\n"); - - RUN("keys" << clipboardBrowserId, ""); - RUN("menu", ""); - ACTIVATE_MENU_ITEM(trayMenuId, clipboardBrowserId, "B"); -} - -void Tests::configMove() -{ - SKIP_ON_ENV("COPYQ_TESTS_SKIP_CONFIG_MOVE"); - - RUN("add" << "B" << "A", ""); - - RUN("config" << "tray_item_paste" << "false", "false\n"); - - RUN("config" << "move" << "true", "true\n"); - - RUN("keys" << clipboardBrowserId, ""); - RUN("menu", ""); - RUN("keys" << trayMenuId << "DOWN", ""); - ACTIVATE_MENU_ITEM(trayMenuId, clipboardBrowserId, "B"); - RUN("read" << "0" << "1", "B\nA"); - - RUN("config" << "move" << "false", "false\n"); - - RUN("keys" << clipboardBrowserId, ""); - RUN("menu", ""); - RUN("keys" << trayMenuId << "DOWN", ""); - ACTIVATE_MENU_ITEM(trayMenuId, clipboardBrowserId, "A"); - RUN("read" << "0" << "1", "B\nA"); -} - -void Tests::configTrayTabIsCurrent() -{ - const auto tab1 = testTab(1); - RUN("tab" << tab1 << "add" << "A", ""); - - const auto tab2 = testTab(2); - RUN("tab" << tab2 << "add" << "B", ""); - - RUN("config" << "tray_tab_is_current" << "true", "true\n"); - - RUN("setCurrentTab" << tab1, ""); - RUN("keys" << clipboardBrowserId, ""); - RUN("menu", ""); - ACTIVATE_MENU_ITEM(trayMenuId, clipboardBrowserId, "A"); - - RUN("setCurrentTab" << tab2, ""); - RUN("keys" << clipboardBrowserId, ""); - RUN("menu", ""); - ACTIVATE_MENU_ITEM(trayMenuId, clipboardBrowserId, "B"); -} - -void Tests::configAutostart() -{ - if ( !platformNativeInterface()->canAutostart() ) { - SKIP("Autostart is unsupported on this platform"); - return; - } - - RUN("config" << "autostart" << "true", "true\n"); - RUN("config" << "autostart", "true\n"); - RUN("config" << "autostart" << "false", "false\n"); - RUN("config" << "autostart", "false\n"); -} - -void Tests::configPathEnvVariable() -{ - const auto path = QDir::home().absoluteFilePath("copyq-settings"); - const auto environment = QStringList("COPYQ_SETTINGS_PATH=" + path); - - QByteArray out; - QByteArray err; - run(Args() << "info" << "config", &out, &err, QByteArray(), environment); - QVERIFY2( testStderr(err), err ); - - const auto expectedOut = path.toUtf8(); - QCOMPARE( out.left(expectedOut.size()), expectedOut ); -} - -void Tests::itemDataPathEnvVariable() -{ - const auto path = QDir::home().absoluteFilePath("copyq-data"); - const auto environment = QStringList("COPYQ_ITEM_DATA_PATH=" + path); - - QByteArray out; - QByteArray err; - run(Args() << "info" << "data", &out, &err, QByteArray(), environment); - QVERIFY2( testStderr(err), err ); - - const auto expectedOut = path.toUtf8(); - QCOMPARE( out.left(expectedOut.size()), expectedOut ); -} - -void Tests::configTabs() -{ - const QString sep = QStringLiteral("\n"); - RUN("config" << "tabs", clipboardTabName + sep); - - const QString tab1 = testTab(1); - RUN("tab" << tab1 << "add" << "test", ""); - RUN("config" << "tabs", clipboardTabName + sep + tab1 + sep); - - const QString tab2 = testTab(2); - RUN(QString("config('tabs', ['%1', '%2'])").arg(clipboardTabName, tab2), clipboardTabName + sep + tab2 + sep); - RUN("config" << "tabs", clipboardTabName + sep + tab2 + sep + tab1 + sep); - RUN("tab", clipboardTabName + sep + tab2 + sep + tab1 + sep); - - RUN(QString("config('tabs', ['%1', '%2'])").arg(tab1, tab2), tab1 + sep + tab2 + sep); - RUN("config" << "tabs", tab1 + sep + tab2 + sep + clipboardTabName + sep); - RUN("tab", tab1 + sep + tab2 + sep + clipboardTabName + sep); -} - -void Tests::selectedItems() -{ - const auto tab1 = testTab(1); - const Args args = Args("tab") << tab1; - - RUN("selectedTab", "CLIPBOARD\n"); - RUN("selectedItems", ""); - - RUN(args << "add" << "D" << "C" << "B" << "A", ""); - RUN(args << "setCurrentTab" << tab1 << "selectItems" << "1" << "2", "true\n"); - RUN("selectedTab", tab1 + "\n"); - RUN("selectedItems", "1\n2\n"); - RUN("currentItem", "2\n"); - - const auto print = R"( - print([selectedTab(), "c:" + currentItem(), "s:" + selectedItems()]); - print("\\n") - )"; - - // Selection stays consistent when moving items - RUN(print << "move(0)" << print, tab1 + ",c:2,s:1,2\n" + tab1 + ",c:1,s:0,1\n"); - RUN(print, tab1 + ",c:1,s:0,1\n"); - - RUN(print << "keys('HOME', 'CTRL+DOWN')" << print, tab1 + ",c:1,s:0,1\n" + tab1 + ",c:0,s:1,0\n"); - RUN(print, tab1 + ",c:1,s:1\n"); - - // Selection stays consistent when removing items - RUN(args << "setCurrentTab" << tab1 << "selectItems" << "1" << "2" << "3", "true\n"); - RUN(print << "remove(2)" << print, tab1 + ",c:3,s:1,2,3\n" + tab1 + ",c:2,s:1,-1,2\n"); - RUN(print, tab1 + ",c:2,s:1,2\n"); - - // Renaming tab invalidates selection and all items because the tab - // underlying data needs to be loaded again using plugins. - const QString tab2 = testTab(2); - const auto rename = QString("renameTab('%1', '%2')").arg(tab1, tab2); - RUN(print << rename << print, tab1 + ",c:2,s:1,2\n" + tab1 + ",c:-1,s:-1,-1\n"); - - RUN(print, tab2 + ",c:0,s:0\n"); -} - -void Tests::shortcutCommand() -{ - RUN("setCommands([{name: 'test', inMenu: true, shortcuts: ['Ctrl+F1'], cmd: 'copyq add OK'}])", ""); - RUN("keys" << "CTRL+F1", ""); - WAIT_ON_OUTPUT("read" << "0", "OK"); -} - -void Tests::shortcutCommandOverrideEnter() -{ - RUN("setCommands([{name: 'test', inMenu: true, shortcuts: ['Enter'], cmd: 'copyq add OK'}])", ""); - RUN("keys" << "ENTER" << "ENTER", ""); - WAIT_ON_OUTPUT("read" << "0" << "1", "OK\nOK"); -} - -void Tests::shortcutCommandMatchInput() -{ - // Activate only one of the two actions depending on input MIME format. - const auto script = R"( - function cmd(name) { - var format = 'application/x-copyq-' + name - return { - name: name, - inMenu: true, - shortcuts: ['Ctrl+F1'], - input: format, - cmd: 'copyq add ' + name - } - } - setCommands([ cmd('test1'), cmd('test2') ]) - )"; - RUN(script, ""); - - RUN("write" << "application/x-copyq-test1" << "", ""); - RUN("keys" << "CTRL+F1", ""); - WAIT_ON_OUTPUT("read" << "0", "test1"); - RUN("tab" << QString(clipboardTabName) << "size", "2\n"); - - RUN("write" << "application/x-copyq-test2" << "", ""); - RUN("keys" << "CTRL+F1", ""); - WAIT_ON_OUTPUT("read" << "0", "test2"); - RUN("tab" << QString(clipboardTabName) << "size", "4\n"); -} - -void Tests::shortcutCommandMatchCmd() -{ - const auto tab = testTab(1); - const Args args = Args("tab") << tab; - - // Activate only one of the three actions depending on exit code of command which matches input MIME format. - const auto script = R"( - function cmd(name) { - var format = 'application/x-copyq-' + name - return { - name: name, - inMenu: true, - shortcuts: ['Ctrl+F1'], - matchCmd: 'copyq: str(data("' + format + '")) || fail()', - cmd: 'copyq tab )" + tab + R"( add ' + name - } - } - setCommands([ cmd('test1'), cmd('test2') ]) - )"; - RUN(script, ""); - - RUN("show" << tab, ""); - - RUN(args << "write" << "application/x-copyq-test1" << "1", ""); - WAIT_ON_OUTPUT(args << "keys('Ctrl+F1'); read(0)", "test1"); - - RUN(args << "write" << "application/x-copyq-test2" << "2", ""); - WAIT_ON_OUTPUT(args << "keys('Ctrl+F1'); read(0)", "test2"); -} - -void Tests::shortcutCommandSelectedItemData() -{ - const auto tab1 = testTab(1); - const auto script = R"( - setCommands([{ - name: 'Move Second Selected Item to Other Tab', - inMenu: true, - shortcuts: ['Ctrl+F1'], - output: 'text/plain', - outputTab: ')" + tab1 + R"(', - cmd: 'copyq: selectedItemData(1)["text/plain"]' - }]) - )"; - RUN(script, ""); - - RUN("add" << "C" << "B" << "A", ""); - RUN("selectItems" << "1" << "2", "true\n"); - RUN("keys" << "CTRL+F1", ""); - WAIT_ON_OUTPUT("tab" << tab1 << "read" << "0", "C"); -} - -void Tests::shortcutCommandSetSelectedItemData() -{ - const auto script = R"( - setCommands([{ - name: 'Set Data for Second Selected Item', - inMenu: true, - shortcuts: ['Ctrl+F1'], - cmd: 'copyq: setSelectedItemData(1, {"text/plain": "X", "DATA": "TEST"})' - }]) - )"; - RUN(script, ""); - - RUN("add" << "C" << "B" << "A", ""); - RUN("selectItems" << "1" << "2", "true\n"); - RUN("keys" << "CTRL+F1", ""); - WAIT_ON_OUTPUT("read" << "2", "X"); - RUN("read" << "DATA" << "2", "TEST"); -} - -void Tests::shortcutCommandSelectedItemsData() -{ - const auto tab1 = testTab(1); - const auto script = R"( - setCommands([{ - name: 'Concatenate Selected Items to Other Tab', - inMenu: true, - shortcuts: ['Ctrl+F1'], - output: 'text/plain', - outputTab: ')" + tab1 + R"(', - cmd: 'copyq: d = selectedItemsData();' - + 'for (i in d) { print(d[i][mimeText]); print(",") }' - }]) - )"; - RUN(script, ""); - - RUN("add" << "C" << "B" << "A", ""); - RUN("selectItems" << "1" << "2", "true\n"); - RUN("keys" << "CTRL+F1", ""); - WAIT_ON_OUTPUT("tab" << tab1 << "read" << "0", "B,C,"); -} - -void Tests::shortcutCommandSetSelectedItemsData() -{ - const auto script = R"( - setCommands([{ - name: 'Set Data for Second Selected Item', - inMenu: true, - shortcuts: ['Ctrl+F1'], - cmd: 'copyq: setSelectedItemsData([{"text/plain": "X"}, {"text/plain": "Y"}])' - }]) - )"; - RUN(script, ""); - - RUN("add" << "C" << "B" << "A", ""); - RUN("selectItems" << "1" << "2", "true\n"); - RUN("keys" << "CTRL+F1", ""); - WAIT_ON_OUTPUT("read" << "0" << "1" << "2", "A\nX\nY"); -} - -void Tests::shortcutCommandSelectedAndCurrent() -{ - const auto script = R"( - setCommands([{ - name: 'Set Data for Second Selected Item', - inMenu: true, - shortcuts: ['Ctrl+F1'], - output: 'text/plain', - cmd: 'copyq: print(selectedItems() + "|" + currentItem() + "|" + selectedTab())' - }]) - )"; - RUN(script, ""); - - const auto tab1 = testTab(1); - RUN("tab" << tab1 << "add" << "C" << "B" << "A", ""); - - RUN("tab" << tab1 << "setCurrentTab" << tab1 << "selectItems" << "1" << "2", "true\n"); - RUN("keys" << "CTRL+F1", ""); - WAIT_ON_OUTPUT("tab" << tab1 << "read(0)", "1,2|2|" + tab1.toUtf8()); -} - -void Tests::shortcutCommandMoveSelected() -{ - const QString script = R"( - setCommands([{ - name: 'Move Selected', - inMenu: true, - shortcuts: ['Ctrl+F1'], - output: 'text/plain', - cmd: 'copyq: move(%1); settings("done", 1)' - }]) - )"; - RUN(script.arg(1), ""); - - const Args args = Args("tab") << testTab(1) << "separator" << ","; - RUN("setCurrentTab" << testTab(1), ""); - RUN(args << "add" << "4" << "3" << "2" << "1", ""); - -#define MOVE_SELECTED(EXPECTED_ITEMS) \ - RUN("settings" << "done" << "0" << "keys" << "CTRL+F1", ""); \ - WAIT_ON_OUTPUT("settings" << "done", "1\n"); \ - RUN(args << "read(0,1,2,3,4)", EXPECTED_ITEMS) - - RUN(args << "selectItems" << "1" << "2", "true\n"); - MOVE_SELECTED("1,2,3,4,"); - - RUN(args << "selectItems" << "2" << "3", "true\n"); - MOVE_SELECTED("1,3,4,2,"); - - RUN(script.arg(5), ""); - MOVE_SELECTED("1,3,4,2,"); - - RUN(script.arg(-1), ""); - MOVE_SELECTED("1,3,4,2,"); - - RUN(script.arg(4), ""); - MOVE_SELECTED("1,2,3,4,"); - - RUN(script.arg(0), ""); - MOVE_SELECTED("3,4,1,2,"); - -#undef MOVE_SELECTED -} - -void Tests::automaticCommandIgnore() -{ - const auto script = R"( - setCommands([ - { automatic: true, cmd: 'copyq ignore; copyq add OK' }, - { automatic: true, cmd: 'copyq add "SHOULD NOT BE EXECUTED"' } - ]) - )"; - RUN(script, ""); - WAIT_ON_OUTPUT("commands().length", "2\n"); - - TEST( m_test->setClipboard("SHOULD BE IGNORED 1") ); - WAIT_ON_OUTPUT("read" << "0", "OK"); - RUN("separator" << "," << "read" << "0" << "1" << "2", "OK,,"); - RUN("size", "1\n"); - - TEST( m_test->setClipboard("SHOULD BE IGNORED 2") ); - WAIT_ON_OUTPUT("size", "2\n"); - - RUN("separator" << "," << "read" << "0" << "1" << "2", "OK,OK,"); -} - -void Tests::automaticCommandRemove() -{ - const auto script = R"( - setCommands([ - { automatic: true, remove: true, cmd: 'copyq add OK' }, - { automatic: true, cmd: 'copyq add "SHOULD NOT BE EXECUTED"' } - ]) - )"; - RUN(script, ""); - WAIT_ON_OUTPUT("commands().length", "2\n"); - - TEST( m_test->setClipboard("SHOULD BE IGNORED 1") ); - WAIT_ON_OUTPUT("read" << "0", "OK"); - - TEST( m_test->setClipboard("SHOULD BE IGNORED 2") ); - WAIT_ON_OUTPUT("size", "2\n"); - - RUN("separator" << "," << "read" << "0" << "1" << "2", "OK,OK,"); -} - -void Tests::automaticCommandInput() -{ - const auto script = R"( - setCommands([ - { automatic: true, input: 'DATA', cmd: 'copyq: setData("DATA", "???")' }, - { automatic: true, input: 'text/plain', cmd: 'copyq: setData("text/plain", "OK")' }, - ]) - )"; - RUN(script, ""); - WAIT_ON_OUTPUT("commands().length", "2\n"); - - TEST( m_test->setClipboard("SHOULD BE CHANGED") ); - WAIT_ON_OUTPUT("read" << "0", "OK"); - RUN("read" << "DATA" << "0", ""); -} - -void Tests::automaticCommandRegExp() -{ - const auto script = R"( - setCommands([ - { automatic: true, re: 'SHOULD BE (CHANGED)$', cmd: 'copyq: setData(mimeText, arguments[1])' }, - { automatic: true, cmd: 'copyq: setData("DATA", "DONE")' }, - ]) - )"; - RUN(script, ""); - WAIT_ON_OUTPUT("commands().length", "2\n"); - - TEST( m_test->setClipboard("SHOULD BE CHANGED") ); - WAIT_ON_OUTPUT("read" << "DATA" << "0", "DONE"); - RUN("read" << "0", "CHANGED"); - RUN("remove" << "0", ""); - - TEST( m_test->setClipboard("SHOULD NOT BE CHANGED") ); - WAIT_ON_OUTPUT("read" << "DATA" << "0", "DONE"); - RUN("read" << "0", "SHOULD NOT BE CHANGED"); -} - -void Tests::automaticCommandSetData() -{ - const auto script = R"( - setCommands([{automatic: true, cmd: 'copyq: setData("text/plain", "OK")'}]) - )"; - RUN(script, ""); - WAIT_ON_OUTPUT("commands().length", "1\n"); - - TEST( m_test->setClipboard("SHOULD BE CHANGED") ); - WAIT_ON_OUTPUT("read" << "0", "OK"); -} - -void Tests::automaticCommandOutputTab() -{ - const auto tab1 = testTab(1); - const auto script = R"( - var tab1 = ')" + tab1 + R"(' - setCommands([{automatic: true, cmd: 'copyq: setData(mimeOutputTab, "' + tab1 + '")'}]) - )"; - RUN(script, ""); - WAIT_ON_OUTPUT("commands().length", "1\n"); - - TEST( m_test->setClipboard("TEST") ); - WAIT_ON_OUTPUT("tab" << tab1 << "read" << "0", "TEST"); - RUN("tab" << QString(clipboardTabName) << "size", "0\n"); -} - -void Tests::automaticCommandNoOutputTab() -{ - const auto script = R"( - setCommands([{automatic: true, cmd: 'copyq: removeData(mimeOutputTab)'}]) - )"; - RUN(script, ""); - WAIT_ON_OUTPUT("commands().length", "1\n"); - - TEST( m_test->setClipboard("TEST") ); - waitFor(1000); - RUN("tab" << QString(clipboardTabName) << "size", "0\n"); -} - -void Tests::automaticCommandChaining() -{ - const auto script = R"( - setCommands([ - {automatic: true, cmd: 'copyq: setData(mimeText, 1)'}, - {automatic: true, cmd: 'copyq: setData(mimeText, str(data(mimeText)) + 2)'}, - {automatic: true, cmd: 'copyq: setData(mimeText, str(data(mimeText)) + 3)'} - ]) - )"; - RUN(script, ""); - WAIT_ON_OUTPUT("commands().length", "3\n"); - - TEST( m_test->setClipboard("TEST") ); - WAIT_ON_OUTPUT("read" << "0", "123"); -} - -void Tests::automaticCommandCopyToTab() -{ - const auto tab1 = testTab(1); - const auto script = R"( - setCommands([{automatic: true, tab: ')" + tab1 + R"('}]) - )"; - RUN(script, ""); - WAIT_ON_OUTPUT("commands().length", "1\n"); - - TEST( m_test->setClipboard("TEST") ); - WAIT_ON_OUTPUT("tab" << QString(clipboardTabName) << "read" << "0", "TEST"); - RUN("tab" << tab1 << "read" << "0", "TEST"); -} - -void Tests::automaticCommandStoreSpecialFormat() -{ - const auto script = R"( - setCommands([ - { automatic: true, name: 'CMD1', input: 'test-format' } - ]) - )"; - RUN(script, ""); - WAIT_ON_OUTPUT("commands().length", "1\n"); - - TEST( m_test->setClipboard("DATA", "test-format") ); - WAIT_ON_OUTPUT("separator" << "," << "read" << "test-format" << "0" << "1", "DATA,"); -} - -void Tests::automaticCommandIgnoreSpecialFormat() -{ - const auto script = R"( - setCommands([ - { automatic: true, name: 'CMD1', cmd: 'copyq add CMD1', input: 'test-format', remove: true }, - { automatic: true, name: 'CMD2', cmd: 'copyq add CMD2' }, - { automatic: true, name: 'CMD3', cmd: 'copyq add CMD3', input: 'test-format' } - ]) - )"; - RUN(script, ""); - WAIT_ON_OUTPUT("commands().length", "3\n"); - - TEST( m_test->setClipboard("SHOULD BE IGNORED", "test-format") ); - WAIT_ON_OUTPUT("separator" << "," << "read" << "0" << "1", "CMD1,"); - - TEST( m_test->setClipboard("SHOULD NOT BE IGNORED") ); - WAIT_ON_OUTPUT("separator" << "," << "read" << "0" << "1" << "2" << "3", "SHOULD NOT BE IGNORED,CMD2,CMD1,"); -} - -void Tests::globalCommandInMenu() -{ - const auto script = R"( - setCommands([ - { isGlobalShortcut: true, name: 'test', cmd: 'copyq add test' }, - ]) - )"; - RUN(script, ""); - WAIT_ON_OUTPUT("commands().length", "1\n"); - RUN("menu", ""); - RUN("keys" << trayMenuId << "DOWN" << "ENTER", ""); - RUN("keys" << clipboardBrowserId, ""); - WAIT_ON_OUTPUT("read(0)", "test"); - - RUN("setCommands([])", ""); - WAIT_ON_OUTPUT("commands().length", "0\n"); - - // Test sub-menus - const auto script2 = R"( - setCommands([ - { isGlobalShortcut: true, name: 'test|test1|test2', cmd: 'copyq add test2' }, - ]) - )"; - RUN(script2, ""); - WAIT_ON_OUTPUT("commands().length", "1\n"); - RUN("menu", ""); - RUN("keys" << trayMenuId << "DOWN" << "DOWN" << "ENTER", ""); - waitFor(100); - RUN("keys" << trayMenuId << "ENTER", ""); - waitFor(100); - RUN("keys" << trayMenuId << "ENTER", ""); - RUN("keys" << clipboardBrowserId, ""); - WAIT_ON_OUTPUT("read(0)", "test2"); -} - -void Tests::scriptCommandLoaded() -{ - const auto script = R"( - setCommands([{ - isScript: true, - cmd: 'add("LOADED")' - }]) - )"; - RUN(script, ""); - RUN("read(0)", "LOADED"); -} - -void Tests::scriptCommandAddFunction() -{ - const auto script = R"( - setCommands([{ - isScript: true, - cmd: 'global.test = function() { return "TEST"; }' - }]) - )"; - RUN(script, ""); - RUN("test", "TEST\n"); -} - -void Tests::scriptCommandOverrideFunction() -{ - const auto script = R"( - setCommands([{ - isScript: true, - cmd: 'popup = function(msg) { return msg; }' - }]) - )"; - RUN(script, ""); - RUN("popup" << "test" << "xxx", "test"); -} - -void Tests::scriptCommandEnhanceFunction() -{ - const auto script = R"( - setCommands([ - { - isScript: true, - cmd: 'var popup_ = popup; global.popup = function(msg) { popup_(msg); return msg + 1; }' - }, - { - isScript: true, - cmd: 'var popup_ = popup; global.popup = function(msg) { return popup_(msg) + msg + 2; }' - }, - ]) - )"; - RUN(script, ""); - RUN("popup" << "test", "test1test2\n"); -} - -void Tests::scriptCommandEndingWithComment() -{ - /* - With Qml scripts in Qt 5, it's not possible to execute script in new context, - only in the global one. - - Workaround is to wrap the script properly in a new function: - - function() { - %1 - }() - - (Unfortunately, it's still possible to escape the new context with a script injection.) - */ - - const auto script = R"( - setCommands([ - { - isScript: true, - cmd: 'global.popup = function(msg) { return msg + 1; } // TEST' - }, - ]) - )"; - RUN(script, ""); - RUN("popup" << "test", "test1\n"); -} - -void Tests::scriptCommandWithError() -{ - const auto script = R"( - setCommands([ - { - isScript: true, - name: 'bad_script', - cmd: 'if (env("COPYQ_TEST_THROW") == "1") throw Error("BAD SCRIPT")' - }, - ]) - )"; - RUN(script, ""); - m_test->setEnv("COPYQ_TEST_THROW", "1"); - RUN_EXPECT_ERROR_WITH_STDERR( - "", CommandError, - "ScriptError: BAD SCRIPT\n" - "\n" - "--- backtrace ---\n" - ); - RUN_EXPECT_ERROR_WITH_STDERR( - "", CommandError, - "\neval:source@\n" - "--- end backtrace ---\n" - ); - m_test->setEnv("COPYQ_TEST_THROW", "0"); -} - -void Tests::scriptPaste() -{ - const auto script = R"( - setCommands([ - { - isScript: true, - cmd: 'global.paste = function() { add("PASTE") }' - }, - ]) - )"; - RUN(script, ""); - RUN("add(1)", ""); - RUN("keys" << clipboardBrowserId << "ENTER", ""); - WAIT_ON_OUTPUT("read(0)", "PASTE"); -} - -void Tests::scriptOnTabSelected() -{ - const auto script = R"( - setCommands([ - { - isScript: true, - cmd: 'global.onTabSelected = function() { add(selectedTab()) }' - }, - ]) - )"; - RUN(script, ""); - - const auto tab1 = testTab(1); - const auto tab2 = testTab(2); - RUN("show" << tab1, ""); - WAIT_ON_OUTPUT("tab" << tab1 << "read(0)", tab1); - RUN("show" << tab2, ""); - WAIT_ON_OUTPUT("tab" << tab2 << "read(0)", tab2); -} - -void Tests::scriptOnItemsRemoved() -{ - const auto script = R"( - setCommands([ - { - isScript: true, - cmd: ` - global.onItemsRemoved = function() { - items = ItemSelection().current().items(); - tab(tab()[0]); - add("R0:" + str(items[0][mimeText])); - add("R1:" + str(items[1][mimeText])); - } - ` - }, - ]) - )"; - RUN(script, ""); - const auto tab1 = testTab(1); - RUN("tab" << tab1 << "add(3,2,1,0)", ""); - RUN("tab" << tab1 << "remove(1,2)", ""); - WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2,)", "R1:2,R0:1,"); - - // Cancel item removal - const auto script2 = R"( - setCommands([ - { - isScript: true, - cmd: "global.onItemsRemoved = global.fail", - }, - ]) - )"; - RUN(script2, ""); - const auto tab2 = testTab(2); - RUN("tab" << tab2 << "add(3,2,1,0)", ""); - RUN("tab" << tab2 << "remove(1,2)", ""); - waitFor(1000); - RUN("tab" << tab2 << "separator" << "," << "read(0,1,2,3,4)", "0,1,2,3,"); - - // Avoid crash if the tab itself is removed while removing items - const auto script3 = R"( - setCommands([ - { - isScript: true, - cmd: ` - global.onItemsRemoved = function() { - removeTab(selectedTab()) - } - ` - }, - ]) - )"; - RUN(script3, ""); - const auto tab3 = testTab(3); - RUN("tab" << tab3 << "add(3,2,1,0)", ""); - RUN("tab" << tab3 << "remove(1,2)", ""); - waitFor(1000); - RUN("tab" << tab3 << "separator" << "," << "read(0,1,2,3,4)", ",,,,"); -} - -void Tests::scriptOnItemsAdded() -{ - const auto script = R"( - setCommands([ - { - isScript: true, - cmd: ` - global.onItemsAdded = function() { - sel = ItemSelection().current(); - items = sel.items(); - for (i = 0; i < items.length; ++i) - items[i][mimeText] = "A:" + str(items[i][mimeText]) - sel.setItems(items); - } - ` - }, - ]) - )"; - RUN(script, ""); - const auto tab1 = testTab(1); - RUN("tab" << tab1 << "add(1,0)", ""); - WAIT_ON_OUTPUT("tab" << tab1 << "separator" << "," << "read(0,1,2)", "A:0,A:1,"); -} - -void Tests::scriptOnItemsChanged() -{ - const auto script = R"( - setCommands([ - { - isScript: true, - cmd: ` - global.onItemsChanged = function() { - if (selectedTab() == tab()[0]) abort(); - items = ItemSelection().current().items(); - tab(tab()[0]); - add("C:" + str(items[0][mimeText])); - } - ` - }, - ]) - )"; - RUN(script, ""); - const auto tab1 = testTab(1); - RUN("tab" << tab1 << "add(0)", ""); - RUN("tab" << tab1 << "change(0, mimeText, 'A')", ""); - WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2)", "C:A,,"); - RUN("tab" << tab1 << "change(0, mimeText, 'B')", ""); - WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2)", "C:B,C:A,"); -} - -void Tests::scriptOnItemsLoaded() +void Tests::configPathEnvVariable() { - const auto script = R"( - setCommands([ - { - isScript: true, - cmd: ` - global.onItemsLoaded = function() { - if (selectedTab() == tab()[0]) abort(); - tab(tab()[0]); - add(selectedTab()); - } - ` - }, - ]) - )"; - RUN(script, ""); + const auto path = QDir::home().absoluteFilePath("copyq-settings"); + const auto environment = QStringList("COPYQ_SETTINGS_PATH=" + path); - const auto tab1 = testTab(1); - RUN("show" << tab1, ""); - WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2)", tab1 + ",,"); + QByteArray out; + QByteArray err; + run(Args() << "info" << "config", &out, &err, QByteArray(), environment); + QVERIFY2( testStderr(err), err ); - const auto tab2 = testTab(2); - RUN("show" << tab2, ""); - WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2)", tab2 + "," + tab1 + ","); + const auto expectedOut = path.toUtf8(); + QCOMPARE( out.left(expectedOut.size()), expectedOut ); } -void Tests::scriptEventMaxRecursion() +void Tests::itemDataPathEnvVariable() { - const auto script = R"( - setCommands([ - { - isScript: true, - cmd: `global.onItemsRemoved = function() { - const toRemove = str(selectedItemData(0)[mimeText]); - const newItem = (toRemove == "X") ? "A" : ("WRONG:" + toRemove); - add(newItem); - remove(size()-1); - }` - }, - ]) - )"; - RUN(script, ""); - RUN("add('X'); remove(0)", ""); - WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2,3,4,5,6,7,8,9,10)", "A,A,A,A,A,A,A,A,A,A,"); - waitFor(200); - RUN("separator" << "," << "read(0,1,2,3,4,5,6,7,8,9,10)", "A,A,A,A,A,A,A,A,A,A,"); -} + const auto path = QDir::home().absoluteFilePath("copyq-data"); + const auto environment = QStringList("COPYQ_ITEM_DATA_PATH=" + path); -void Tests::scriptSlowCollectOverrides() -{ - const auto script = R"( - setCommands([ - { - isScript: true, - cmd: 'global.onTabSelected = function() { add(selectedTab()) }' - }, - { - isScript: true, - cmd: ` - var collectOverrides_ = global.collectOverrides; - global.collectOverrides = function() { sleep(1000); collectOverrides_() } - ` - }, - ]) - )"; - RUN(script, ""); + QByteArray out; + QByteArray err; + run(Args() << "info" << "data", &out, &err, QByteArray(), environment); + QVERIFY2( testStderr(err), err ); - const auto tab1 = testTab(1); - RUN("show" << tab1, ""); - WAIT_ON_OUTPUT("tab" << tab1 << "read(0)", tab1); + const auto expectedOut = path.toUtf8(); + QCOMPARE( out.left(expectedOut.size()), expectedOut ); } -void Tests::displayCommand() +void Tests::configTabs() { - const auto testMime = COPYQ_MIME_PREFIX "test"; - const auto script = QString(R"( - setCommands([{ - display: true, - input: '%1', - cmd: 'copyq:' - + 'text = str(data(mimeText));' - + 'currentTab = str(data(mimeCurrentTab));' - + 'add(currentTab + "/" + text);' - }]) - )").arg(testMime); + const QString sep = QStringLiteral("\n"); + RUN("config" << "tabs", clipboardTabName + sep); - RUN(script, ""); + const QString tab1 = testTab(1); + RUN("tab" << tab1 << "add" << "test", ""); + RUN("config" << "tabs", clipboardTabName + sep + tab1 + sep); + + const QString tab2 = testTab(2); + RUN(QString("config('tabs', ['%1', '%2'])").arg(clipboardTabName, tab2), clipboardTabName + sep + tab2 + sep); + RUN("config" << "tabs", clipboardTabName + sep + tab2 + sep + tab1 + sep); + RUN("tab", clipboardTabName + sep + tab2 + sep + tab1 + sep); - RUN("write" << "0" << testMime << "" << mimeText << "a", ""); - WAIT_ON_OUTPUT( - "read(0,1,2)", - QString::fromLatin1("%1/a\na\n") - .arg(clipboardTabName) - .toUtf8() ); - - RUN("write" << "0" << testMime << "" << mimeText << "b", ""); - WAIT_ON_OUTPUT( - "read(0,1,2,3,4)", - QString::fromLatin1("%1/b\nb\n%1/a\na\n") - .arg(clipboardTabName) - .toUtf8() ); + RUN(QString("config('tabs', ['%1', '%2'])").arg(tab1, tab2), tab1 + sep + tab2 + sep); + RUN("config" << "tabs", tab1 + sep + tab2 + sep + clipboardTabName + sep); + RUN("tab", tab1 + sep + tab2 + sep + clipboardTabName + sep); } -void Tests::displayCommandForMenu() +void Tests::selectedItems() { - const auto tab = testTab(1); - const auto args = Args("tab") << tab << "separator" << ","; - const auto script = QString(R"( - setCommands([{ - display: true, - cmd: 'copyq:' - + 'currentTab = str(data(mimeCurrentTab));' - + 'inMenu = str(data(mimeDisplayItemInMenu));' - + 'if (inMenu != "1" || currentTab != "%1") abort();' - + 'text = str(data(mimeText));' - + 'setData(mimeText, "display:" + text);' - + 'setData(mimeIcon, String.fromCharCode(0xF328));' - + 'setData("application/x-copyq-item-tag", "TAG");' - + 'tab(tab()[0]);' - + 'old = str(read(0));' - + 'add(old + "|" + text);' - }]) - )").arg(tab); + const auto tab1 = testTab(1); + const Args args = Args("tab") << tab1; - RUN("config" << "tray_tab" << tab, tab + "\n"); - RUN("config" << "tray_tab_is_current" << "false", "false\n"); - RUN(script, ""); + RUN("selectedTab", "CLIPBOARD\n"); + RUN("selectedItems", ""); + + RUN(args << "add" << "D" << "C" << "B" << "A", ""); + RUN(args << "setCurrentTab" << tab1 << "selectItems" << "1" << "2", "true\n"); + RUN("selectedTab", tab1 + "\n"); + RUN("selectedItems", "1\n2\n"); + RUN("currentItem", "2\n"); + + const auto print = R"( + print([selectedTab(), "c:" + currentItem(), "s:" + selectedItems()]); + print("\\n") + )"; + + // Selection stays consistent when moving items + RUN(print << "move(0)" << print, tab1 + ",c:2,s:1,2\n" + tab1 + ",c:1,s:0,1\n"); + RUN(print, tab1 + ",c:1,s:0,1\n"); + + RUN(print << "keys('HOME', 'CTRL+DOWN')" << print, tab1 + ",c:1,s:0,1\n" + tab1 + ",c:0,s:1,0\n"); + RUN(print, tab1 + ",c:1,s:1\n"); + + // Selection stays consistent when removing items + RUN(args << "setCurrentTab" << tab1 << "selectItems" << "1" << "2" << "3", "true\n"); + RUN(print << "remove(2)" << print, tab1 + ",c:3,s:1,2,3\n" + tab1 + ",c:2,s:1,-1,2\n"); + RUN(print, tab1 + ",c:2,s:1,2\n"); + + // Renaming tab invalidates selection and all items because the tab + // underlying data needs to be loaded again using plugins. + const QString tab2 = testTab(2); + const auto rename = QString("renameTab('%1', '%2')").arg(tab1, tab2); + RUN(print << rename << print, tab1 + ",c:2,s:1,2\n" + tab1 + ",c:-1,s:-1,-1\n"); - RUN(args << "add(1,2,3,4,5)", ""); - RUN("menu", ""); - WAIT_ON_OUTPUT("read(0)", "|5|4|3|2|1"); + RUN(print, tab2 + ",c:0,s:0\n"); } void Tests::synchronizeInternalCommands() @@ -4530,297 +1360,6 @@ void Tests::setTabName() RUN(script, "1,0"); } -void Tests::showHideAboutDialog() -{ - const auto aboutShortcut = keyNameFor(QKeySequence::QKeySequence::WhatsThis); - RUN("keys" << clipboardBrowserId << aboutShortcut << aboutDialogId, ""); - RUN("keys" << aboutDialogId << "ESCAPE" << clipboardBrowserId, ""); -} - -void Tests::showHideClipboardDialog() -{ - TEST( m_test->setClipboard("TEST", "test-format") ); - RUN("keys" << clipboardBrowserId << "CTRL+SHIFT+C" << clipboardDialogId, ""); - - RUN("keys" << clipboardDialogId << "DOWN" << "HOME" << clipboardDialogFormatListId, ""); - - RUN("keys" << clipboardDialogFormatListId << keyNameFor(QKeySequence::Copy), ""); -#ifdef Q_OS_WIN - WAIT_FOR_CLIPBOARD("application/x-qt-windows-mime;value=\"test-format\""); -#else - WAIT_FOR_CLIPBOARD("test-format"); -#endif - - RUN("keys" << clipboardDialogId << "ESCAPE" << clipboardBrowserId, ""); -} - -void Tests::showHideItemDialog() -{ - RUN("write" << "test-format" << "TEST", ""); - RUN("selectItems" << "0", "true\n"); - - RUN("keys" << clipboardBrowserId << "F4" << clipboardDialogId, ""); - - RUN("keys" << clipboardDialogId << "DOWN" << clipboardDialogFormatListId, ""); - RUN("keys" << clipboardDialogFormatListId << keyNameFor(QKeySequence::Copy), ""); - WAIT_FOR_CLIPBOARD("test-format"); - - RUN("keys" << clipboardDialogFormatListId << "DOWN", ""); - RUN("keys" << clipboardDialogFormatListId << keyNameFor(QKeySequence::Copy), ""); - WAIT_FOR_CLIPBOARD("test-format"); - - RUN("keys" << clipboardDialogId << "ESCAPE" << clipboardBrowserId, ""); -} - -void Tests::showHideLogDialog() -{ - RUN("keys" << clipboardBrowserId << "F12" << logDialogId, ""); - - RUN("keys" << logDialogId << "CTRL+A" << "CTRL+C" << logDialogId, ""); - const QByteArray expectedLog = "Starting callback: onStart"; - TEST( m_test->verifyClipboard(expectedLog, mimeHtml, false) ); - - RUN("keys" << logDialogId << "ESCAPE" << clipboardBrowserId, ""); -} - -void Tests::showHideActionHandlerDialog() -{ - RUN("keys" << clipboardBrowserId << "CTRL+SHIFT+Z" << actionHandlerDialogId, ""); - - RUN("keys" << actionHandlerFilterId << ":onstart" << "TAB" << actionHandlerTableId, ""); - - RUN("keys" << actionHandlerTableId << "RIGHT" << "CTRL+C", ""); - WAIT_FOR_CLIPBOARD("copyq onStart"); - - RUN("keys" << actionHandlerDialogId << "ESCAPE" << clipboardBrowserId, ""); -} - -void Tests::shortcutDialogAddShortcut() -{ -#ifdef Q_OS_MAC - SKIP("Mnemonic for focusing shortcut button doesn't work on OS X"); -#endif - - RUN("setCommands([{name: 'test', inMenu: true, cmd: 'copyq add OK'}])", ""); - RUN("commands()[0].shortcuts", ""); - - RUN("keys" << clipboardBrowserId << "F6" << commandDialogId, ""); - RUN("keys" << commandDialogId << "ALT+S" << shortcutButtonId, ""); - RUN("keys" << shortcutButtonId << "Space" << shortcutDialogId, ""); - RUN("keys" << shortcutDialogId << "CTRL+F1" << shortcutButtonId, ""); - - RUN("keys" << commandDialogId << "ESCAPE" << commandDialogSaveButtonId, ""); - RUN("keys" << commandDialogSaveButtonId << "Enter" << clipboardBrowserId, ""); - RUN("commands()[0].shortcuts", "ctrl+f1\n"); -} - -void Tests::shortcutDialogAddTwoShortcuts() -{ -#ifdef Q_OS_MAC - SKIP("Mnemonic for focusing shortcut button doesn't work on OS X"); -#endif - - RUN("setCommands([{name: 'test', inMenu: true, shortcuts: ['ctrl+f1'], cmd: 'copyq add OK'}])", ""); - RUN("commands()[0].shortcuts", "ctrl+f1\n"); - - RUN("keys" << clipboardBrowserId << "F6" << commandDialogId, ""); - RUN("keys" << commandDialogId << "ALT+S" << shortcutButtonId, ""); - - RUN("keys" << shortcutButtonId << "TAB" << shortcutButtonId, ""); - RUN("keys" << shortcutButtonId << "Space" << shortcutDialogId, ""); - RUN("keys" << shortcutDialogId << "F1" << shortcutButtonId, ""); - - RUN("keys" << shortcutButtonId << "Space" << shortcutDialogId, ""); - RUN("keys" << shortcutDialogId << "F2" << shortcutButtonId, ""); - - RUN("keys" << commandDialogId << "ESCAPE" << commandDialogSaveButtonId, ""); - RUN("keys" << commandDialogSaveButtonId << "Enter" << clipboardBrowserId, ""); - RUN("commands()[0].shortcuts", "ctrl+f1\nf1\nf2\n"); -} - -void Tests::shortcutDialogChangeShortcut() -{ -#ifdef Q_OS_MAC - SKIP("Mnemonic for focusing shortcut button doesn't work on OS X"); -#endif - - RUN("setCommands([{name: 'test', inMenu: true, shortcuts: ['f1','f2','f3'], cmd: 'copyq add OK'}])", ""); - RUN("commands()[0].shortcuts", "f1\nf2\nf3\n"); - - RUN("keys" << clipboardBrowserId << "F6" << commandDialogId, ""); - RUN("keys" << commandDialogId << "ALT+S" << shortcutButtonId, ""); - RUN("keys" << commandDialogId << "TAB" << shortcutButtonId, ""); - RUN("keys" << shortcutButtonId << "Space" << shortcutDialogId, ""); - RUN("keys" << shortcutDialogId << "F4" << shortcutButtonId, ""); - - RUN("keys" << commandDialogId << "ESCAPE" << commandDialogSaveButtonId, ""); - RUN("keys" << commandDialogSaveButtonId << "Enter" << clipboardBrowserId, ""); - RUN("commands()[0].shortcuts", "f1\nf4\nf3\n"); -} - -void Tests::shortcutDialogSameShortcut() -{ -#ifdef Q_OS_MAC - SKIP("Mnemonic for focusing shortcut button doesn't work on OS X"); -#endif - - RUN("setCommands([{name: 'test', inMenu: true, shortcuts: ['ctrl+f1'], cmd: 'copyq add OK'}])", ""); - RUN("commands()[0].shortcuts", "ctrl+f1\n"); - - RUN("keys" << clipboardBrowserId << "F6" << commandDialogId, ""); - RUN("keys" << commandDialogId << "ALT+S" << shortcutButtonId, ""); - RUN("keys" << shortcutButtonId << "TAB" << shortcutButtonId, ""); - RUN("keys" << shortcutButtonId << "Space" << shortcutDialogId, ""); - RUN("keys" << shortcutDialogId << "CTRL+F1" << shortcutButtonId, ""); - - RUN("keys" << commandDialogId << "ESCAPE" << clipboardBrowserId, ""); - RUN("commands()[0].shortcuts", "ctrl+f1\n"); -} - -void Tests::shortcutDialogCancel() -{ -#ifdef Q_OS_MAC - SKIP("Mnemonic for focusing shortcut button doesn't work on OS X"); -#endif - - RUN("setCommands([{name: 'test', inMenu: true, shortcuts: ['ctrl+f1'], cmd: 'copyq add OK'}])", ""); - RUN("commands()[0].shortcuts", "ctrl+f1\n"); - - RUN("keys" << clipboardBrowserId << "F6" << commandDialogId, ""); - RUN("keys" << commandDialogId << "ALT+S" << shortcutButtonId, ""); - RUN("keys" << commandDialogId << "TAB" << shortcutButtonId, ""); - RUN("keys" << shortcutButtonId << "Space" << shortcutDialogId, ""); - RUN("keys" << shortcutDialogId << "TAB" << "focus:ShortcutDialog", ""); - RUN("keys" << "ESCAPE" << shortcutButtonId, ""); - - RUN("keys" << commandDialogId << "ESCAPE" << clipboardBrowserId, ""); - RUN("commands()[0].shortcuts", "ctrl+f1\n"); -} - -void Tests::actionDialogCancel() -{ - const auto script = R"( - setCommands([{ - name: 'test', - inMenu: true, - shortcuts: ['ctrl+f1'], - wait: true, - cmd: 'copyq settings test SHOULD_NOT_BE_SET' - }]) - )"; - RUN(script, ""); - - RUN("keys" << clipboardBrowserId << "CTRL+F1" << actionDialogId, ""); - RUN("keys" << actionDialogId << "ESCAPE" << clipboardBrowserId, ""); - RUN("settings" << "test", ""); -} - -void Tests::actionDialogAccept() -{ - const auto script = R"( - setCommands([{ - name: 'test', - inMenu: true, - shortcuts: ['ctrl+f1'], - wait: true, - cmd: 'copyq settings test SHOULD_BE_SET' - }]) - )"; - RUN(script, ""); - - RUN("keys" << clipboardBrowserId << "CTRL+F1" << actionDialogId, ""); - // Can't focus configuration checkboxes on OS X -#ifdef Q_OS_MAC - RUN("keys" << actionDialogId << "BACKTAB" << "ENTER" << clipboardBrowserId, ""); -#else - RUN("keys" << actionDialogId << "ENTER" << clipboardBrowserId, ""); -#endif - WAIT_ON_OUTPUT("settings" << "test", "SHOULD_BE_SET"); -} - -void Tests::actionDialogSelection() -{ - const auto script = R"( - setCommands([{ - name: 'test', - inMenu: true, - shortcuts: ['ctrl+f1'], - wait: true, - cmd: 'copyq settings test %1' - }]) - )"; - RUN(script, ""); - - const auto tab = testTab(1); - const auto args = Args("tab") << tab; - RUN(args << "add" << "C" << "B" << "A", ""); - RUN("setCurrentTab" << tab, ""); - RUN(args << "selectItems" << "0" << "2", "true\n"); - - RUN("keys" << clipboardBrowserId << "CTRL+F1" << actionDialogId, ""); - // Can't focus configuration checkboxes on OS X -#ifdef Q_OS_MAC - RUN("keys" << actionDialogId << "BACKTAB" << "ENTER" << clipboardBrowserId, ""); -#else - RUN("keys" << actionDialogId << "ENTER" << clipboardBrowserId, ""); -#endif - WAIT_ON_OUTPUT("settings" << "test", "A\nC"); -} - -void Tests::actionDialogSelectionInputOutput() -{ - const auto script = R"( - setCommands([{ - name: 'test', - inMenu: true, - shortcuts: ['ctrl+f1'], - wait: true, - input: 'text/plain', - output: 'text/plain', - cmd: 'copyq input' - }]) - )"; - RUN(script, ""); - - const auto tab = testTab(1); - const auto args = Args("tab") << tab; - RUN(args << "add" << "C" << "B" << "A", ""); - RUN("setCurrentTab" << tab, ""); - RUN(args << "selectItems" << "0" << "2", "true\n"); - - RUN("keys" << clipboardBrowserId << "CTRL+F1" << actionDialogId, ""); - // Can't focus configuration checkboxes on OS X -#ifdef Q_OS_MAC - RUN("keys" << actionDialogId << "BACKTAB" << "ENTER" << clipboardBrowserId, ""); -#else - RUN("keys" << actionDialogId << "ENTER" << clipboardBrowserId, ""); -#endif - WAIT_ON_OUTPUT(args << "read" << "0", "A\nC"); -} - -void Tests::exitConfirm() -{ - RUN("keys" << clipboardBrowserId << "CTRL+Q" << confirmExitDialogId, ""); - RUN("keys" << confirmExitDialogId << "ENTER", ""); - TEST( m_test->waitForServerToStop() ); -} - -void Tests::exitNoConfirm() -{ - RUN("config" << "confirm_exit" << "false", "false\n"); - RUN("keys" << clipboardBrowserId << "CTRL+Q", ""); - TEST( m_test->waitForServerToStop() ); -} - -void Tests::exitStopCommands() -{ - RUN("config" << "confirm_exit" << "false", "false\n"); - RUN("action" << "copyq sleep 999999", ""); - RUN("keys" << clipboardBrowserId << "CTRL+Q", ""); - RUN("keys" << confirmExitDialogId << "ENTER", ""); - TEST( m_test->waitForServerToStop() ); -} - void Tests::abortInputReader() { RUN_WITH_INPUT("afterMilliseconds(0, abort); input(); 'DONE'", KEEP_STDIN_OPEN, ""); @@ -5092,20 +1631,6 @@ void Tests::clipboardUriList() WAIT_ON_OUTPUT("clipboard(mimeUriList)", uris); } -int Tests::run( - const QStringList &arguments, QByteArray *stdoutData, QByteArray *stderrData, const QByteArray &in, - const QStringList &environment) -{ - return m_test->run(arguments, stdoutData, stderrData, in, environment); -} - -bool Tests::hasTab(const QString &tabName) -{ - QByteArray out; - run(Args("tab"), &out); - return splitLines(out).contains(tabName); -} - int runTests(int argc, char *argv[]) { QRegularExpression onlyPlugins; @@ -5144,11 +1669,21 @@ int runTests(int argc, char *argv[]) int exitCode = 0; std::shared_ptr test(new TestInterfaceImpl); - Tests tc(test); + const auto runTests = [&](QObject *tests){ + exitCode = std::max(exitCode, test->runTests(tests, argc, argv)); + }; if (onlyPlugins.pattern().isEmpty()) { test->setupTest("CORE", QVariant()); - exitCode = test->runTests(&tc, argc, argv); + {TestsCli t(test); runTests(&t);} + {TestsScripts t(test); runTests(&t);} + {TestsCommands t(test); runTests(&t);} + {TestsScriptCommands t(test); runTests(&t);} + {TestsClasses t(test); runTests(&t);} + {TestsItems t(test); runTests(&t);} + {TestsDialogs t(test); runTests(&t);} + {TestsTray t(test); runTests(&t);} + {Tests t(test); runTests(&t);} test->stopServer(); } @@ -5160,8 +1695,7 @@ int runTests(int argc, char *argv[]) std::unique_ptr pluginTests( loader->tests(test) ); if ( pluginTests != nullptr ) { test->setupTest(loader->id(), pluginTests->property("CopyQ_test_settings")); - const int pluginTestsExitCode = test->runTests(pluginTests.get(), argc, argv); - exitCode = qMax(exitCode, pluginTestsExitCode); + runTests(pluginTests.get()); test->stopServer(); } } diff --git a/src/tests/tests.h b/src/tests/tests.h index 9b7eb5eb31..c2983b192f 100644 --- a/src/tests/tests.h +++ b/src/tests/tests.h @@ -1,179 +1,22 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#ifndef TESTS_H -#define TESTS_H +#pragma once #include "tests/testinterface.h" +#include "tests/tests_base.h" -#include -#include - -class QProcess; -class QByteArray; - -/** - * Tests for the application. - */ -class Tests final : public QObject +class Tests : public TestsBase { Q_OBJECT public: - explicit Tests(const TestInterfacePtr &test, QObject *parent = nullptr); + explicit Tests(const TestInterfacePtr &test) : TestsBase(test) {} private slots: - void initTestCase(); - void cleanupTestCase(); - void init(); - void cleanup(); - - void readLog(); - void commandHelp(); - void commandVersion(); - void badCommand(); - void badSessionName(); - - void commandCatchExceptions(); - - void commandExit(); - void commandEval(); - void commandEvalThrows(); - void commandEvalSyntaxError(); - void commandEvalArguments(); - void commandEvalEndingWithComment(); - void commandPrint(); - void commandAbort(); - void commandFail(); - void commandSource(); - - void commandVisible(); - void commandToggle(); - void commandHide(); - void commandShow(); - void commandShowAt(); - void commandFocused(); - - void commandsUnicode(); - - void commandsAddRead(); - void commandsWriteRead(); - void commandChange(); - - void commandSetCurrentTab(); - - void commandConfig(); - void commandToggleConfig(); - - void commandDialog(); - void commandDialogCloseOnDisconnect(); - - void commandMenuItems(); - - void commandsPackUnpack(); - void commandsBase64(); - void commandsGetSetItem(); - - void commandsChecksums(); - - void commandEscapeHTML(); - - void commandExecute(); - - void commandSettings(); - - void commandsEnvSetEnv(); - - void commandSleep(); - - void commandsData(); - - void commandCurrentWindowTitle(); - - void commandCopy(); - void commandClipboard(); - void commandHasClipboardFormat(); - - void commandEdit(); - void commandEditItem(); - - void commandGetSetCurrentPath(); - - void commandSelectItems(); - - void commandsExportImport(); - - void commandsGetSetCommands(); - - void commandsImportExportCommands(); - void commandsImportExportCommandsFixIndentation(); - - void commandsAddCommandsRegExp(); - - void commandScreenshot(); - - void commandNotification(); - - void commandIcon(); - void commandIconTag(); - void commandIconTagColor(); - void commandLoadTheme(); - - void commandDateString(); - - void commandAfterMilliseconds(); - - void commandAsync(); - - void commandFilter(); - - void commandMimeTypes(); - - void commandUnload(); - void commandForceUnload(); - - void commandServerLogAndLogs(); - - void classByteArray(); - void classFile(); - void classDir(); - void classTemporaryFile(); - void classItemSelection(); - void classItemSelectionGetCurrent(); - void classItemSelectionByteArray(); - void classItemSelectionSort(); - void classSettings(); - void calledWithInstance(); - void pipingCommands(); - void chainingCommands(); - - void configMaxitems(); - - void keysAndFocusing(); - - void selectItems(); - - void moveItems(); - void deleteItems(); - void searchItems(); - void searchItemsAndSelect(); - void searchItemsAndCopy(); - void searchRowNumber(); - void searchAccented(); - void copyItems(); - void selectAndCopyOrder(); - - void sortAndReverse(); - - void createTabDialog(); - void copyPasteCommands(); - void editItems(); - void createNewItem(); - void editNotes(); - void toggleClipboardMonitoring(); void clipboardToItem(); @@ -182,7 +25,6 @@ private slots: void tabRemove(); void tabIcon(); void action(); - void insertRemoveItems(); void renameTab(); void renameClipboardTab(); void importExportTab(); @@ -201,19 +43,8 @@ private slots: void pasteFromMainWindow(); - void tray(); - void menu(); - - void traySearch(); - void trayPaste(); - void pasteNext(); - // Options for tray menu. - void configTrayTab(); - void configMove(); - void configTrayTabIsCurrent(); - void configAutostart(); void configPathEnvVariable(); @@ -223,51 +54,6 @@ private slots: void selectedItems(); - void shortcutCommand(); - void shortcutCommandOverrideEnter(); - void shortcutCommandMatchInput(); - void shortcutCommandMatchCmd(); - - void shortcutCommandSelectedItemData(); - void shortcutCommandSetSelectedItemData(); - void shortcutCommandSelectedItemsData(); - void shortcutCommandSetSelectedItemsData(); - void shortcutCommandSelectedAndCurrent(); - void shortcutCommandMoveSelected(); - - void automaticCommandIgnore(); - void automaticCommandRemove(); - void automaticCommandInput(); - void automaticCommandRegExp(); - void automaticCommandSetData(); - void automaticCommandOutputTab(); - void automaticCommandNoOutputTab(); - void automaticCommandChaining(); - void automaticCommandCopyToTab(); - void automaticCommandStoreSpecialFormat(); - void automaticCommandIgnoreSpecialFormat(); - - void globalCommandInMenu(); - - void scriptCommandLoaded(); - void scriptCommandAddFunction(); - void scriptCommandOverrideFunction(); - void scriptCommandEnhanceFunction(); - void scriptCommandEndingWithComment(); - void scriptCommandWithError(); - - void scriptPaste(); - void scriptOnTabSelected(); - void scriptOnItemsRemoved(); - void scriptOnItemsAdded(); - void scriptOnItemsChanged(); - void scriptOnItemsLoaded(); - void scriptEventMaxRecursion(); - void scriptSlowCollectOverrides(); - - void displayCommand(); - void displayCommandForMenu(); - void synchronizeInternalCommands(); void queryKeyboardModifiersCommand(); @@ -276,27 +62,6 @@ private slots: void setTabName(); - void showHideAboutDialog(); - void showHideClipboardDialog(); - void showHideItemDialog(); - void showHideLogDialog(); - void showHideActionHandlerDialog(); - - void shortcutDialogAddShortcut(); - void shortcutDialogAddTwoShortcuts(); - void shortcutDialogChangeShortcut(); - void shortcutDialogSameShortcut(); - void shortcutDialogCancel(); - - void actionDialogCancel(); - void actionDialogAccept(); - void actionDialogSelection(); - void actionDialogSelectionInputOutput(); - - void exitConfirm(); - void exitNoConfirm(); - void exitStopCommands(); - void abortInputReader(); void changeAlwaysOnTop(); @@ -317,17 +82,6 @@ private slots: void slowClipboard(); void clipboardUriList(); - -private: - void clearServerErrors(); - int run(const QStringList &arguments, QByteArray *stdoutData = nullptr, - QByteArray *stderrData = nullptr, const QByteArray &in = QByteArray(), - const QStringList &environment = QStringList()); - bool hasTab(const QString &tabName); - - TestInterfacePtr m_test; }; int runTests(int argc, char *argv[]); - -#endif // TESTS_H diff --git a/src/tests/tests_base.cpp b/src/tests/tests_base.cpp new file mode 100644 index 0000000000..4a548e77b1 --- /dev/null +++ b/src/tests/tests_base.cpp @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "test_utils.h" +#include "tests/tests_base.h" + +#include +#include + +TestsBase::TestsBase(const TestInterfacePtr &test) + : m_test(test) +{ +} + +void TestsBase::initTestCase() +{ + TEST(m_test->initTestCase()); +} + +void TestsBase::cleanupTestCase() +{ + TEST(m_test->cleanupTestCase()); +} + +void TestsBase::init() +{ + TEST(m_test->init()); +} + +void TestsBase::cleanup() +{ + TEST( m_test->cleanup() ); +} + +int TestsBase::run( + const QStringList &arguments, + QByteArray *stdoutData, + QByteArray *stderrData, const QByteArray &in, + const QStringList &environment) +{ + return m_test->run(arguments, stdoutData, stderrData, in, environment); +} + +bool TestsBase::hasTab(const QString &tabName) +{ + QByteArray out; + run(Args("tab"), &out); + return splitLines(out).contains(tabName); +} + +TemporaryFile::TemporaryFile() +{ + QTemporaryFile tmp; + tmp.setAutoRemove(false); + QVERIFY(tmp.open()); + m_fileName = tmp.fileName(); + tmp.close(); +} + +TemporaryFile::~TemporaryFile() +{ + QFile::remove(m_fileName); +} + +bool testStderr(const QByteArray &stderrData, TestInterface::ReadStderrFlag flag) +{ + static const QRegularExpression reFailure("(Warning:|ERROR:|ASSERT|ScriptError:).*", QRegularExpression::CaseInsensitiveOption); + const QLatin1String scriptError("ScriptError:"); + + const auto plain = [](const char *str){ + return QRegularExpression(QRegularExpression::escape(QLatin1String(str))); + }; + const auto regex = [](const char *str){ + return QRegularExpression(QLatin1String(str)); + }; + // Ignore exceptions and errors from clients in application log + // (these are expected in some tests). + static const std::array ignoreList{ + regex(R"(CopyQ Note \[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\] ignoreLog; + + const QString output = QString::fromUtf8(stderrData); + QRegularExpressionMatchIterator it = reFailure.globalMatch(output); + while ( it.hasNext() ) { + const auto match = it.next(); + + const QString log = match.captured(); + + if ( flag == TestInterface::ReadErrorsWithoutScriptException + && log.contains(scriptError) ) + { + return false; + } + + if ( ignoreLog.contains(log) ) + return ignoreLog[log]; + + qDebug() << "Failure in logs:" << log; + + const bool ignore = std::any_of( + std::begin(ignoreList), std::end(ignoreList), + [&output](const QRegularExpression &reIgnore){ + return output.contains(reIgnore); + }); + + ignoreLog[log] = ignore; + if (!ignore) + return false; + } + + return true; +} + +int count(const QStringList &items, const QString &pattern) +{ + int from = -1; + int count = 0; + const QRegularExpression re(pattern); + while ( (from = items.indexOf(re, from + 1)) != -1 ) + ++count; + return count; +} + +QStringList splitLines(const QByteArray &nativeText) +{ + return QString::fromUtf8(nativeText).split(QRegularExpression("\r\n|\n|\r")); +} + +QByteArray generateData() +{ + static int i = 0; + const QByteArray id = "tests_" + + QByteArray::number(QDateTime::currentMSecsSinceEpoch() % 1000); + return id + '_' + QByteArray::number(++i); +} + +QString appWindowTitle(const QString &text) +{ +#ifdef Q_OS_MAC + return QStringLiteral("CopyQ - %1\n").arg(text); +#elif defined(Q_OS_WIN) + return QStringLiteral("%1 - CopyQ-TEST\n").arg(text); +#else + return QStringLiteral("%1 — CopyQ-TEST\n").arg(text); +#endif +} diff --git a/src/tests/tests_base.h b/src/tests/tests_base.h new file mode 100644 index 0000000000..f949610bfe --- /dev/null +++ b/src/tests/tests_base.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "tests/testinterface.h" + +#include +#include + +class QByteArray; + +class TestsBase : public QObject +{ + Q_OBJECT + +public: + explicit TestsBase(const TestInterfacePtr &test); + +private slots: + void initTestCase(); + void cleanupTestCase(); + void init(); + void cleanup(); + +protected: + int run( + const QStringList &arguments, + QByteArray *stdoutData = nullptr, + QByteArray *stderrData = nullptr, const QByteArray &in = QByteArray(), + const QStringList &environment = QStringList()); + bool hasTab(const QString &tabName); + + TestInterfacePtr m_test; +}; + +// Similar to QTemporaryFile but allows removing from other process. +class TemporaryFile { +public: + TemporaryFile(); + ~TemporaryFile(); + QString fileName() const { return m_fileName; } + +private: + QString m_fileName; +}; + +int count(const QStringList &items, const QString &pattern); + +QStringList splitLines(const QByteArray &nativeText); + +bool testStderr( + const QByteArray &stderrData, + TestInterface::ReadStderrFlag flag = TestInterface::ReadErrors); + +/// Generate unique data. +QByteArray generateData(); + +QString appWindowTitle(const QString &text); diff --git a/src/tests/tests_classes.cpp b/src/tests/tests_classes.cpp new file mode 100644 index 0000000000..90ac865b8e --- /dev/null +++ b/src/tests/tests_classes.cpp @@ -0,0 +1,448 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "test_utils.h" +#include "tests/tests_classes.h" + +#include "common/appconfig.h" + +void TestsClasses::classByteArray() +{ + RUN("ByteArray", ""); + RUN("ByteArray('test')", "test"); + RUN("ByteArray(ByteArray('test'))", "test"); + RUN("typeof(ByteArray('test'))", "object\n"); + RUN("ByteArray('test') instanceof ByteArray", "true\n"); + RUN("b = ByteArray('0123'); b.chop(2); b", "01"); + RUN("ByteArray('0123').equals(ByteArray('0123'))", "true\n"); + RUN("ByteArray('0123').left(3)", "012"); + RUN("ByteArray('0123').mid(1, 2)", "12"); + RUN("ByteArray('0123').mid(1)", "123"); + RUN("ByteArray('0123').right(3)", "123"); + RUN("ByteArray('0123').remove(1, 2)", "03"); + RUN("ByteArray(' 01 23 ').simplified()", "01 23"); + RUN("ByteArray('0123').toBase64()", "MDEyMw=="); + RUN("ByteArray('ABCd').toLower()", "abcd"); + RUN("ByteArray('abcD').toUpper()", "ABCD"); + RUN("ByteArray(' 01 23 ').trimmed()", "01 23"); + RUN("b = ByteArray('0123'); b.truncate(2); b", "01"); + RUN("ByteArray('0123').toLatin1String() == '0123'", "true\n"); + RUN("ByteArray('0123').valueOf() == '0123'", "true\n"); + RUN("ByteArray(8).size()", "8\n"); + RUN("b = ByteArray(); b.length = 10; b.length", "10\n"); + + // ByteArray implicitly converts to str. + RUN("ByteArray('test') == 'test'", "true\n"); + RUN("ByteArray('test1') == 'test2'", "false\n"); + RUN("ByteArray('test') === 'test'", "false\n"); + RUN("ByteArray('a') + 'b'", "ab\n"); +} + +void TestsClasses::classFile() +{ + RUN("var f = new File('/copyq_missing_file'); f.exists()", "false\n"); +} + +void TestsClasses::classDir() +{ + RUN("var d = new Dir('/missing_directory/')" + "; d.exists()" + , "false\n" + ); + + const auto home = QDir::homePath(); + RUN("Dir().homePath()", home + "\n"); + RUN("Dir().home().path()", home + "\n"); + + const auto root = QDir::rootPath(); + RUN("Dir().rootPath()", root + "\n"); + RUN("Dir().root().path()", root + "\n"); + + const auto temp = QDir::tempPath(); + RUN("Dir().tempPath()", temp + "\n"); + RUN("Dir().temp().path()", temp + "\n"); + + RUN("Dir().separator()", QString(QDir::separator()) + "\n"); + + QTemporaryDir tmpDir; + QVERIFY(tmpDir.isValid()); + QDir dir(tmpDir.path()); + const auto path = dir.path(); + const auto args = QString::fromLatin1("var d = new Dir('%1')").arg(path); + + RUN(args << "d.exists()", "true\n"); + RUN(args << "d.isReadable()", "true\n"); + RUN(args << "d.isAbsolute()", "true\n"); + RUN(args << "d.isRelative()", "false\n"); + RUN(args << "d.absolutePath()", path + "\n"); + RUN(args << "d.path()", path + "\n"); + RUN(args << "d.makeAbsolute()", "true\n"); + + RUN(args << "d.mkdir('test')", "true\n"); + QVERIFY( QDir(dir.filePath("test")).exists() ); + RUN(args << "d.exists('test')", "true\n"); + + RUN(args << "d.mkpath('a/b/c')", "true\n"); + QVERIFY( QDir(dir.filePath("a/b/c")).exists() ); + RUN(args << "d.exists('a/b/c')", "true\n"); + RUN(args << "d.filePath('a/b/c')", dir.filePath("a/b/c") + "\n"); + RUN(args << "d.relativeFilePath('" + path + "/a/b/c')", "a/b/c\n"); + RUN("Dir('" + path + "/test/../a//b/c/..').canonicalPath()", dir.filePath("a/b") + "\n"); + RUN(args << "d.setPath('" + path + "/a/b/c')" << "d.path()", dir.filePath("a/b/c") + "\n"); + + RUN(args << "d.cd('a')" << "d.cd('b')", "true\n"); + RUN(args << "d.cd('x')", "false\n"); + RUN(args << "d.cd('a')" << "d.cd('b')" << "d.path()", dir.filePath("a/b") + "\n"); + RUN(args << "d.cd('a')" << "d.cd('..')" << "d.path()", path + "\n"); + RUN(args << "d.cd('a')" << "d.cdUp()" << "d.path()", path + "\n"); + + RUN(args << "d.count()", QString::fromLatin1("%1\n").arg(dir.count())); + RUN(args << "d.dirName()", QString::fromLatin1("%1\n").arg(dir.dirName())); + + RUN(args << "d.match(['a*'], 'test')", "false\n"); + RUN(args << "d.match(['t*'], 'test')", "true\n"); + RUN(args << "d.entryList()", ".\n..\na\ntest\n"); + RUN(args << "d.entryList(['t*'])", "test\n"); + RUN(args << "d.entryList(['t?st', 'a*'])", "a\ntest\n"); + + RUN(args << "d.setNameFilters(['t?st', 'a*'])" << "d.nameFilters()", "t?st\na*\n"); + + QFile f(dir.filePath("test.txt")); + QVERIFY( f.open(QIODevice::WriteOnly) ); + f.close(); + RUN(args << "d.exists('test.txt')", "true\n"); + RUN(args << "d.absoluteFilePath('test.txt')", dir.filePath("test.txt") + "\n"); + RUN(args << "d.rename('test.txt', 'test2.txt')", "true\n"); + RUN(args << "d.exists('test2.txt')", "true\n"); + RUN(args << "d.remove('test2.txt')", "true\n"); + RUN(args << "d.exists('test2.txt')", "false\n"); + + RUN(args << "d.rmdir('test')", "true\n"); + RUN(args << "d.exists('test')", "false\n"); + + RUN("Dir().cleanPath('/a//b/../c/')", QDir::cleanPath("/a//b/../c/") + "\n"); +} + +void TestsClasses::classTemporaryFile() +{ + RUN("var f = new TemporaryFile(); f.open()", "true\n"); + + QByteArray err; + + for ( const auto autoRemove : {true, false} ) { + QByteArray fileName; + const auto script = + QString(R"( + var f = new TemporaryFile() + if (!f.open()) + throw 'Failed to open temporary file' + + f.setAutoRemove(%1) + print(f.fileName()) + )").arg(autoRemove); + run(Args() << script, &fileName, &err); + QVERIFY2( testStderr(err), err ); + + QFile f( QString::fromUtf8(fileName) ); + QVERIFY( f.exists() != autoRemove ); + + if (!autoRemove) + f.remove(); + } + + QByteArray fileName; + const auto scriptWrite = R"( + var f = new TemporaryFile() + if (!f.open()) + throw 'Failed to open temporary file' + + if (!f.write('LINE')) + throw 'Failed to write to temporary file' + + f.setAutoRemove(false) + print(f.fileName()) + )"; + run(Args() << scriptWrite, &fileName); + QVERIFY2( testStderr(err), err ); + QVERIFY( QFile::exists(QString::fromUtf8(fileName)) ); + + QByteArray out; + const auto scriptRead = R"( + var f = new File(str(input())) + if (!f.openReadOnly()) + throw 'Failed to open file' + + print('' + + ' exists()=' + str(f.exists()) + + ' isOpen()=' + str(f.isOpen()) + + ' isReadable()=' + str(f.isReadable()) + + ' isWritable()=' + str(f.isWritable()) + + ' size()=' + str(f.size()) + + ' readAll()=' + str(f.readAll()) + + ' atEnd()=' + str(f.atEnd()) + + ' seek(0)=' + str(f.seek(0)) + + ' read(1)=' + str(f.read(1)) + + ' pos()=' + str(f.pos()) + + ' peek(1)=' + str(f.peek(1)) + + ' readLine()=' + str(f.readLine()) + ) + )"; + const QByteArray expectedOut = + " exists()=true" + " isOpen()=true" + " isReadable()=true" + " isWritable()=false" + " size()=4" + " readAll()=LINE" + " atEnd()=true" + " seek(0)=true" + " read(1)=L" + " pos()=1" + " peek(1)=I" + " readLine()=INE"; + run(Args() << scriptRead, &out, &err, fileName); + QVERIFY2( testStderr(err), err ); + QCOMPARE(out, expectedOut); + + const auto scriptRemove = R"( + var f = new File(str(input())) + if (!f.remove()) + throw 'Failed to remove file' + )"; + run(Args() << scriptRemove, &out, &err, fileName); + QVERIFY2( testStderr(err), err ); + QCOMPARE(QByteArray(), out); + QVERIFY( !QFile::exists(QString::fromUtf8(fileName)) ); + + RUN("TemporaryFile().autoRemove()", "true\n"); + RUN("TemporaryFile().fileTemplate()", QDir::temp().filePath("copyq.test.XXXXXX") + "\n"); +} + +void TestsClasses::classItemSelection() +{ + const auto tab1 = testTab(1); + const Args args = Args("tab") << tab1 << "separator" << ","; + const QString outRows("ItemSelection(tab=\"" + tab1 + "\", rows=[%1])\n"); + + RUN(args << "add" << "C" << "B" << "A", ""); + RUN(args << "ItemSelection().length", "0\n"); + RUN("ItemSelection('" + tab1 + "').length", "0\n"); + RUN(args << "ItemSelection().selectAll().length", "3\n"); + RUN("ItemSelection('" + tab1 + "').selectAll().length", "3\n"); + + RUN(args << "a = ItemSelection(); b = a; a === b", "true\n"); + RUN(args << "a = ItemSelection(); b = a.selectAll(); a === b", "true\n"); + + RUN(args << "ItemSelection().selectAll().str()", outRows.arg("0..2")); + RUN(args << "ItemSelection().selectRemovable().str()", outRows.arg("0..2")); + RUN(args << "ItemSelection().selectRemovable().removeAll().str()", outRows.arg("")); + RUN(args << "read(0,1,2)", ",,"); + + RUN(args << "add" << "C" << "B" << "A", ""); + RUN(args << "ItemSelection().select(/A|C/).str()", outRows.arg("0,2")); + RUN(args << "ItemSelection().select(/a|c/i).str()", outRows.arg("0,2")); + RUN(args << "ItemSelection().select(/A/).select(/C/).str()", outRows.arg("0,2")); + RUN(args << "ItemSelection().select(/C/).select(/A/).str()", outRows.arg("2,0")); + RUN(args << "ItemSelection().select(/A|C/).invert().str()", outRows.arg("1")); + + RUN(args << "ItemSelection().select(/A|C/).deselectIndexes([0]).str()", outRows.arg("2")); + RUN(args << "ItemSelection().select(/A|C/).deselectIndexes([1]).str()", outRows.arg("0")); + RUN(args << "ItemSelection().select(/A|C/).deselectIndexes([0,1]).str()", outRows.arg("")); + RUN(args << "ItemSelection().select(/A|C/).deselectSelection(ItemSelection().select(/A/)).str()", outRows.arg("2")); + RUN(args << "ItemSelection().select(/A|C/).deselectSelection(ItemSelection().select(/C/)).str()", outRows.arg("0")); + RUN(args << "ItemSelection().select(/A|C/).deselectSelection(ItemSelection().selectAll()).str()", outRows.arg("")); + + RUN(args << "a = ItemSelection().select(/a/i); b = a.copy(); a !== b", "true\n"); + RUN(args << "a = ItemSelection().select(/a/i); b = a.copy(); a.str() == b.str()", "true\n"); + RUN(args << "a = ItemSelection().select(/a|b/i); b = a.copy(); b.select(/C/); [a.rows(), '', b.rows()]", "0\n1\n\n0\n1\n2\n"); + + RUN(args << "s = ItemSelection().selectAll(); insert(1, 'X'); insert(3, 'Y'); s.invert().str()", outRows.arg("1,3")); + + RUN(args << "ItemSelection().select(/a/i).invert().removeAll().str()", outRows.arg("")); + RUN(args << "read(0,1,2)", "A,,"); + + RUN(args << "ItemSelection().selectAll().removeAll().str()", outRows.arg("")); + RUN(args << "read(0,1,2)", ",,"); + + RUN(args << "write('application/x-tst', 'ghi')", ""); + RUN(args << "write('application/x-tst', 'def')", ""); + RUN(args << "write('application/x-tst', 'abc')", ""); + RUN(args << "read('application/x-tst',0,1,2)", "abc,def,ghi"); + RUN(args << "ItemSelection().select(/e/, 'application/x-tst').str()", outRows.arg("1")); + RUN(args << "ItemSelection().select(/e/, 'application/x-tst').removeAll().str()", outRows.arg("")); + RUN(args << "read('application/x-tst',0,1,2)", "abc,ghi,"); + + RUN(args << "ItemSelection().selectAll().itemAtIndex(0)['application/x-tst']", "abc"); + RUN(args << "ItemSelection().selectAll().itemAtIndex(1)['application/x-tst']", "ghi"); + RUN(args << "ItemSelection().select(/h/, 'application/x-tst').itemAtIndex(0)['application/x-tst']", "ghi"); + RUN(args << "ItemSelection().select(/h/, 'application/x-tst').itemAtIndex(1)['application/x-tst'] == undefined", "true\n"); + + RUN(args << "ItemSelection().select(/ghi/, 'application/x-tst').setItemAtIndex(0, {'application/x-tst': 'def'}).str()", + outRows.arg("1")); + RUN(args << "read('application/x-tst',0,1,2)", "abc,def,"); + + RUN(args << "d = ItemSelection().selectAll().items(); [d.length, str(d[0]['application/x-tst']), str(d[1]['application/x-tst'])]", "2\nabc\ndef\n"); + RUN(args << "ItemSelection().selectAll().setItems([{'application/x-tst': 'xyz'}]).str()", outRows.arg("0,1")); + RUN(args << "read('application/x-tst',0,1,2)", "xyz,def,"); + + RUN(args << "ItemSelection().selectAll().setItemsFormat(mimeItemNotes, 'test1').str()", outRows.arg("0,1")); + RUN(args << "read(mimeItemNotes,0,1,2)", "test1,test1,"); + RUN(args << "read('application/x-tst',0,1,2)", "xyz,def,"); + + RUN(args << "ItemSelection().selectAll().setItems([{'application/x-tst': ByteArray('abc')}]).str()", outRows.arg("0,1")); + RUN(args << "read('application/x-tst',0,1,2)", "abc,def,"); + + RUN(args << "ItemSelection().selectAll().setItemsFormat(mimeItemNotes, ByteArray('test2')).str()", outRows.arg("0,1")); + RUN(args << "read(mimeItemNotes,0,1,2)", "test2,test2,"); + RUN(args << "read('application/x-tst',0,1,2)", "abc,def,"); + + RUN(args << "ItemSelection().selectAll().itemsFormat(mimeItemNotes).map(str)", "test2\ntest2\n"); + RUN(args << "ItemSelection().selectAll().itemsFormat('application/x-tst').map(str)", "abc\ndef\n"); + RUN(args << "ItemSelection().selectAll().itemsFormat(ByteArray('application/x-tst')).map(str)", "abc\ndef\n"); + + RUN(args << "ItemSelection().selectAll().setItemsFormat(mimeItemNotes, undefined).str()", outRows.arg("0,1")); + RUN(args << "read(mimeItemNotes,0,1,2)", ",,"); + RUN(args << "read('application/x-tst',0,1,2)", "abc,def,"); + + RUN(args << "ItemSelection().selectAll().removeAll().str()", outRows.arg("")); + RUN(args << "add" << "C" << "B" << "A", ""); + RUN(args << "ItemSelection().select(/C/).move(1).str()", outRows.arg("1")); + RUN(args << "read(0,1,2)", "A,C,B"); + RUN(args << "ItemSelection().select(/B/).select(/C/).move(1).str()", outRows.arg("2,1")); + RUN(args << "read(0,1,2)", "A,C,B"); + RUN(args << "ItemSelection().select(/A/).move(2).str()", outRows.arg("1")); + RUN(args << "read(0,1,2)", "C,A,B"); + RUN(args << "ItemSelection().select(/C/).select(/B/).move(2).str()", outRows.arg("1,2")); + RUN(args << "read(0,1,2)", "A,C,B"); + + RUN(args << "change(1, mimeItemNotes, 'NOTE'); read(mimeItemNotes,0,1,2)", ",NOTE,"); + RUN(args << "ItemSelection().select(/.*/, mimeItemNotes).str()", outRows.arg("1")); + RUN(args << "ItemSelection().select(undefined, mimeItemNotes).str()", outRows.arg("0,2")); + + // Match nothing if select() argument is not a regular expression. + RUN(args << "add" << "", ""); + RUN(args << "ItemSelection().select('A').str()", outRows.arg("")); +} + +void TestsClasses::classItemSelectionGetCurrent() +{ + const auto tab1 = testTab(1); + const Args args = Args("tab") << tab1 << "separator" << ","; + + RUN("ItemSelection().tab", "CLIPBOARD\n"); + RUN(args << "ItemSelection().tab", tab1 + "\n"); + + RUN(args << "ItemSelection().current().tab", "CLIPBOARD\n"); + RUN(args << "ItemSelection().current().str()", "ItemSelection(tab=\"CLIPBOARD\", rows=[])\n"); + RUN("setCurrentTab" << tab1, ""); + RUN(args << "ItemSelection().current().tab", tab1 + "\n"); + RUN(args << "ItemSelection().current().str()", "ItemSelection(tab=\"" + tab1 + "\", rows=[])\n"); + + RUN(args << "add" << "C" << "B" << "A", ""); + RUN(args << "ItemSelection().current().str()", "ItemSelection(tab=\"" + tab1 + "\", rows=[0])\n"); + + RUN("setCommands([{name: 'test', inMenu: true, shortcuts: ['Ctrl+F1'], cmd: 'copyq: add(ItemSelection().current().str())'}])", ""); + RUN("keys" << "CTRL+F1", ""); + WAIT_ON_OUTPUT(args << "read(0)", "ItemSelection(tab=\"" + tab1 + "\", rows=[0])"); + RUN("keys" << "END" << "SHIFT+UP" << "CTRL+F1", ""); + WAIT_ON_OUTPUT(args << "read(0)", "ItemSelection(tab=\"" + tab1 + "\", rows=[2,3])"); +} + +void TestsClasses::classItemSelectionByteArray() +{ + const auto tab1 = testTab(1); + const Args args = Args("tab") << tab1 << "separator" << ","; + RUN("setCurrentTab" << tab1, ""); + + RUN(args << "add" << "C" << "B" << "A", ""); + RUN(args << "ByteArray(ItemSelection().selectAll().itemAtIndex(0)[mimeText])", "A"); + RUN(args << "str(ItemSelection().selectAll().itemAtIndex(0)[mimeText])", "A\n"); + RUN(args << "write(0, [ItemSelection().selectAll().itemAtIndex(2)])" + << "read(mimeText, 0)", "C"); +} + +void TestsClasses::classItemSelectionSort() +{ + const auto tab1 = testTab(1); + const Args args = Args("tab") << tab1 << "separator" << ","; + const QString outRows("ItemSelection(tab=\"" + tab1 + "\", rows=[%1])\n"); + RUN("setCurrentTab" << tab1, ""); + + RUN(args << "add(2,5,1,3,4)", ""); + RUN(args << "read(0,1,2,3,4)", "4,3,1,5,2"); + + const auto script = R"( + var sel = ItemSelection().selectAll(); + const texts = sel.itemsFormat(mimeText); + sel.sort(function(i,j){ + return texts[i] < texts[j]; + }); + sel.str(); + )"; + RUN(args << script, outRows.arg("3,2,0,4,1")); + RUN(args << "read(0,1,2,3,4)", "1,2,3,4,5"); + RUN(args << "size", "5\n"); +} + +void TestsClasses::classSettings() +{ + TemporaryFile configFile; + const QString fileName = configFile.fileName(); + + RUN("eval" << "s=Settings(str(arguments[1])); print(s.fileName())" << fileName, fileName); + RUN("eval" << "s=Settings(str(arguments[1])); s.isWritable() === true" << fileName, "true\n"); + + RUN("eval" << "s=Settings(str(arguments[1])); s.contains('o1')" << fileName, "false\n"); + RUN("eval" << "s=Settings(str(arguments[1])); s.setValue('o1', 1); s.sync(); s.contains('o1')" << fileName, "true\n"); + RUN("eval" << "s=Settings(str(arguments[1])); s.value('o1')" << fileName, "1\n"); + + RUN("eval" << "s=Settings(str(arguments[1])); s.setValue('o2', 2)" << fileName, ""); + RUN("eval" << "s=Settings(str(arguments[1])); s.value('o2')" << fileName, "2\n"); + + RUN("eval" << "s=Settings(str(arguments[1])); s.setValue('o2', [1,2,3])" << fileName, ""); + RUN("eval" << "s=Settings(str(arguments[1])); s.value('o2')[0]" << fileName, "1\n"); + RUN("eval" << "s=Settings(str(arguments[1])); s.value('o2')[1]" << fileName, "2\n"); + RUN("eval" << "s=Settings(str(arguments[1])); s.value('o2')[2]" << fileName, "3\n"); + + RUN("eval" << "s=Settings(str(arguments[1])); s.setValue('g1/o3', true)" << fileName, ""); + RUN("eval" << "s=Settings(str(arguments[1])); s.value('g1/o3')" << fileName, "true\n"); + + RUN("eval" << "s=Settings(str(arguments[1])); s.childKeys()" << fileName, "o1\no2\n"); + RUN("eval" << "s=Settings(str(arguments[1])); s.allKeys()" << fileName, "g1/o3\no1\no2\n"); + + RUN("eval" << "s=Settings(str(arguments[1])); s.beginGroup('g1'); s.group()" << fileName, "g1\n"); + RUN("eval" << "s=Settings(str(arguments[1])); s.beginGroup('g1'); s.setValue('g1.2/o4', 'test')" << fileName, ""); + RUN("eval" << "s=Settings(str(arguments[1])); s.beginGroup('g1'); s.childGroups()" << fileName, "g1.2\n"); + RUN("eval" << "s=Settings(str(arguments[1])); s.beginGroup('g1'); s.endGroup(); s.childGroups()" << fileName, "g1\n"); + RUN("eval" << "s=Settings(str(arguments[1])); s.value('g1/g1.2/o4')" << fileName, "test\n"); + RUN("eval" << "s=Settings(str(arguments[1])); s.allKeys()" << fileName, "g1/g1.2/o4\ng1/o3\no1\no2\n"); + RUN("eval" << "s=Settings(str(arguments[1])); s.remove('g1/g1.2/o4'); s.allKeys()" << fileName, "g1/o3\no1\no2\n"); + + RUN("eval" << "s=Settings(str(arguments[1])); s.beginWriteArray('a1', 3); s.setArrayIndex(1); s.setValue('o1', 'v1'); s.endArray()" << fileName, ""); + RUN("eval" << "s=Settings(str(arguments[1])); s.beginReadArray('a1')" << fileName, "3\n"); + RUN("eval" << "s=Settings(str(arguments[1])); s.beginReadArray('a1'); s.setArrayIndex(1); s.value('o1');" << fileName, "v1\n"); + + RUN("eval" << "s=Settings(str(arguments[1])); s.clear(); s.allKeys()" << fileName, ""); + + QVERIFY(QFile::remove(fileName)); + QVERIFY(!QFile::exists(fileName)); + RUN("eval" << "s=Settings(str(arguments[1])); s.setValue('o1', 1); s.sync(); File(str(arguments[1])).exists()" << fileName, "true\n"); + QVERIFY(QFile::exists(fileName)); + + QVERIFY(QFile::remove(fileName)); + QVERIFY(!QFile::exists(fileName)); + RUN("eval" << "s=Settings(str(arguments[1])); s.setValue('o1', 1)" << fileName, ""); + QVERIFY(QFile::exists(fileName)); + + const QString appConfigFileName = AppConfig().settings().fileName(); + RUN("Settings().fileName()", QStringLiteral("%1\n").arg(appConfigFileName)); + RUN("Settings().value('Options/tabs')", QStringLiteral("%1\n").arg(clipboardTabName)); +} + +void TestsClasses::calledWithInstance() +{ + // These would fail with the old deprecated Qt Script module. + RUN("f=ByteArray().size; f()", "0\n"); + RUN("f=Dir().path; f()", ".\n"); + RUN("f=File('test').fileName; f()", "test\n"); + RUN("f=TemporaryFile().autoRemove; f()", "true\n"); +} diff --git a/src/tests/tests_classes.h b/src/tests/tests_classes.h new file mode 100644 index 0000000000..ebf917a7f5 --- /dev/null +++ b/src/tests/tests_classes.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "tests/tests_base.h" + +class TestsClasses : public TestsBase +{ + Q_OBJECT + +public: + explicit TestsClasses(const TestInterfacePtr &test) : TestsBase(test) {} + +private slots: + void classByteArray(); + void classFile(); + void classDir(); + void classTemporaryFile(); + void classItemSelection(); + void classItemSelectionGetCurrent(); + void classItemSelectionByteArray(); + void classItemSelectionSort(); + void classSettings(); + void calledWithInstance(); +}; diff --git a/src/tests/tests_cli.cpp b/src/tests/tests_cli.cpp new file mode 100644 index 0000000000..cc2abf6e5f --- /dev/null +++ b/src/tests/tests_cli.cpp @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "test_utils.h" +#include "tests/tests_cli.h" + +#include "common/commandstatus.h" +#include "common/log.h" +#include "common/version.h" + +#include + +void TestsCli::readLog() +{ + QByteArray stdoutActual; + QByteArray stderrActual; + QCOMPARE( run(Args("info") << "log", &stdoutActual, &stderrActual), 0 ); + QVERIFY2( testStderr(stderrActual), stderrActual ); + QCOMPARE( logFileName() + "\n", QString::fromUtf8(stdoutActual) ); + QTRY_VERIFY( !readLogFile(maxReadLogSize).isEmpty() ); + +#define LOGGED_ONCE(PATTERN) \ + QTRY_COMPARE( count(splitLines(readLogFile(maxReadLogSize)), PATTERN), 1 ) + + LOGGED_ONCE( + R"(^CopyQ DEBUG \[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\] : Loading configuration$)"); + + LOGGED_ONCE( + R"(^CopyQ DEBUG \[.*\] : Starting monitor$)"); + + LOGGED_ONCE( + R"(^.*: Clipboard formats to save: .*$)"); +} + +void TestsCli::commandHelp() +{ + QByteArray stdoutActual; + QByteArray stderrActual; + QCOMPARE( run(Args("help"), &stdoutActual, &stderrActual), 0 ); + QVERIFY2( testStderr(stderrActual), stderrActual ); + QVERIFY(!stdoutActual.isEmpty()); + + const QStringList commands = QStringList() + << "show" + << "hide" + << "toggle" + << "menu" + << "exit" + << "help" + << "version" + << "clipboard" + << "copy" + << "paste" + << "action" + << "add" + << "remove"; + + for (const auto &command : commands) { + QCOMPARE( run(Args("help") << command, &stdoutActual, &stderrActual), 0 ); + QVERIFY2( testStderr(stderrActual), stderrActual ); + QVERIFY( !stdoutActual.isEmpty() ); + const QString help = QString::fromUtf8(stdoutActual); + QVERIFY( help.contains(QRegularExpression("\\b" + QRegularExpression::escape(command) + "\\b")) ); + } + + // Print error on unknown function name. + RUN_EXPECT_ERROR("help" << "xxx", CommandException); +} + +void TestsCli::commandVersion() +{ + QByteArray stdoutActual; + QByteArray stderrActual; + QCOMPARE( run(Args("version"), &stdoutActual, &stderrActual), 0 ); + QVERIFY2( testStderr(stderrActual), stderrActual ); + QVERIFY( !stdoutActual.isEmpty() ); + + const QString version = QString::fromUtf8(stdoutActual); + // Version contains application name and version. + QVERIFY( version.contains(QRegularExpression("\\bCopyQ\\b.*" + QRegularExpression::escape(versionString))) ); + // Version contains Qt version. + QVERIFY( version.contains(QRegularExpression("\\bQt:\\s+\\d")) ); +} + +void TestsCli::badCommand() +{ + RUN_EXPECT_ERROR_WITH_STDERR("xxx", CommandException, "xxx"); + RUN_EXPECT_ERROR_WITH_STDERR("tab" << testTab(1) << "yyy", CommandException, "yyy"); + + // Bad command shouldn't create new tab. + QByteArray stdoutActual; + QByteArray stderrActual; + QCOMPARE( run(Args("tab"), &stdoutActual, &stderrActual), 0 ); + QVERIFY2( testStderr(stderrActual), stderrActual ); + QVERIFY( !QString::fromUtf8(stdoutActual) + .contains(QRegularExpression("^" + QRegularExpression::escape(testTab(1)) + "$")) ); +} + +void TestsCli::badSessionName() +{ + RUN_EXPECT_ERROR("-s" << "max_16_characters_in_session_name_allowed" << "", CommandBadSyntax); + RUN_EXPECT_ERROR("-s" << "spaces disallowed" << "", CommandBadSyntax); +} + +void TestsCli::commandCatchExceptions() +{ +#if QT_VERSION < QT_VERSION_CHECK(5,12,0) + SKIP("Internal exceptions are not handled well before Qt 5.12."); +#endif + + RUN("try { removeTab('MISSING') } catch(e) { print(e) }", + "Error: Tab with given name doesn't exist!"); +} diff --git a/src/tests/tests_cli.h b/src/tests/tests_cli.h new file mode 100644 index 0000000000..82bdede8c0 --- /dev/null +++ b/src/tests/tests_cli.h @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "tests/tests_base.h" + +class TestsCli : public TestsBase +{ + Q_OBJECT + +public: + explicit TestsCli(const TestInterfacePtr &test) : TestsBase(test) {} + +private slots: + void readLog(); + void commandHelp(); + void commandVersion(); + void badCommand(); + void badSessionName(); + + void commandCatchExceptions(); +}; diff --git a/src/tests/tests_commands.cpp b/src/tests/tests_commands.cpp new file mode 100644 index 0000000000..b5a2a0f939 --- /dev/null +++ b/src/tests/tests_commands.cpp @@ -0,0 +1,440 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "test_utils.h" +#include "tests/tests_commands.h" + +#include "common/sleeptimer.h" + +void TestsCommands::shortcutCommand() +{ + RUN("setCommands([{name: 'test', inMenu: true, shortcuts: ['Ctrl+F1'], cmd: 'copyq add OK'}])", ""); + RUN("keys" << "CTRL+F1", ""); + WAIT_ON_OUTPUT("read" << "0", "OK"); +} + +void TestsCommands::shortcutCommandOverrideEnter() +{ + RUN("setCommands([{name: 'test', inMenu: true, shortcuts: ['Enter'], cmd: 'copyq add OK'}])", ""); + RUN("keys" << "ENTER" << "ENTER", ""); + WAIT_ON_OUTPUT("read" << "0" << "1", "OK\nOK"); +} + +void TestsCommands::shortcutCommandMatchInput() +{ + // Activate only one of the two actions depending on input MIME format. + const auto script = R"( + function cmd(name) { + var format = 'application/x-copyq-' + name + return { + name: name, + inMenu: true, + shortcuts: ['Ctrl+F1'], + input: format, + cmd: 'copyq add ' + name + } + } + setCommands([ cmd('test1'), cmd('test2') ]) + )"; + RUN(script, ""); + + RUN("write" << "application/x-copyq-test1" << "", ""); + RUN("keys" << "CTRL+F1", ""); + WAIT_ON_OUTPUT("read" << "0", "test1"); + RUN("tab" << QString(clipboardTabName) << "size", "2\n"); + + RUN("write" << "application/x-copyq-test2" << "", ""); + RUN("keys" << "CTRL+F1", ""); + WAIT_ON_OUTPUT("read" << "0", "test2"); + RUN("tab" << QString(clipboardTabName) << "size", "4\n"); +} + +void TestsCommands::shortcutCommandMatchCmd() +{ + const auto tab = testTab(1); + const Args args = Args("tab") << tab; + + // Activate only one of the three actions depending on exit code of command which matches input MIME format. + const auto script = R"( + function cmd(name) { + var format = 'application/x-copyq-' + name + return { + name: name, + inMenu: true, + shortcuts: ['Ctrl+F1'], + matchCmd: 'copyq: str(data("' + format + '")) || fail()', + cmd: 'copyq tab )" + tab + R"( add ' + name + } + } + setCommands([ cmd('test1'), cmd('test2') ]) + )"; + RUN(script, ""); + + RUN("show" << tab, ""); + + RUN(args << "write" << "application/x-copyq-test1" << "1", ""); + WAIT_ON_OUTPUT(args << "keys('Ctrl+F1'); read(0)", "test1"); + + RUN(args << "write" << "application/x-copyq-test2" << "2", ""); + WAIT_ON_OUTPUT(args << "keys('Ctrl+F1'); read(0)", "test2"); +} + +void TestsCommands::shortcutCommandSelectedItemData() +{ + const auto tab1 = testTab(1); + const auto script = R"( + setCommands([{ + name: 'Move Second Selected Item to Other Tab', + inMenu: true, + shortcuts: ['Ctrl+F1'], + output: 'text/plain', + outputTab: ')" + tab1 + R"(', + cmd: 'copyq: selectedItemData(1)["text/plain"]' + }]) + )"; + RUN(script, ""); + + RUN("add" << "C" << "B" << "A", ""); + RUN("selectItems" << "1" << "2", "true\n"); + RUN("keys" << "CTRL+F1", ""); + WAIT_ON_OUTPUT("tab" << tab1 << "read" << "0", "C"); +} + +void TestsCommands::shortcutCommandSetSelectedItemData() +{ + const auto script = R"( + setCommands([{ + name: 'Set Data for Second Selected Item', + inMenu: true, + shortcuts: ['Ctrl+F1'], + cmd: 'copyq: setSelectedItemData(1, {"text/plain": "X", "DATA": "TEST"})' + }]) + )"; + RUN(script, ""); + + RUN("add" << "C" << "B" << "A", ""); + RUN("selectItems" << "1" << "2", "true\n"); + RUN("keys" << "CTRL+F1", ""); + WAIT_ON_OUTPUT("read" << "2", "X"); + RUN("read" << "DATA" << "2", "TEST"); +} + +void TestsCommands::shortcutCommandSelectedItemsData() +{ + const auto tab1 = testTab(1); + const auto script = R"( + setCommands([{ + name: 'Concatenate Selected Items to Other Tab', + inMenu: true, + shortcuts: ['Ctrl+F1'], + output: 'text/plain', + outputTab: ')" + tab1 + R"(', + cmd: 'copyq: d = selectedItemsData();' + + 'for (i in d) { print(d[i][mimeText]); print(",") }' + }]) + )"; + RUN(script, ""); + + RUN("add" << "C" << "B" << "A", ""); + RUN("selectItems" << "1" << "2", "true\n"); + RUN("keys" << "CTRL+F1", ""); + WAIT_ON_OUTPUT("tab" << tab1 << "read" << "0", "B,C,"); +} + +void TestsCommands::shortcutCommandSetSelectedItemsData() +{ + const auto script = R"( + setCommands([{ + name: 'Set Data for Second Selected Item', + inMenu: true, + shortcuts: ['Ctrl+F1'], + cmd: 'copyq: setSelectedItemsData([{"text/plain": "X"}, {"text/plain": "Y"}])' + }]) + )"; + RUN(script, ""); + + RUN("add" << "C" << "B" << "A", ""); + RUN("selectItems" << "1" << "2", "true\n"); + RUN("keys" << "CTRL+F1", ""); + WAIT_ON_OUTPUT("read" << "0" << "1" << "2", "A\nX\nY"); +} + +void TestsCommands::shortcutCommandSelectedAndCurrent() +{ + const auto script = R"( + setCommands([{ + name: 'Set Data for Second Selected Item', + inMenu: true, + shortcuts: ['Ctrl+F1'], + output: 'text/plain', + cmd: 'copyq: print(selectedItems() + "|" + currentItem() + "|" + selectedTab())' + }]) + )"; + RUN(script, ""); + + const auto tab1 = testTab(1); + RUN("tab" << tab1 << "add" << "C" << "B" << "A", ""); + + RUN("tab" << tab1 << "setCurrentTab" << tab1 << "selectItems" << "1" << "2", "true\n"); + RUN("keys" << "CTRL+F1", ""); + WAIT_ON_OUTPUT("tab" << tab1 << "read(0)", "1,2|2|" + tab1.toUtf8()); +} + +void TestsCommands::shortcutCommandMoveSelected() +{ + const QString script = R"( + setCommands([{ + name: 'Move Selected', + inMenu: true, + shortcuts: ['Ctrl+F1'], + output: 'text/plain', + cmd: 'copyq: move(%1); settings("done", 1)' + }]) + )"; + RUN(script.arg(1), ""); + + const Args args = Args("tab") << testTab(1) << "separator" << ","; + RUN("setCurrentTab" << testTab(1), ""); + RUN(args << "add" << "4" << "3" << "2" << "1", ""); + +#define MOVE_SELECTED(EXPECTED_ITEMS) \ + RUN("settings" << "done" << "0" << "keys" << "CTRL+F1", ""); \ + WAIT_ON_OUTPUT("settings" << "done", "1\n"); \ + RUN(args << "read(0,1,2,3,4)", EXPECTED_ITEMS) + + RUN(args << "selectItems" << "1" << "2", "true\n"); + MOVE_SELECTED("1,2,3,4,"); + + RUN(args << "selectItems" << "2" << "3", "true\n"); + MOVE_SELECTED("1,3,4,2,"); + + RUN(script.arg(5), ""); + MOVE_SELECTED("1,3,4,2,"); + + RUN(script.arg(-1), ""); + MOVE_SELECTED("1,3,4,2,"); + + RUN(script.arg(4), ""); + MOVE_SELECTED("1,2,3,4,"); + + RUN(script.arg(0), ""); + MOVE_SELECTED("3,4,1,2,"); + +#undef MOVE_SELECTED +} + +void TestsCommands::automaticCommandIgnore() +{ + const auto script = R"( + setCommands([ + { automatic: true, cmd: 'copyq ignore; copyq add OK' }, + { automatic: true, cmd: 'copyq add "SHOULD NOT BE EXECUTED"' } + ]) + )"; + RUN(script, ""); + WAIT_ON_OUTPUT("commands().length", "2\n"); + + TEST( m_test->setClipboard("SHOULD BE IGNORED 1") ); + WAIT_ON_OUTPUT("read" << "0", "OK"); + RUN("separator" << "," << "read" << "0" << "1" << "2", "OK,,"); + RUN("size", "1\n"); + + TEST( m_test->setClipboard("SHOULD BE IGNORED 2") ); + WAIT_ON_OUTPUT("size", "2\n"); + + RUN("separator" << "," << "read" << "0" << "1" << "2", "OK,OK,"); +} + +void TestsCommands::automaticCommandRemove() +{ + const auto script = R"( + setCommands([ + { automatic: true, remove: true, cmd: 'copyq add OK' }, + { automatic: true, cmd: 'copyq add "SHOULD NOT BE EXECUTED"' } + ]) + )"; + RUN(script, ""); + WAIT_ON_OUTPUT("commands().length", "2\n"); + + TEST( m_test->setClipboard("SHOULD BE IGNORED 1") ); + WAIT_ON_OUTPUT("read" << "0", "OK"); + + TEST( m_test->setClipboard("SHOULD BE IGNORED 2") ); + WAIT_ON_OUTPUT("size", "2\n"); + + RUN("separator" << "," << "read" << "0" << "1" << "2", "OK,OK,"); +} + +void TestsCommands::automaticCommandInput() +{ + const auto script = R"( + setCommands([ + { automatic: true, input: 'DATA', cmd: 'copyq: setData("DATA", "???")' }, + { automatic: true, input: 'text/plain', cmd: 'copyq: setData("text/plain", "OK")' }, + ]) + )"; + RUN(script, ""); + WAIT_ON_OUTPUT("commands().length", "2\n"); + + TEST( m_test->setClipboard("SHOULD BE CHANGED") ); + WAIT_ON_OUTPUT("read" << "0", "OK"); + RUN("read" << "DATA" << "0", ""); +} + +void TestsCommands::automaticCommandRegExp() +{ + const auto script = R"( + setCommands([ + { automatic: true, re: 'SHOULD BE (CHANGED)$', cmd: 'copyq: setData(mimeText, arguments[1])' }, + { automatic: true, cmd: 'copyq: setData("DATA", "DONE")' }, + ]) + )"; + RUN(script, ""); + WAIT_ON_OUTPUT("commands().length", "2\n"); + + TEST( m_test->setClipboard("SHOULD BE CHANGED") ); + WAIT_ON_OUTPUT("read" << "DATA" << "0", "DONE"); + RUN("read" << "0", "CHANGED"); + RUN("remove" << "0", ""); + + TEST( m_test->setClipboard("SHOULD NOT BE CHANGED") ); + WAIT_ON_OUTPUT("read" << "DATA" << "0", "DONE"); + RUN("read" << "0", "SHOULD NOT BE CHANGED"); +} + +void TestsCommands::automaticCommandSetData() +{ + const auto script = R"( + setCommands([{automatic: true, cmd: 'copyq: setData("text/plain", "OK")'}]) + )"; + RUN(script, ""); + WAIT_ON_OUTPUT("commands().length", "1\n"); + + TEST( m_test->setClipboard("SHOULD BE CHANGED") ); + WAIT_ON_OUTPUT("read" << "0", "OK"); +} + +void TestsCommands::automaticCommandOutputTab() +{ + const auto tab1 = testTab(1); + const auto script = R"( + var tab1 = ')" + tab1 + R"(' + setCommands([{automatic: true, cmd: 'copyq: setData(mimeOutputTab, "' + tab1 + '")'}]) + )"; + RUN(script, ""); + WAIT_ON_OUTPUT("commands().length", "1\n"); + + TEST( m_test->setClipboard("TEST") ); + WAIT_ON_OUTPUT("tab" << tab1 << "read" << "0", "TEST"); + RUN("tab" << QString(clipboardTabName) << "size", "0\n"); +} + +void TestsCommands::automaticCommandNoOutputTab() +{ + const auto script = R"( + setCommands([{automatic: true, cmd: 'copyq: removeData(mimeOutputTab)'}]) + )"; + RUN(script, ""); + WAIT_ON_OUTPUT("commands().length", "1\n"); + + TEST( m_test->setClipboard("TEST") ); + waitFor(1000); + RUN("tab" << QString(clipboardTabName) << "size", "0\n"); +} + +void TestsCommands::automaticCommandChaining() +{ + const auto script = R"( + setCommands([ + {automatic: true, cmd: 'copyq: setData(mimeText, 1)'}, + {automatic: true, cmd: 'copyq: setData(mimeText, str(data(mimeText)) + 2)'}, + {automatic: true, cmd: 'copyq: setData(mimeText, str(data(mimeText)) + 3)'} + ]) + )"; + RUN(script, ""); + WAIT_ON_OUTPUT("commands().length", "3\n"); + + TEST( m_test->setClipboard("TEST") ); + WAIT_ON_OUTPUT("read" << "0", "123"); +} + +void TestsCommands::automaticCommandCopyToTab() +{ + const auto tab1 = testTab(1); + const auto script = R"( + setCommands([{automatic: true, tab: ')" + tab1 + R"('}]) + )"; + RUN(script, ""); + WAIT_ON_OUTPUT("commands().length", "1\n"); + + TEST( m_test->setClipboard("TEST") ); + WAIT_ON_OUTPUT("tab" << QString(clipboardTabName) << "read" << "0", "TEST"); + RUN("tab" << tab1 << "read" << "0", "TEST"); +} + +void TestsCommands::automaticCommandStoreSpecialFormat() +{ + const auto script = R"( + setCommands([ + { automatic: true, name: 'CMD1', input: 'test-format' } + ]) + )"; + RUN(script, ""); + WAIT_ON_OUTPUT("commands().length", "1\n"); + + TEST( m_test->setClipboard("DATA", "test-format") ); + WAIT_ON_OUTPUT("separator" << "," << "read" << "test-format" << "0" << "1", "DATA,"); +} + +void TestsCommands::automaticCommandIgnoreSpecialFormat() +{ + const auto script = R"( + setCommands([ + { automatic: true, name: 'CMD1', cmd: 'copyq add CMD1', input: 'test-format', remove: true }, + { automatic: true, name: 'CMD2', cmd: 'copyq add CMD2' }, + { automatic: true, name: 'CMD3', cmd: 'copyq add CMD3', input: 'test-format' } + ]) + )"; + RUN(script, ""); + WAIT_ON_OUTPUT("commands().length", "3\n"); + + TEST( m_test->setClipboard("SHOULD BE IGNORED", "test-format") ); + WAIT_ON_OUTPUT("separator" << "," << "read" << "0" << "1", "CMD1,"); + + TEST( m_test->setClipboard("SHOULD NOT BE IGNORED") ); + WAIT_ON_OUTPUT("separator" << "," << "read" << "0" << "1" << "2" << "3", "SHOULD NOT BE IGNORED,CMD2,CMD1,"); +} + +void TestsCommands::globalCommandInMenu() +{ + const auto script = R"( + setCommands([ + { isGlobalShortcut: true, name: 'test', cmd: 'copyq add test' }, + ]) + )"; + RUN(script, ""); + WAIT_ON_OUTPUT("commands().length", "1\n"); + RUN("menu", ""); + RUN("keys" << trayMenuId << "DOWN" << "ENTER", ""); + RUN("keys" << clipboardBrowserId, ""); + WAIT_ON_OUTPUT("read(0)", "test"); + + RUN("setCommands([])", ""); + WAIT_ON_OUTPUT("commands().length", "0\n"); + + // Test sub-menus + const auto script2 = R"( + setCommands([ + { isGlobalShortcut: true, name: 'test|test1|test2', cmd: 'copyq add test2' }, + ]) + )"; + RUN(script2, ""); + WAIT_ON_OUTPUT("commands().length", "1\n"); + RUN("menu", ""); + RUN("keys" << trayMenuId << "DOWN" << "DOWN" << "ENTER", ""); + waitFor(100); + RUN("keys" << trayMenuId << "ENTER", ""); + waitFor(100); + RUN("keys" << trayMenuId << "ENTER", ""); + RUN("keys" << clipboardBrowserId, ""); + WAIT_ON_OUTPUT("read(0)", "test2"); +} diff --git a/src/tests/tests_commands.h b/src/tests/tests_commands.h new file mode 100644 index 0000000000..c2aec94ef6 --- /dev/null +++ b/src/tests/tests_commands.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "tests/testinterface.h" +#include "tests/tests_base.h" + +class TestsCommands : public TestsBase +{ + Q_OBJECT + +public: + explicit TestsCommands(const TestInterfacePtr &test) : TestsBase(test) {} + +private slots: + void shortcutCommand(); + void shortcutCommandOverrideEnter(); + void shortcutCommandMatchInput(); + void shortcutCommandMatchCmd(); + + void shortcutCommandSelectedItemData(); + void shortcutCommandSetSelectedItemData(); + void shortcutCommandSelectedItemsData(); + void shortcutCommandSetSelectedItemsData(); + void shortcutCommandSelectedAndCurrent(); + void shortcutCommandMoveSelected(); + + void automaticCommandIgnore(); + void automaticCommandRemove(); + void automaticCommandInput(); + void automaticCommandRegExp(); + void automaticCommandSetData(); + void automaticCommandOutputTab(); + void automaticCommandNoOutputTab(); + void automaticCommandChaining(); + void automaticCommandCopyToTab(); + void automaticCommandStoreSpecialFormat(); + void automaticCommandIgnoreSpecialFormat(); + + void globalCommandInMenu(); +}; diff --git a/src/tests/tests_dialogs.cpp b/src/tests/tests_dialogs.cpp new file mode 100644 index 0000000000..05698bca26 --- /dev/null +++ b/src/tests/tests_dialogs.cpp @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "test_utils.h" +#include "tests/tests_dialogs.h" + +#include "common/mimetypes.h" + +void TestsDialogs::createTabDialog() +{ + const auto tab1 = testTab(1); + RUN("keys" + << clipboardBrowserId << "CTRL+T" + << tabDialogLineEditId << ":" + tab1 << "ENTER", ""); + RUN("testSelected", tab1 + "\n"); +} + +void TestsDialogs::showHideAboutDialog() +{ + const auto aboutShortcut = keyNameFor(QKeySequence::QKeySequence::WhatsThis); + RUN("keys" << clipboardBrowserId << aboutShortcut << aboutDialogId, ""); + RUN("keys" << aboutDialogId << "ESCAPE" << clipboardBrowserId, ""); +} + +void TestsDialogs::showHideClipboardDialog() +{ + TEST( m_test->setClipboard("TEST", "test-format") ); + RUN("keys" << clipboardBrowserId << "CTRL+SHIFT+C" << clipboardDialogId, ""); + + RUN("keys" << clipboardDialogId << "DOWN" << "HOME" << clipboardDialogFormatListId, ""); + + RUN("keys" << clipboardDialogFormatListId << keyNameFor(QKeySequence::Copy), ""); +#ifdef Q_OS_WIN + WAIT_FOR_CLIPBOARD("application/x-qt-windows-mime;value=\"test-format\""); +#else + WAIT_FOR_CLIPBOARD("test-format"); +#endif + + RUN("keys" << clipboardDialogId << "ESCAPE" << clipboardBrowserId, ""); +} + +void TestsDialogs::showHideItemDialog() +{ + RUN("write" << "test-format" << "TEST", ""); + RUN("selectItems" << "0", "true\n"); + + RUN("keys" << clipboardBrowserId << "F4" << clipboardDialogId, ""); + + RUN("keys" << clipboardDialogId << "DOWN" << clipboardDialogFormatListId, ""); + RUN("keys" << clipboardDialogFormatListId << keyNameFor(QKeySequence::Copy), ""); + WAIT_FOR_CLIPBOARD("test-format"); + + RUN("keys" << clipboardDialogFormatListId << "DOWN", ""); + RUN("keys" << clipboardDialogFormatListId << keyNameFor(QKeySequence::Copy), ""); + WAIT_FOR_CLIPBOARD("test-format"); + + RUN("keys" << clipboardDialogId << "ESCAPE" << clipboardBrowserId, ""); +} + +void TestsDialogs::showHideLogDialog() +{ + RUN("keys" << clipboardBrowserId << "F12" << logDialogId, ""); + + RUN("keys" << logDialogId << "CTRL+A" << "CTRL+C" << logDialogId, ""); + const QByteArray expectedLog = "Starting callback: onStart"; + TEST( m_test->verifyClipboard(expectedLog, mimeHtml, false) ); + + RUN("keys" << logDialogId << "ESCAPE" << clipboardBrowserId, ""); +} + +void TestsDialogs::showHideActionHandlerDialog() +{ + RUN("keys" << clipboardBrowserId << "CTRL+SHIFT+Z" << actionHandlerDialogId, ""); + + RUN("keys" << actionHandlerFilterId << ":onstart" << "TAB" << actionHandlerTableId, ""); + + RUN("keys" << actionHandlerTableId << "RIGHT" << "CTRL+C", ""); + WAIT_FOR_CLIPBOARD("copyq onStart"); + + RUN("keys" << actionHandlerDialogId << "ESCAPE" << clipboardBrowserId, ""); +} + +void TestsDialogs::shortcutDialogAddShortcut() +{ +#ifdef Q_OS_MAC + SKIP("Mnemonic for focusing shortcut button doesn't work on OS X"); +#endif + + RUN("setCommands([{name: 'test', inMenu: true, cmd: 'copyq add OK'}])", ""); + RUN("commands()[0].shortcuts", ""); + + RUN("keys" << clipboardBrowserId << "F6" << commandDialogId, ""); + RUN("keys" << commandDialogId << "ALT+S" << shortcutButtonId, ""); + RUN("keys" << shortcutButtonId << "Space" << shortcutDialogId, ""); + RUN("keys" << shortcutDialogId << "CTRL+F1" << shortcutButtonId, ""); + + RUN("keys" << commandDialogId << "ESCAPE" << commandDialogSaveButtonId, ""); + RUN("keys" << commandDialogSaveButtonId << "Enter" << clipboardBrowserId, ""); + RUN("commands()[0].shortcuts", "ctrl+f1\n"); +} + +void TestsDialogs::shortcutDialogAddTwoShortcuts() +{ +#ifdef Q_OS_MAC + SKIP("Mnemonic for focusing shortcut button doesn't work on OS X"); +#endif + + RUN("setCommands([{name: 'test', inMenu: true, shortcuts: ['ctrl+f1'], cmd: 'copyq add OK'}])", ""); + RUN("commands()[0].shortcuts", "ctrl+f1\n"); + + RUN("keys" << clipboardBrowserId << "F6" << commandDialogId, ""); + RUN("keys" << commandDialogId << "ALT+S" << shortcutButtonId, ""); + + RUN("keys" << shortcutButtonId << "TAB" << shortcutButtonId, ""); + RUN("keys" << shortcutButtonId << "Space" << shortcutDialogId, ""); + RUN("keys" << shortcutDialogId << "F1" << shortcutButtonId, ""); + + RUN("keys" << shortcutButtonId << "Space" << shortcutDialogId, ""); + RUN("keys" << shortcutDialogId << "F2" << shortcutButtonId, ""); + + RUN("keys" << commandDialogId << "ESCAPE" << commandDialogSaveButtonId, ""); + RUN("keys" << commandDialogSaveButtonId << "Enter" << clipboardBrowserId, ""); + RUN("commands()[0].shortcuts", "ctrl+f1\nf1\nf2\n"); +} + +void TestsDialogs::shortcutDialogChangeShortcut() +{ +#ifdef Q_OS_MAC + SKIP("Mnemonic for focusing shortcut button doesn't work on OS X"); +#endif + + RUN("setCommands([{name: 'test', inMenu: true, shortcuts: ['f1','f2','f3'], cmd: 'copyq add OK'}])", ""); + RUN("commands()[0].shortcuts", "f1\nf2\nf3\n"); + + RUN("keys" << clipboardBrowserId << "F6" << commandDialogId, ""); + RUN("keys" << commandDialogId << "ALT+S" << shortcutButtonId, ""); + RUN("keys" << commandDialogId << "TAB" << shortcutButtonId, ""); + RUN("keys" << shortcutButtonId << "Space" << shortcutDialogId, ""); + RUN("keys" << shortcutDialogId << "F4" << shortcutButtonId, ""); + + RUN("keys" << commandDialogId << "ESCAPE" << commandDialogSaveButtonId, ""); + RUN("keys" << commandDialogSaveButtonId << "Enter" << clipboardBrowserId, ""); + RUN("commands()[0].shortcuts", "f1\nf4\nf3\n"); +} + +void TestsDialogs::shortcutDialogSameShortcut() +{ +#ifdef Q_OS_MAC + SKIP("Mnemonic for focusing shortcut button doesn't work on OS X"); +#endif + + RUN("setCommands([{name: 'test', inMenu: true, shortcuts: ['ctrl+f1'], cmd: 'copyq add OK'}])", ""); + RUN("commands()[0].shortcuts", "ctrl+f1\n"); + + RUN("keys" << clipboardBrowserId << "F6" << commandDialogId, ""); + RUN("keys" << commandDialogId << "ALT+S" << shortcutButtonId, ""); + RUN("keys" << shortcutButtonId << "TAB" << shortcutButtonId, ""); + RUN("keys" << shortcutButtonId << "Space" << shortcutDialogId, ""); + RUN("keys" << shortcutDialogId << "CTRL+F1" << shortcutButtonId, ""); + + RUN("keys" << commandDialogId << "ESCAPE" << clipboardBrowserId, ""); + RUN("commands()[0].shortcuts", "ctrl+f1\n"); +} + +void TestsDialogs::shortcutDialogCancel() +{ +#ifdef Q_OS_MAC + SKIP("Mnemonic for focusing shortcut button doesn't work on OS X"); +#endif + + RUN("setCommands([{name: 'test', inMenu: true, shortcuts: ['ctrl+f1'], cmd: 'copyq add OK'}])", ""); + RUN("commands()[0].shortcuts", "ctrl+f1\n"); + + RUN("keys" << clipboardBrowserId << "F6" << commandDialogId, ""); + RUN("keys" << commandDialogId << "ALT+S" << shortcutButtonId, ""); + RUN("keys" << commandDialogId << "TAB" << shortcutButtonId, ""); + RUN("keys" << shortcutButtonId << "Space" << shortcutDialogId, ""); + RUN("keys" << shortcutDialogId << "TAB" << "focus:ShortcutDialog", ""); + RUN("keys" << "ESCAPE" << shortcutButtonId, ""); + + RUN("keys" << commandDialogId << "ESCAPE" << clipboardBrowserId, ""); + RUN("commands()[0].shortcuts", "ctrl+f1\n"); +} + +void TestsDialogs::actionDialogCancel() +{ + const auto script = R"( + setCommands([{ + name: 'test', + inMenu: true, + shortcuts: ['ctrl+f1'], + wait: true, + cmd: 'copyq settings test SHOULD_NOT_BE_SET' + }]) + )"; + RUN(script, ""); + + RUN("keys" << clipboardBrowserId << "CTRL+F1" << actionDialogId, ""); + RUN("keys" << actionDialogId << "ESCAPE" << clipboardBrowserId, ""); + RUN("settings" << "test", ""); +} + +void TestsDialogs::actionDialogAccept() +{ + const auto script = R"( + setCommands([{ + name: 'test', + inMenu: true, + shortcuts: ['ctrl+f1'], + wait: true, + cmd: 'copyq settings test SHOULD_BE_SET' + }]) + )"; + RUN(script, ""); + + RUN("keys" << clipboardBrowserId << "CTRL+F1" << actionDialogId, ""); + // Can't focus configuration checkboxes on OS X +#ifdef Q_OS_MAC + RUN("keys" << actionDialogId << "BACKTAB" << "ENTER" << clipboardBrowserId, ""); +#else + RUN("keys" << actionDialogId << "ENTER" << clipboardBrowserId, ""); +#endif + WAIT_ON_OUTPUT("settings" << "test", "SHOULD_BE_SET"); +} + +void TestsDialogs::actionDialogSelection() +{ + const auto script = R"( + setCommands([{ + name: 'test', + inMenu: true, + shortcuts: ['ctrl+f1'], + wait: true, + cmd: 'copyq settings test %1' + }]) + )"; + RUN(script, ""); + + const auto tab = testTab(1); + const auto args = Args("tab") << tab; + RUN(args << "add" << "C" << "B" << "A", ""); + RUN("setCurrentTab" << tab, ""); + RUN(args << "selectItems" << "0" << "2", "true\n"); + + RUN("keys" << clipboardBrowserId << "CTRL+F1" << actionDialogId, ""); + // Can't focus configuration checkboxes on OS X +#ifdef Q_OS_MAC + RUN("keys" << actionDialogId << "BACKTAB" << "ENTER" << clipboardBrowserId, ""); +#else + RUN("keys" << actionDialogId << "ENTER" << clipboardBrowserId, ""); +#endif + WAIT_ON_OUTPUT("settings" << "test", "A\nC"); +} + +void TestsDialogs::actionDialogSelectionInputOutput() +{ + const auto script = R"( + setCommands([{ + name: 'test', + inMenu: true, + shortcuts: ['ctrl+f1'], + wait: true, + input: 'text/plain', + output: 'text/plain', + cmd: 'copyq input' + }]) + )"; + RUN(script, ""); + + const auto tab = testTab(1); + const auto args = Args("tab") << tab; + RUN(args << "add" << "C" << "B" << "A", ""); + RUN("setCurrentTab" << tab, ""); + RUN(args << "selectItems" << "0" << "2", "true\n"); + + RUN("keys" << clipboardBrowserId << "CTRL+F1" << actionDialogId, ""); + // Can't focus configuration checkboxes on OS X +#ifdef Q_OS_MAC + RUN("keys" << actionDialogId << "BACKTAB" << "ENTER" << clipboardBrowserId, ""); +#else + RUN("keys" << actionDialogId << "ENTER" << clipboardBrowserId, ""); +#endif + WAIT_ON_OUTPUT(args << "read" << "0", "A\nC"); +} + +void TestsDialogs::exitConfirm() +{ + RUN("keys" << clipboardBrowserId << "CTRL+Q" << confirmExitDialogId, ""); + RUN("keys" << confirmExitDialogId << "ENTER", ""); + TEST( m_test->waitForServerToStop() ); +} + +void TestsDialogs::exitNoConfirm() +{ + RUN("config" << "confirm_exit" << "false", "false\n"); + RUN("keys" << clipboardBrowserId << "CTRL+Q", ""); + TEST( m_test->waitForServerToStop() ); +} + +void TestsDialogs::exitStopCommands() +{ + RUN("config" << "confirm_exit" << "false", "false\n"); + RUN("action" << "copyq sleep 999999", ""); + RUN("keys" << clipboardBrowserId << "CTRL+Q", ""); + RUN("keys" << confirmExitDialogId << "ENTER", ""); + TEST( m_test->waitForServerToStop() ); +} diff --git a/src/tests/tests_dialogs.h b/src/tests/tests_dialogs.h new file mode 100644 index 0000000000..ecbc185733 --- /dev/null +++ b/src/tests/tests_dialogs.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "tests/testinterface.h" +#include "tests/tests_base.h" + +class TestsDialogs : public TestsBase +{ + Q_OBJECT + +public: + explicit TestsDialogs(const TestInterfacePtr &test) : TestsBase(test) {} + +private slots: + void createTabDialog(); + + void showHideAboutDialog(); + void showHideClipboardDialog(); + void showHideItemDialog(); + void showHideLogDialog(); + void showHideActionHandlerDialog(); + + void shortcutDialogAddShortcut(); + void shortcutDialogAddTwoShortcuts(); + void shortcutDialogChangeShortcut(); + void shortcutDialogSameShortcut(); + void shortcutDialogCancel(); + + void actionDialogCancel(); + void actionDialogAccept(); + void actionDialogSelection(); + void actionDialogSelectionInputOutput(); + + void exitConfirm(); + void exitNoConfirm(); + void exitStopCommands(); +}; diff --git a/src/tests/tests_items.cpp b/src/tests/tests_items.cpp new file mode 100644 index 0000000000..137bf37e37 --- /dev/null +++ b/src/tests/tests_items.cpp @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "test_utils.h" +#include "tests/tests_items.h" + +#include "common/mimetypes.h" +#include "common/commandstatus.h" + +void TestsItems::configMaxitems() +{ + RUN("config" << "maxitems" << "3", "3\n"); + RUN("add" << "A" << "B" << "C", ""); + RUN("add" << "D", ""); + RUN("separator" << " " << "read" << "0" << "1" << "2", "D C B"); + RUN("size", "3\n"); + + RUN("add" << "E" << "F", ""); + RUN("separator" << " " << "read" << "0" << "1" << "2", "F E D"); + RUN("size", "3\n"); + + RUN("config" << "maxitems" << "2", "2\n"); + RUN("separator" << " " << "read" << "0" << "1", "F E"); + RUN("size", "2\n"); + + // Adding too many items fails. + RUN_EXPECT_ERROR("add" << "1" << "2" << "3", CommandException); + RUN("separator" << " " << "read" << "0" << "1", "F E"); + RUN("size", "2\n"); + + // Single item in tabs. + RUN("config" << "maxitems" << "1", "1\n"); + RUN("separator" << " " << "read" << "0", "F"); + RUN("size", "1\n"); + + RUN("add" << "G", ""); + RUN("separator" << " " << "read" << "0", "G"); + RUN("size", "1\n"); + + RUN("write" << "1" << "text/plain" << "H", ""); + RUN("separator" << " " << "read" << "0", "H"); + RUN("size", "1\n"); + + // No items in tabs. + RUN("config" << "maxitems" << "0", "0\n"); + RUN("size", "0\n"); + + RUN_EXPECT_ERROR("add" << "1", CommandException); + RUN_EXPECT_ERROR("write" << "1", CommandException); + RUN("size", "0\n"); + + // Invalid value. + RUN("config" << "maxitems" << "-99", "0\n"); + RUN("size", "0\n"); +} + +void TestsItems::keysAndFocusing() +{ + RUN("disable", ""); + RUN("keys" << clipboardBrowserId << "CTRL+T", ""); + WAIT_ON_OUTPUT("currentWindowTitle", appWindowTitle("New Tab")); + + RUN("keys" << tabDialogLineEditId << "ESC", ""); + WAIT_ON_OUTPUT("currentWindowTitle", appWindowTitle("*Clipboard Storing Disabled*")); + + RUN("enable", ""); +} + +void TestsItems::selectItems() +{ + const auto tab = QString(clipboardTabName); + RUN("add" << "C" << "B" << "A", ""); + + RUN("keys" << "RIGHT" << "SHIFT+DOWN" << "SHIFT+DOWN", ""); + RUN("testSelected", tab + " 2 0 1 2\n"); + + RUN("keys" << "SHIFT+UP", ""); + RUN("testSelected", tab + " 1 0 1\n"); + + RUN("keys" << "END", ""); + RUN("testSelected", tab + " 2 2\n"); + + RUN("keys" << "SHIFT+UP", ""); + RUN("testSelected", tab + " 1 1 2\n"); + + RUN("keys" << "CTRL+A", ""); + RUN("testSelected", tab + " 1 0 1 2\n"); + + // CTRL+SPACE toggles current item selection + RUN("add" << "D", ""); + RUN("keys" << "PGUP" << "CTRL+SHIFT+DOWN" << "CTRL+SHIFT+DOWN", ""); + RUN("testSelected", tab + " 2 0\n"); + RUN("keys" << "CTRL+SPACE", ""); + RUN("testSelected", tab + " 2 0 2\n"); + RUN("keys" << "SHIFT+DOWN", ""); + RUN("testSelected", tab + " 3 0 2 3\n"); + RUN("keys" << "CTRL+SPACE", ""); + RUN("testSelected", tab + " 3 0 2\n"); +} + +void TestsItems::moveItems() +{ + const auto tab = QString(clipboardTabName); + const auto args = Args() << "separator" << " "; + RUN(args << "add" << "C" << "B" << "A", ""); + + // move item one down + RUN(args << "keys" << "RIGHT" << "CTRL+DOWN", ""); + RUN(args << "read" << "0" << "1" << "2", "B A C"); + RUN(args << "testSelected", tab + " 1 1\n"); + + // move items to top + RUN(args << "keys" << "SHIFT+DOWN" << "CTRL+HOME", ""); + RUN(args << "read" << "0" << "1" << "2", "A C B"); + RUN(args << "testSelected", tab + " 1 0 1\n"); +} + +void TestsItems::deleteItems() +{ + const auto tab = QString(clipboardTabName); + const auto args = Args() << "separator" << ","; + RUN(args << "add" << "C" << "B" << "A", ""); + + // delete first item + RUN(args << "keys" << "RIGHT" << m_test->shortcutToRemove(), ""); + RUN(args << "read" << "0" << "1" << "2", "B,C,"); + RUN(args << "testSelected", tab + " 0 0\n"); + + // select all and delete + RUN(args << "keys" << "CTRL+A" << m_test->shortcutToRemove(), ""); + RUN(args << "size", "0\n"); +} + +void TestsItems::searchItems() +{ + RUN("add" << "a" << "b" << "c", ""); + RUN("keys" << ":b" << "TAB", ""); + RUN("testSelected", QString(clipboardTabName) + " 1 1\n"); +} + +void TestsItems::searchItemsAndSelect() +{ + RUN("add" << "xx1" << "a" << "xx2" << "c" << "xx3" << "d", ""); + RUN("keys" << ":xx" << filterEditId, ""); + RUN("testSelected", QString(clipboardTabName) + " 1 1\n"); + + RUN("keys" << filterEditId << "DOWN" << filterEditId, ""); + RUN("testSelected", QString(clipboardTabName) + " 3 3\n"); + + RUN("keys" << filterEditId << "DOWN" << filterEditId, ""); + RUN("testSelected", QString(clipboardTabName) + " 5 5\n"); + + RUN("keys" << filterEditId << "TAB" << clipboardBrowserId, ""); +} + +void TestsItems::searchItemsAndCopy() +{ + RUN("add" << "TEST_ITEM", ""); + RUN("keys" << ":test" << "CTRL+C" << filterEditId, ""); + WAIT_FOR_CLIPBOARD("TEST_ITEM"); +} + +void TestsItems::searchRowNumber() +{ + RUN("add" << "d2" << "c" << "b2" << "a", ""); + + RUN("keys" << ":2" << "TAB", ""); + RUN("testSelected", QString(clipboardTabName) + " 1 1\n"); + RUN("keys" << "CTRL+A", ""); + RUN("testSelected", QString(clipboardTabName) + " 1 1 3\n"); + + RUN("config" << "row_index_from_one" << "false", "false\n"); + RUN("keys" << ":2" << "TAB", ""); + RUN("testSelected", QString(clipboardTabName) + " 2 2\n"); + RUN("keys" << "CTRL+A", ""); + RUN("testSelected", QString(clipboardTabName) + " 2 1 2 3\n"); +} + +void TestsItems::searchAccented() +{ + RUN("add" << "a" << "väčšina" << "a", ""); + RUN("filter" << "vacsina", ""); + WAIT_ON_OUTPUT("testSelected", QByteArray(clipboardTabName) + " 1 1\n"); +} + +void TestsItems::copyItems() +{ + const auto tab = QString(clipboardTabName); + RUN("add" << "C" << "B" << "A", ""); + + // Select and copy all items. + RUN("keys" << "CTRL+A" << keyNameFor(QKeySequence::Copy), ""); + + // This seems to be required on Windows. + WAIT_ON_OUTPUT("clipboard", "A\nB\nC"); + + // Paste all items. + RUN("keys" << keyNameFor(QKeySequence::Paste), ""); + RUN("separator" << " " << "read" << "0" << "1" << "2" << "3" << "4" << "5", "A B C A B C"); + RUN("size", "6\n"); +} + +void TestsItems::selectAndCopyOrder() +{ + const auto tab = testTab(1); + const Args args = Args("tab") << tab << "separator" << " "; + RUN(args << "add" << "D" << "C" << "B" << "A", ""); + RUN("setCurrentTab" << tab, ""); + + RUN("keys" << "END" << "SHIFT+UP" << "SHIFT+UP" << "SHIFT+UP", ""); + RUN(args << "testSelected", tab + " 0 0 1 2 3\n"); + + RUN("keys" << keyNameFor(QKeySequence::Copy), ""); + WAIT_ON_OUTPUT("clipboard", "D\nC\nB\nA"); +} + +void TestsItems::sortAndReverse() +{ + const auto tab = testTab(1); + const Args args = Args("tab") << tab << "separator" << " "; + RUN(args << "add" << "D" << "A" << "C" << "B", ""); + RUN("setCurrentTab" << tab, ""); + + RUN("keys" << "CTRL+A", ""); + RUN(args << "testSelected", tab + " 0 0 1 2 3\n"); + + RUN("keys" << "CTRL+SHIFT+S", ""); + RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "A B C D "); + RUN(args << "testSelected", tab + " 1 0 1 2 3\n"); + RUN("keys" << keyNameFor(QKeySequence::Copy), ""); + WAIT_ON_OUTPUT("clipboard", "A\nB\nC\nD"); + + RUN("keys" << "CTRL+SHIFT+R", ""); + RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "D C B A "); + RUN(args << "testSelected", tab + " 2 0 1 2 3\n"); + RUN("keys" << keyNameFor(QKeySequence::Copy), ""); + WAIT_ON_OUTPUT("clipboard", "D\nC\nB\nA"); + + RUN("keys" << "CTRL+SHIFT+R", ""); + RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "A B C D "); + RUN(args << "testSelected", tab + " 1 0 1 2 3\n"); +} + +void TestsItems::editItems() +{ + RUN("config" << "edit_ctrl_return" << "true", "true\n"); + + RUN("add" << "Line 4" << "Line 1", ""); + + RUN("keys" + << clipboardBrowserId << "F2" + << editorId << "END" << "ENTER" << ":Line 2" << "F2", ""); + RUN("read" << "0", "Line 1\nLine 2"); + + RUN("keys" + << clipboardBrowserId << "DOWN" << "F2" + << editorId << "HOME" << ":Line 3" << "ENTER" << "F2", ""); + RUN("read" << "1", "Line 3\nLine 4"); + RUN("read" << "0", "Line 1\nLine 2"); + + // Edit multiple items + RUN("keys" + << clipboardBrowserId << "SHIFT+UP" << "F2" + << editorId << "END" << "ENTER" << ":Line 5" << "F2", ""); + RUN("read" << "0", "Line 3\nLine 4\nLine 1\nLine 2\nLine 5"); + RUN("read" << "1", "Line 1\nLine 2"); + RUN("read" << "2", "Line 3\nLine 4"); +} + +void TestsItems::createNewItem() +{ + RUN("config" << "edit_ctrl_return" << "true", "true\n"); + + RUN("keys" << "CTRL+N" << editorId << ":Line 1" << "ENTER" << ":Line 2" << "F2", ""); + RUN("read" << "0", "Line 1\nLine 2"); + + RUN("keys" << "CTRL+N" << editorId << ":Line 3" << "ENTER" << ":Line 4" << "F2", ""); + RUN("read" << "0", "Line 3\nLine 4"); +} + +void TestsItems::editNotes() +{ + RUN("add" << "B" << "A", ""); + + RUN("config" << "editor" << "", "\n"); + RUN("keys" << "SHIFT+F2" << ":A Note" << "F2", ""); + RUN("read" << mimeText << "0" << mimeItemNotes << "0" << "F2", "A\nA Note"); + RUN("read" << mimeText << "1" << mimeItemNotes << "1" << "F2", "B\n"); + + RUN("keys" << "DOWN", ""); + + RUN("keys" << "SHIFT+F2" << ":B Note" << "F2", ""); + RUN("read" << mimeText << "1" << mimeItemNotes << "1" << "F2", "B\nB Note"); + RUN("read" << mimeText << "0" << mimeItemNotes << "0" << "F2", "A\nA Note"); +} diff --git a/src/tests/tests_items.h b/src/tests/tests_items.h new file mode 100644 index 0000000000..87c38a0f3e --- /dev/null +++ b/src/tests/tests_items.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "tests/testinterface.h" +#include "tests/tests_base.h" + +class TestsItems : public TestsBase +{ + Q_OBJECT + +public: + explicit TestsItems(const TestInterfacePtr &test) : TestsBase(test) {} + +private slots: + void configMaxitems(); + + void keysAndFocusing(); + + void selectItems(); + + void moveItems(); + void deleteItems(); + void searchItems(); + void searchItemsAndSelect(); + void searchItemsAndCopy(); + void searchRowNumber(); + void searchAccented(); + void copyItems(); + void selectAndCopyOrder(); + + void sortAndReverse(); + + void editItems(); + void createNewItem(); + void editNotes(); +}; diff --git a/src/tests/tests_script_commands.cpp b/src/tests/tests_script_commands.cpp new file mode 100644 index 0000000000..7f75ca1890 --- /dev/null +++ b/src/tests/tests_script_commands.cpp @@ -0,0 +1,396 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "test_utils.h" +#include "tests/tests_script_commands.h" + +#include "common/commandstatus.h" +#include "common/mimetypes.h" +#include "common/sleeptimer.h" + +void TestsScriptCommands::scriptCommandLoaded() +{ + const auto script = R"( + setCommands([{ + isScript: true, + cmd: 'add("LOADED")' + }]) + )"; + RUN(script, ""); + RUN("read(0)", "LOADED"); +} + +void TestsScriptCommands::scriptCommandAddFunction() +{ + const auto script = R"( + setCommands([{ + isScript: true, + cmd: 'global.test = function() { return "TEST"; }' + }]) + )"; + RUN(script, ""); + RUN("test", "TEST\n"); +} + +void TestsScriptCommands::scriptCommandOverrideFunction() +{ + const auto script = R"( + setCommands([{ + isScript: true, + cmd: 'popup = function(msg) { return msg; }' + }]) + )"; + RUN(script, ""); + RUN("popup" << "test" << "xxx", "test"); +} + +void TestsScriptCommands::scriptCommandEnhanceFunction() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: 'var popup_ = popup; global.popup = function(msg) { popup_(msg); return msg + 1; }' + }, + { + isScript: true, + cmd: 'var popup_ = popup; global.popup = function(msg) { return popup_(msg) + msg + 2; }' + }, + ]) + )"; + RUN(script, ""); + RUN("popup" << "test", "test1test2\n"); +} + +void TestsScriptCommands::scriptCommandEndingWithComment() +{ + /* + With Qml scripts in Qt 5, it's not possible to execute script in new context, + only in the global one. + + Workaround is to wrap the script properly in a new function: + + function() { + %1 + }() + + (Unfortunately, it's still possible to escape the new context with a script injection.) + */ + + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: 'global.popup = function(msg) { return msg + 1; } // TEST' + }, + ]) + )"; + RUN(script, ""); + RUN("popup" << "test", "test1\n"); +} + +void TestsScriptCommands::scriptCommandWithError() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + name: 'bad_script', + cmd: 'if (env("COPYQ_TEST_THROW") == "1") throw Error("BAD SCRIPT")' + }, + ]) + )"; + RUN(script, ""); + m_test->setEnv("COPYQ_TEST_THROW", "1"); + RUN_EXPECT_ERROR_WITH_STDERR( + "", CommandError, + "ScriptError: BAD SCRIPT\n" + "\n" + "--- backtrace ---\n" + ); + RUN_EXPECT_ERROR_WITH_STDERR( + "", CommandError, + "\neval:source@\n" + "--- end backtrace ---\n" + ); + m_test->setEnv("COPYQ_TEST_THROW", "0"); +} + +void TestsScriptCommands::scriptPaste() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: 'global.paste = function() { add("PASTE") }' + }, + ]) + )"; + RUN(script, ""); + RUN("add(1)", ""); + RUN("keys" << clipboardBrowserId << "ENTER", ""); + WAIT_ON_OUTPUT("read(0)", "PASTE"); +} + +void TestsScriptCommands::scriptOnTabSelected() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: 'global.onTabSelected = function() { add(selectedTab()) }' + }, + ]) + )"; + RUN(script, ""); + + const auto tab1 = testTab(1); + const auto tab2 = testTab(2); + RUN("show" << tab1, ""); + WAIT_ON_OUTPUT("tab" << tab1 << "read(0)", tab1); + RUN("show" << tab2, ""); + WAIT_ON_OUTPUT("tab" << tab2 << "read(0)", tab2); +} + +void TestsScriptCommands::scriptOnItemsRemoved() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: ` + global.onItemsRemoved = function() { + items = ItemSelection().current().items(); + tab(tab()[0]); + add("R0:" + str(items[0][mimeText])); + add("R1:" + str(items[1][mimeText])); + } + ` + }, + ]) + )"; + RUN(script, ""); + const auto tab1 = testTab(1); + RUN("tab" << tab1 << "add(3,2,1,0)", ""); + RUN("tab" << tab1 << "remove(1,2)", ""); + WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2,)", "R1:2,R0:1,"); + + // Cancel item removal + const auto script2 = R"( + setCommands([ + { + isScript: true, + cmd: "global.onItemsRemoved = global.fail", + }, + ]) + )"; + RUN(script2, ""); + const auto tab2 = testTab(2); + RUN("tab" << tab2 << "add(3,2,1,0)", ""); + RUN("tab" << tab2 << "remove(1,2)", ""); + waitFor(1000); + RUN("tab" << tab2 << "separator" << "," << "read(0,1,2,3,4)", "0,1,2,3,"); + + // Avoid crash if the tab itself is removed while removing items + const auto script3 = R"( + setCommands([ + { + isScript: true, + cmd: ` + global.onItemsRemoved = function() { + removeTab(selectedTab()) + } + ` + }, + ]) + )"; + RUN(script3, ""); + const auto tab3 = testTab(3); + RUN("tab" << tab3 << "add(3,2,1,0)", ""); + RUN("tab" << tab3 << "remove(1,2)", ""); + waitFor(1000); + RUN("tab" << tab3 << "separator" << "," << "read(0,1,2,3,4)", ",,,,"); +} + +void TestsScriptCommands::scriptOnItemsAdded() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: ` + global.onItemsAdded = function() { + sel = ItemSelection().current(); + items = sel.items(); + for (i = 0; i < items.length; ++i) + items[i][mimeText] = "A:" + str(items[i][mimeText]) + sel.setItems(items); + } + ` + }, + ]) + )"; + RUN(script, ""); + const auto tab1 = testTab(1); + RUN("tab" << tab1 << "add(1,0)", ""); + WAIT_ON_OUTPUT("tab" << tab1 << "separator" << "," << "read(0,1,2)", "A:0,A:1,"); +} + +void TestsScriptCommands::scriptOnItemsChanged() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: ` + global.onItemsChanged = function() { + if (selectedTab() == tab()[0]) abort(); + items = ItemSelection().current().items(); + tab(tab()[0]); + add("C:" + str(items[0][mimeText])); + } + ` + }, + ]) + )"; + RUN(script, ""); + const auto tab1 = testTab(1); + RUN("tab" << tab1 << "add(0)", ""); + RUN("tab" << tab1 << "change(0, mimeText, 'A')", ""); + WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2)", "C:A,,"); + RUN("tab" << tab1 << "change(0, mimeText, 'B')", ""); + WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2)", "C:B,C:A,"); +} + +void TestsScriptCommands::scriptOnItemsLoaded() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: ` + global.onItemsLoaded = function() { + if (selectedTab() == tab()[0]) abort(); + tab(tab()[0]); + add(selectedTab()); + } + ` + }, + ]) + )"; + RUN(script, ""); + + const auto tab1 = testTab(1); + RUN("show" << tab1, ""); + WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2)", tab1 + ",,"); + + const auto tab2 = testTab(2); + RUN("show" << tab2, ""); + WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2)", tab2 + "," + tab1 + ","); +} + +void TestsScriptCommands::scriptEventMaxRecursion() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: `global.onItemsRemoved = function() { + const toRemove = str(selectedItemData(0)[mimeText]); + const newItem = (toRemove == "X") ? "A" : ("WRONG:" + toRemove); + add(newItem); + remove(size()-1); + }` + }, + ]) + )"; + RUN(script, ""); + RUN("add('X'); remove(0)", ""); + WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2,3,4,5,6,7,8,9,10)", "A,A,A,A,A,A,A,A,A,A,"); + waitFor(200); + RUN("separator" << "," << "read(0,1,2,3,4,5,6,7,8,9,10)", "A,A,A,A,A,A,A,A,A,A,"); +} + +void TestsScriptCommands::scriptSlowCollectOverrides() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: 'global.onTabSelected = function() { add(selectedTab()) }' + }, + { + isScript: true, + cmd: ` + var collectOverrides_ = global.collectOverrides; + global.collectOverrides = function() { sleep(1000); collectOverrides_() } + ` + }, + ]) + )"; + RUN(script, ""); + + const auto tab1 = testTab(1); + RUN("show" << tab1, ""); + WAIT_ON_OUTPUT("tab" << tab1 << "read(0)", tab1); +} + +void TestsScriptCommands::displayCommand() +{ + const auto testMime = COPYQ_MIME_PREFIX "test"; + const auto script = QString(R"( + setCommands([{ + display: true, + input: '%1', + cmd: 'copyq:' + + 'text = str(data(mimeText));' + + 'currentTab = str(data(mimeCurrentTab));' + + 'add(currentTab + "/" + text);' + }]) + )").arg(testMime); + + RUN(script, ""); + + RUN("write" << "0" << testMime << "" << mimeText << "a", ""); + WAIT_ON_OUTPUT( + "read(0,1,2)", + QString::fromLatin1("%1/a\na\n") + .arg(clipboardTabName) + .toUtf8() ); + + RUN("write" << "0" << testMime << "" << mimeText << "b", ""); + WAIT_ON_OUTPUT( + "read(0,1,2,3,4)", + QString::fromLatin1("%1/b\nb\n%1/a\na\n") + .arg(clipboardTabName) + .toUtf8() ); +} + +void TestsScriptCommands::displayCommandForMenu() +{ + const auto tab = testTab(1); + const auto args = Args("tab") << tab << "separator" << ","; + const auto script = QString(R"( + setCommands([{ + display: true, + cmd: 'copyq:' + + 'currentTab = str(data(mimeCurrentTab));' + + 'inMenu = str(data(mimeDisplayItemInMenu));' + + 'if (inMenu != "1" || currentTab != "%1") abort();' + + 'text = str(data(mimeText));' + + 'setData(mimeText, "display:" + text);' + + 'setData(mimeIcon, String.fromCharCode(0xF328));' + + 'setData("application/x-copyq-item-tag", "TAG");' + + 'tab(tab()[0]);' + + 'old = str(read(0));' + + 'add(old + "|" + text);' + }]) + )").arg(tab); + + RUN("config" << "tray_tab" << tab, tab + "\n"); + RUN("config" << "tray_tab_is_current" << "false", "false\n"); + RUN(script, ""); + + RUN(args << "add(1,2,3,4,5)", ""); + RUN("menu", ""); + WAIT_ON_OUTPUT("read(0)", "|5|4|3|2|1"); +} diff --git a/src/tests/tests_script_commands.h b/src/tests/tests_script_commands.h new file mode 100644 index 0000000000..72bf7fb3a4 --- /dev/null +++ b/src/tests/tests_script_commands.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "tests/testinterface.h" +#include "tests/tests_base.h" + +class TestsScriptCommands : public TestsBase +{ + Q_OBJECT + +public: + explicit TestsScriptCommands(const TestInterfacePtr &test) : TestsBase(test) {} + +private slots: + void scriptCommandLoaded(); + void scriptCommandAddFunction(); + void scriptCommandOverrideFunction(); + void scriptCommandEnhanceFunction(); + void scriptCommandEndingWithComment(); + void scriptCommandWithError(); + + void scriptPaste(); + void scriptOnTabSelected(); + void scriptOnItemsRemoved(); + void scriptOnItemsAdded(); + void scriptOnItemsChanged(); + void scriptOnItemsLoaded(); + void scriptEventMaxRecursion(); + void scriptSlowCollectOverrides(); + + void displayCommand(); + void displayCommandForMenu(); +}; diff --git a/src/tests/tests_scripts.cpp b/src/tests/tests_scripts.cpp new file mode 100644 index 0000000000..410b67c991 --- /dev/null +++ b/src/tests/tests_scripts.cpp @@ -0,0 +1,1132 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "test_utils.h" +#include "tests/tests_scripts.h" + +#include "common/mimetypes.h" +#include "common/commandstatus.h" + +#include +#include + +void TestsScripts::commandExit() +{ + RUN("exit", "Terminating server.\n"); + + TEST( m_test->waitForServerToStop() ); + + QCOMPARE( run(Args("exit")), 1 ); +} + +void TestsScripts::commandEval() +{ + RUN("eval" << "", ""); + RUN("eval" << "1", "1\n"); + RUN("eval" << "[1,2,3]", "1\n2\n3\n"); + RUN("eval" << "'123'", "123\n"); + RUN("eval" << "'123'", "123\n"); + + RUN("-e" << "", ""); + RUN("-e" << "1", "1\n"); +} + +void TestsScripts::commandEvalThrows() +{ + RUN_EXPECT_ERROR_WITH_STDERR( + "throw Error('Some exception')", CommandException, + "ScriptError: Some exception\n" + "\n" + "--- backtrace ---\n" + ); + + RUN_EXPECT_ERROR_WITH_STDERR( + "throw 'Some exception'", CommandException, + "ScriptError: Some exception\n" + ); + + RUN_EXPECT_ERROR("eval('throw Error(1)')", CommandException); + RUN_EXPECT_ERROR("eval('throw 1')", CommandException); + RUN_EXPECT_ERROR("eval" << "throw Error(1)", CommandException); + RUN_EXPECT_ERROR("eval" << "throw 1", CommandException); +} + +void TestsScripts::commandEvalSyntaxError() +{ + RUN_EXPECT_ERROR_WITH_STDERR("eval" << "(", CommandException, "SyntaxError"); +} + +void TestsScripts::commandEvalArguments() +{ + RUN("eval" << "str(arguments[1]) + ', ' + str(arguments[2])" << "Test 1" << "Test 2", + "Test 1, Test 2\n"); +} + +void TestsScripts::commandEvalEndingWithComment() +{ + /* + With Qml scripts in Qt 5, it's not possible to get uncaught exceptions + from `QJSEngine::evaluate()`. + + Workaround is to wrap the script properly in an try/catch block: + + try { + %1 + } catch(e) { + _store_exception_internally(e); + throw e; + } + + (Unfortunately, it's still possible to escape the function with a script injection.) + */ + RUN("eval" << "1 // TEST", "1\n"); +} + +void TestsScripts::commandPrint() +{ + RUN("print" << "1", "1"); + RUN("print" << "TEST", "TEST"); +} + +void TestsScripts::commandAbort() +{ + RUN("eval" << "abort(); 1", ""); + RUN("eval" << "eval('abort(); print(1)'); 2", ""); + RUN("eval" << "execute('copyq', 'eval', '--', 'abort(); print(1)'); 2", "2\n"); +} + +void TestsScripts::commandFail() +{ + QByteArray stdoutActual; + QByteArray stderrActual; + QCOMPARE( run(Args("fail"), &stdoutActual, &stderrActual), 1 ); + QVERIFY2( testStderr(stderrActual), stderrActual ); + QCOMPARE( stdoutActual, QByteArray() ); +} + +void TestsScripts::commandSource() +{ + const auto script = + R"( + test = function() { return " TEST" } + print("SOURCED") + )"; + + QTemporaryFile scriptFile; + QVERIFY(scriptFile.open()); + scriptFile.write(script); + scriptFile.close(); + const auto scriptFileName = scriptFile.fileName(); + + RUN("source" << scriptFileName, "SOURCED"); + RUN("source" << scriptFileName << "test()", "SOURCED TEST\n"); +} + +void TestsScripts::commandVisible() +{ + RUN("visible", "true\n"); +} + +void TestsScripts::commandToggle() +{ + RUN("visible", "true\n"); + RUN("toggle", "false\n"); + WAIT_ON_OUTPUT("visible", "false\n"); + + RUN("toggle", "true\n"); + WAIT_ON_OUTPUT("visible", "true\n"); +} + +void TestsScripts::commandHide() +{ + RUN("visible", "true\n"); + RUN("hide", ""); + WAIT_ON_OUTPUT("visible", "false\n"); +} + +void TestsScripts::commandShow() +{ + RUN("visible", "true\n"); + RUN("hide", ""); + WAIT_ON_OUTPUT("visible", "false\n"); + + RUN("show", ""); + WAIT_ON_OUTPUT("visible", "true\n"); +} + +void TestsScripts::commandShowAt() +{ + RUN("visible", "true\n"); + RUN("hide", ""); + WAIT_ON_OUTPUT("visible", "false\n"); + + RUN("showAt", ""); + WAIT_ON_OUTPUT("visible", "true\n"); +} + +void TestsScripts::commandFocused() +{ + RUN("focused", "true\n"); + RUN("hide", ""); + RUN("focused", "false\n"); +} + +void TestsScripts::commandsUnicode() +{ + const auto text = QString::fromUtf8(QByteArray("Zkouška s různými českými znaky!")); + RUN_WITH_INPUT("eval" << "input()", text, text); + RUN_WITH_INPUT("eval" << "str(input())", text, text + "\n"); + + RUN_WITH_INPUT("eval" << "fromUnicode(str(input()), 'utf8')", text, text); + RUN_WITH_INPUT("eval" << "toUnicode(fromUnicode(str(input()), 'utf16'), 'utf16')", text, text + "\n"); + RUN_WITH_INPUT("eval" << "toUnicode(fromUnicode(str(input()), 'utf32le'), 'utf32le')", text, text + "\n"); + RUN_WITH_INPUT("eval" << "toUnicode( fromUnicode(str(input()), 'utf16le') )", text, text + "\n"); +} + +void TestsScripts::commandsAddRead() +{ + RUN("add" << "A", ""); + RUN("read" << "0", "A"); + + RUN("add" << "B", ""); + RUN("read" << "0", "B"); + RUN("read" << "1", "A"); + + RUN("add" << "C" << "D", ""); + RUN("read" << "0", "D"); + RUN("read" << "1", "C"); + RUN("read" << "2", "B"); + RUN("read" << "3", "A"); +} + +void TestsScripts::commandsWriteRead() +{ + const QByteArray input("\x00\x01\x02\x03\x04", 5); + const auto arg1 = QString::fromLatin1("\x01\x02\x03\x04"); + const auto arg2 = QString::fromLatin1("\x7f\x6f\x5f\x4f"); + TEST( m_test->runClient( + Args() << "write" + << COPYQ_MIME_PREFIX "test1" << arg1 + << COPYQ_MIME_PREFIX "test2" << "-" + << COPYQ_MIME_PREFIX "test3" << arg2, "", + input) ); + RUN("read" << COPYQ_MIME_PREFIX "test1" << "0", arg1.toLatin1()); + RUN("read" << COPYQ_MIME_PREFIX "test2" << "0", input); + RUN("read" << COPYQ_MIME_PREFIX "test3" << "0", arg2.toLatin1()); + + RUN("write(1, {'text/plain': 'A'}, {'text/plain': 'B'})", ""); + RUN("read(mimeText, 0, 1, 2, 3)", "\nB\nA\n"); + + RUN("write(0, [{'text/plain': 'C'}, {'text/plain': 'D'}])", ""); + RUN("read(mimeText, 0, 1, 2, 3)", "D\nC\n\nB"); + + RUN("write(0, ['E', 'F'])", ""); + RUN("read(mimeText, 0, 1, 2, 3)", "F\nE\nD\nC"); + + RUN_EXPECT_ERROR_WITH_STDERR( + "write(0, [{}], [{}])", + CommandException, "Unexpected multiple item list arguments"); + RUN_EXPECT_ERROR_WITH_STDERR( + "write(0)", + CommandException, "Expected item arguments"); + RUN_EXPECT_ERROR_WITH_STDERR( + "write(0, '1', '2', '3')", + CommandException, "Unexpected uneven number of mimeType/data arguments"); +} + +void TestsScripts::commandChange() +{ + RUN("add" << "C" << "B" << "A", ""); + RUN("change" << "1" << "text/plain" << "b", ""); + RUN("separator" << " " << "read" << "0" << "1" << "2", "A b C"); + + RUN("change" << "1" << "text/plain" << "B" << "text/html" << "B", ""); + RUN("read" << "text/html" << "1", "B"); + RUN("separator" << " " << "read" << "0" << "1" << "2", "A B C"); + + RUN("change(1, 'text/html', undefined)", ""); + RUN("read" << "?" << "1", "text/plain\n"); +} + +void TestsScripts::commandSetCurrentTab() +{ + const auto tab = testTab(1); + RUN("setCurrentTab" << tab, ""); + RUN("testSelected", tab + "\n"); +} + +void TestsScripts::commandConfig() +{ + QByteArray stdoutActual; + QByteArray stderrActual; + QCOMPARE( run(Args("config"), &stdoutActual, &stderrActual), 0 ); + QVERIFY2( testStderr(stderrActual), stderrActual ); + QVERIFY( !stdoutActual.isEmpty() ); + + // invalid option + RUN_EXPECT_ERROR_WITH_STDERR("config" << "xxx", CommandException, "xxx"); + + QCOMPARE( run(Args("config") << "tab_tree", &stdoutActual, &stderrActual), 0 ); + QVERIFY2( testStderr(stderrActual), stderrActual ); + QVERIFY2( stdoutActual == "true\n" || stdoutActual == "false\n", stdoutActual); + + RUN("config" << "tab_tree" << "true", "true\n"); + RUN("config" << "tab_tree", "true\n"); + + RUN("config" << "tab_tree" << "false", "false\n"); + RUN("config" << "tab_tree", "false\n"); + + RUN("config" << "tab_tree" << "1", "true\n"); + RUN("config" << "tab_tree", "true\n"); + + RUN("config" << "tab_tree" << "0", "false\n"); + RUN("config" << "tab_tree", "false\n"); + + // Set multiple options. + RUN("config" << "tab_tree" << "0" << "text_wrap" << "1", + "tab_tree=false\n" + "text_wrap=true\n"); + + // Don't set any options if there is an invalid one. + RUN_EXPECT_ERROR_WITH_STDERR("config" << "tab_tree" << "1" << "xxx" << "0", CommandException, "xxx"); + RUN("config" << "tab_tree", "false\n"); +} + +void TestsScripts::commandToggleConfig() +{ + RUN("toggleConfig" << "check_clipboard", "false\n"); + RUN("config" << "check_clipboard", "false\n"); + RUN("toggleConfig" << "check_clipboard", "true\n"); + RUN("config" << "check_clipboard", "true\n"); + + RUN_EXPECT_ERROR("toggleConfig", CommandException); + RUN_EXPECT_ERROR_WITH_STDERR("toggleConfig" << "xxx", CommandException, "xxx"); + RUN_EXPECT_ERROR_WITH_STDERR("toggleConfig" << "clipboard_tab", CommandException, "clipboard_tab"); +} + +void TestsScripts::commandDialog() +{ + RUN(Args() << "keys" << clipboardBrowserId, ""); + runMultiple( + [&]() { RUN(WITH_TIMEOUT "dialog('text')", "TEST\n"); }, + [&]() { RUN(Args() << "keys" << "focus::QLineEdit in :QDialog" << ":TEST" << "ENTER", ""); } + ); + + RUN(Args() << "keys" << clipboardBrowserId, ""); + runMultiple( + [&]() { RUN(WITH_TIMEOUT "dialog('text') === undefined", "true\n"); }, + [&]() { RUN(Args() << "keys" << "focus::QLineEdit in :QDialog" << "ESCAPE", ""); } + ); + + RUN(Args() << "keys" << clipboardBrowserId, ""); + runMultiple( + [&]() { RUN(WITH_TIMEOUT "dialog('.defaultChoice', 2, 'list', [1, 2, 3])", "2\n"); }, + [&]() { RUN(Args() << "keys" << "focus::QComboBox in :QDialog" << "ENTER", ""); } + ); + + RUN(Args() << "keys" << clipboardBrowserId, ""); + runMultiple( + [&]() { RUN(WITH_TIMEOUT "dialog('.defaultChoice', '', 'list', [1, 2, 3])", "\n"); }, + [&]() { RUN(Args() << "keys" << "focus::QComboBox in :QDialog" << "ENTER", ""); } + ); + + RUN(Args() << "keys" << clipboardBrowserId, ""); + runMultiple( + [&]() { RUN(WITH_TIMEOUT "dialog('list', [0, 1, 2])", "0\n"); }, + [&]() { RUN(Args() << "keys" << "focus::QComboBox in :QDialog" << "ENTER", ""); } + ); + + // Can't focus configuration checkboxes on OS X +#ifndef Q_OS_MAC + RUN(Args() << "keys" << clipboardBrowserId, ""); + runMultiple( + [&]() { RUN(WITH_TIMEOUT "dialog('boolean', true) === true", "true\n"); }, + [&]() { RUN(Args() << "keys" << "focus::QCheckBox in :QDialog" << "ENTER", ""); } + ); +#endif + + // Verify that special argument ".title" changes dialog's object name + // so that geometry can be stored. + RUN(Args() << "keys" << clipboardBrowserId, ""); + runMultiple( + [&]() { RUN(WITH_TIMEOUT "dialog('.title', 'test', 'text')", ""); }, + [&]() { RUN(Args() << "keys" << "focus::QLineEdit in dialog_test:QDialog" << "ESCAPE", ""); } + ); + + RUN(Args() << "keys" << clipboardBrowserId, ""); + const QByteArray script = R"( + dialog( + '.width', 100, + '.height', 100, + '.x', 10, + '.y', 10, + '.style', 'background: red', + '.icon', '', + '.label', 'TEST', + 'text', 'DEFAULT', + ) + )"; + runMultiple( + [&]() { RUN(WITH_TIMEOUT + script, "DEFAULT\n"); }, + [&]() { RUN(Args() << "keys" << "focus::QLineEdit in :QDialog" << "ENTER", ""); } + ); + + RUN(Args() << "keys" << clipboardBrowserId, ""); + runMultiple( + [&]() { RUN(WITH_TIMEOUT "dialog('.title', 'Remove Items', '.label', 'Remove all items?') === true", "true\n"); }, + [&]() { RUN(Args() << "keys" << "focus::QPushButton in dialog_Remove Items:QDialog" << "ENTER", ""); } + ); +} + +void TestsScripts::commandDialogCloseOnDisconnect() +{ + RUN("afterMilliseconds(0, abort); dialog()", ""); +} + +void TestsScripts::commandMenuItems() +{ + RUN(Args() << "keys" << clipboardBrowserId, ""); + runMultiple( + [&]() { RUN(WITH_TIMEOUT "menuItems('a', 'b', 'c')", "a\n"); }, + [&]() { RUN(Args() << "keys" << customMenuId << "ENTER", ""); } + ); + + RUN(Args() << "keys" << clipboardBrowserId, ""); + runMultiple( + [&]() { RUN(WITH_TIMEOUT "menuItems([{'text/plain': 'a'}, {'text/plain': 'b'}])", "0\n"); }, + [&]() { RUN(Args() << "keys" << customMenuId << "ENTER", ""); } + ); + + RUN(Args() << "keys" << clipboardBrowserId, ""); + runMultiple( + [&]() { RUN(WITH_TIMEOUT "menuItems('a', 'b', 'c')", "\n"); }, + [&]() { RUN(Args() << "keys" << customMenuId << "ESCAPE", ""); } + ); + + RUN(Args() << "keys" << clipboardBrowserId, ""); + runMultiple( + [&]() { RUN(WITH_TIMEOUT "menuItems([{'text/plain': 'a'}, {'text/plain': 'b'}])", "-1\n"); }, + [&]() { RUN(Args() << "keys" << customMenuId << "ESCAPE", ""); } + ); + + RUN(Args() << "keys" << clipboardBrowserId, ""); + runMultiple( + [&]() { RUN(WITH_TIMEOUT "menuItems('a', 'b', 'c')", "b\n"); }, + [&]() { RUN(Args() << "keys" << customMenuId << ":b" << "ENTER", ""); } + ); + + RUN(Args() << "keys" << clipboardBrowserId, ""); + runMultiple( + [&]() { RUN(WITH_TIMEOUT "menuItems([{'text/plain': 'a'}, {'text/plain': 'b'}])", "1\n"); }, + [&]() { RUN(Args() << "keys" << customMenuId << ":b" << "ENTER", ""); } + ); + + RUN("afterMilliseconds(0, abort); menuItems('a', 'b', 'c')", ""); +} + +void TestsScripts::commandsPackUnpack() +{ + QMap data; + data[mimeText] = "plain text"; + data[mimeHtml] = "HTML text"; + data[QLatin1String(COPYQ_MIME_PREFIX "test1")] = "test1 data"; + data[QLatin1String(COPYQ_MIME_PREFIX "test2")] = "test2 data"; + + auto args = Args() << "write"; + for (const auto &mime : data.keys()) + args << mime << data[mime]; + RUN(args, ""); + + const QByteArray script1 = + "var data = read('" + toByteArray(mimeItems) + "', 0); var item = unpack(data);"; + + // Unpack item read from list. + for (const auto &mime : data.keys()) { + RUN("eval" + << script1 + "var mime = '" + mime + "'; print(mime + ':' + str(item[mime]))", + mime + ':' + data[mime]); + } + + // Test pack and unpack consistency. + const QByteArray script2 = "data = pack(item); item = unpack(data);"; + for (const auto &mime : data.keys()) { + RUN("eval" + << script1 + script2 + "var mime = '" + mime + "'; print(mime + ':' + str(item[mime]))", + mime + ':' + data[mime]); + } +} + +void TestsScripts::commandsBase64() +{ + const QByteArray data = "0123456789\001\002\003\004\005\006\007abcdefghijklmnopqrstuvwxyz!"; + const QByteArray base64 = data.toBase64(); + + TEST( m_test->runClient(Args() << "eval" << "print(input())", data, data) ); + TEST( m_test->runClient(Args() << "eval" << "print(tobase64(input()))", base64, data) ); + + // Line break is added only if return value is string; + // tobase64() returns string, frombase64() returns byte array. + RUN("tobase64" << data, base64 + '\n'); + RUN("frombase64" << base64, data); + + TEST( m_test->runClient(Args() << "eval" << "print(frombase64(tobase64(input())))", data, data) ); + + // Test Base64 encoding and decoding consistency. + TEST( m_test->runClient( + Args() << "eval" + << "var base64 = tobase64(input()); if (str(input()) === str(frombase64(base64))) print('OK')", + "OK", data) ); +} + +void TestsScripts::commandsGetSetItem() +{ + QMap data; + data["text/plain"] = "plain text"; + data["text/html"] = "HTML text"; + data[COPYQ_MIME_PREFIX "test1"] = "test1 data"; + data[COPYQ_MIME_PREFIX "test2"] = "test2 data"; + + const QString tab = testTab(1); + const Args args = Args("tab") << tab; + + Args args2 = args; + args2 << "write"; + for (const auto &mime : data.keys()) + args2 << mime << data[mime]; + RUN(args2, ""); + + // Get item from list. + for (const auto &mime : data.keys()) { + RUN(args << "eval" + << "var mime = '" + mime + "'; print(mime + ':' + str(getitem(0)[mime]))", + mime + ':' + data[mime]); + } + + // Set item. + RUN(args << "eval" + << "setitem(1, { 'text/plain': 'plain text 2', 'text/html': 'HTML text 2' })", + ""); + + RUN(args << "eval" << "print(getitem(1)['text/plain'])", "plain text 2"); + RUN(args << "eval" << "print(getitem(1)['text/html'])", "HTML text 2"); +} + +void TestsScripts::commandsChecksums() +{ + RUN("md5sum" << "TEST", "033bd94b1168d7e4f0d644c3c95e35bf\n"); + RUN("sha1sum" << "TEST", "984816fd329622876e14907634264e6f332e9fb3\n"); + RUN("sha256sum" << "TEST", "94ee059335e587e501cc4bf90613e0814f00a7b08bc7c648fd865a2af6a22cc2\n"); + RUN("sha512sum" << "TEST", "7bfa95a688924c47c7d22381f20cc926f524beacb13f84e203d4bd8cb6ba2fce81c57a5f059bf3d509926487bde925b3bcee0635e4f7baeba054e5dba696b2bf\n"); +} + +void TestsScripts::commandEscapeHTML() +{ + RUN("escapeHTML" << "&\n<\n>", "&
<
>\n"); +} + +void TestsScripts::commandExecute() +{ + const QByteArray script = + "function test(c, expected_stdout, expected_exit_code) {" + " if (str(c.stdout) !== expected_stdout) print('Unexpected stdout: \"' + str(c.stdout) + '\"');" + " if (c.exit_code !== expected_exit_code) print('Unexpected exit_code: ' + str(c.exit_code));" + "}"; + + RUN("eval" << script + + "c = execute('copyq', 'write', 'text/plain', 'plain text', 'text/html', 'test HTML');" + "test(c, '', 0);" + , ""); + + RUN("eval" << script + + "c = execute('copyq', 'read', 'text/plain', 0);" + "test(c, 'plain text', 0);" + , ""); + + RUN("eval" << script + + "c = execute('copyq', 'read', 'text/html', 0);" + "test(c, 'test HTML', 0);" + , ""); + + RUN("eval" << script + + "c = execute('copyq', 'read', 0, function(lines) { print(lines); });" + "test(c, 'plain text', 0);" + , "plain text"); +} + +void TestsScripts::commandSettings() +{ + RUN("config" << "clipboard_tab" << "TEST", "TEST\n"); + + RUN("settings" << "test_variable", ""); + RUN("settings" << "test_variable" << "TEST VALUE", ""); + RUN("settings" << "test_variable", "TEST VALUE"); + RUN("settings" << "test_variable" << "TEST VALUE 2", ""); + RUN("settings" << "test_variable", "TEST VALUE 2"); + + RUN("config" << "clipboard_tab", "TEST\n"); +} + +void TestsScripts::commandsEnvSetEnv() +{ + RUN("eval" << + "\n var name = 'COPYQ_ENV_TEST'" + "\n if (setEnv(name, 'OK'))" + "\n print(env(name))" + "\n else" + "\n print('FAILED')" + , "OK" + ); +} + +void TestsScripts::commandSleep() +{ + QElapsedTimer t; + + t.start(); + RUN("sleep" << "100", ""); + const auto afterElapsed100Ms = t.elapsed(); + QVERIFY(afterElapsed100Ms > 100); + + t.start(); + RUN("sleep" << "1000", ""); + const auto afterElapsed1000Ms = t.elapsed(); + QVERIFY(afterElapsed1000Ms > 1000); +} + +void TestsScripts::commandsData() +{ + RUN("eval" << "setData('x', 'X'); data('x')", "X"); + RUN("eval" << "setData('x', 'X'); setData('y', 'Y'); str(data('x')) + str(data('y'))", "XY\n"); + + RUN("dataFormats", ""); + RUN("eval" << "setData('x'); dataFormats()", "x\n"); + RUN("eval" << "setData('x'); setData('y'); dataFormats()", "x\ny\n"); + + RUN("eval" << "setData('x'); setData('y'); removeData('x'); dataFormats()", "y\n"); + RUN("eval" << "setData('x'); setData('y'); removeData('y'); dataFormats()", "x\n"); +} + +void TestsScripts::commandCurrentWindowTitle() +{ + RUN("disable", ""); + WAIT_ON_OUTPUT("currentWindowTitle", appWindowTitle("*Clipboard Storing Disabled*")); + RUN("enable", ""); +} + +void TestsScripts::commandCopy() +{ + RUN("copy" << "A", "true\n"); + WAIT_FOR_CLIPBOARD("A"); + + RUN("copy" << "DATA" << "B", "true\n"); + WAIT_FOR_CLIPBOARD2("B", "DATA"); + + RUN( Args() << "copy" + << "DATA3" << "C" + << "DATA4" << "D" + , "true\n" ); + WAIT_FOR_CLIPBOARD2("C", "DATA3"); + WAIT_FOR_CLIPBOARD2("D", "DATA4"); + + RUN( "copy({'DATA1': 1, 'DATA2': 2})", "true\n" ); + WAIT_FOR_CLIPBOARD2("1", "DATA1"); + WAIT_FOR_CLIPBOARD2("2", "DATA2"); + + RUN_EXPECT_ERROR_WITH_STDERR( + "copy({}, {})", + CommandException, "Expected single item"); + RUN_EXPECT_ERROR_WITH_STDERR( + "copy([{}, {}])", + CommandException, "Expected single item"); +} + +void TestsScripts::commandClipboard() +{ + TEST( m_test->setClipboard("A") ); + WAIT_FOR_CLIPBOARD("A"); + RUN("clipboard", "A"); + + TEST( m_test->setClipboard("B", "DATA") ); + WAIT_FOR_CLIPBOARD2("B", "DATA"); + RUN("clipboard" << "DATA", "B"); +} + +void TestsScripts::commandHasClipboardFormat() +{ + TEST( m_test->setClipboard("B", "DATA") ); + WAIT_FOR_CLIPBOARD2("B", "DATA"); + WAIT_ON_OUTPUT("hasClipboardFormat('DATA')", "true\n"); + WAIT_ON_OUTPUT("hasClipboardFormat('text/plain')", "false\n"); +} + +void TestsScripts::commandEdit() +{ + SKIP_ON_ENV("COPYQ_TESTS_SKIP_COMMAND_EDIT"); + + RUN("config" << "editor" << "", "\n"); + + // Edit clipboard and new item. + TEST( m_test->setClipboard("TEST") ); + RUN("edit" << "-1", ""); + RUN("keys" << "END" << ":LINE 1" << "F2", ""); + RUN("read" << "0", "TESTLINE 1"); + WAIT_FOR_CLIPBOARD("TESTLINE 1"); + + // Edit existing item. + RUN("edit" << "0", ""); + RUN("keys" << "END" << "ENTER" << ":LINE 2" << "F2", ""); + RUN("read" << "0", "TESTLINE 1\nLINE 2"); + WAIT_FOR_CLIPBOARD("TESTLINE 1"); + + // Edit clipboard (ignore existing data) and new item. + RUN("edit", ""); + RUN("keys" << "END" << ":LINE 1" << "F2", ""); + RUN("read" << "0", "LINE 1"); + WAIT_FOR_CLIPBOARD("LINE 1"); +} + +void TestsScripts::commandEditItem() +{ + SKIP_ON_ENV("COPYQ_TESTS_SKIP_COMMAND_EDIT"); + + RUN("config" << "editor" << "", "\n"); + + // Edit clipboard and new item. + TEST( m_test->setClipboard("TEST", mimeHtml) ); + RUN("editItem" << "-1" << mimeHtml, ""); + RUN("keys" << "END" << ":LINE 1" << "F2", ""); +#ifdef Q_OS_WIN +# define FRAG_START "" +# define FRAG_END "" + const auto expected = QByteArrayLiteral(FRAG_START "TEST" FRAG_END "LINE 1"); +#else + const auto expected = QByteArrayLiteral("TESTLINE 1"); +#endif + RUN("read" << mimeHtml << "0", expected); + RUN("read" << "0", ""); + WAIT_FOR_CLIPBOARD2(expected, mimeHtml); + WAIT_FOR_CLIPBOARD(""); + + // Edit existing item. + RUN("editItem" << "0" << mimeHtml, ""); + RUN("keys" << "END" << "ENTER" << ":LINE 2" << "F2", ""); + RUN("read" << mimeHtml << "0", expected + "\nLINE 2"); + RUN("read" << "0", ""); + WAIT_FOR_CLIPBOARD2(expected, mimeHtml); + WAIT_FOR_CLIPBOARD(""); + + // Edit clipboard (ignore existing data) and new item. + RUN("editItem" << "-1" << mimeHtml << "TEST", ""); + RUN("keys" << "END" << ":LINE 1" << "F2", ""); + RUN("read" << mimeHtml << "0", "TESTLINE 1"); + RUN("read" << "0", ""); +#ifdef Q_OS_WIN + WAIT_FOR_CLIPBOARD2(FRAG_START "TESTLINE 1" FRAG_END, mimeHtml); +#else + WAIT_FOR_CLIPBOARD2("TESTLINE 1", mimeHtml); +#endif + WAIT_FOR_CLIPBOARD(""); +} + +void TestsScripts::commandGetSetCurrentPath() +{ + RUN("currentPath", QDir::currentPath() + "\n"); + + const auto newPath = QDir::homePath().toUtf8(); + + TEST( m_test->runClient( + Args("eval") << "currentPath(input()); print(currentPath())", + newPath, newPath) ); + + TEST( m_test->runClient( + Args("eval") << "currentPath(input()); print(Dir().absolutePath())", + newPath, newPath) ); +} + +void TestsScripts::commandSelectItems() +{ + RUN("add" << "C" << "B" << "A", ""); + + RUN("selectItems" << "1", "true\n"); + RUN("testSelected", QString(clipboardTabName) + " 1 1\n"); + + RUN("selectItems" << "0" << "2", "true\n"); + RUN("testSelected", QString(clipboardTabName) + " 2 0 2\n"); + + const auto tab = testTab(1); + const auto args = Args("tab") << tab; + RUN(args << "add" << "C" << "B" << "A", ""); + RUN(args << "selectItems" << "1" << "2", "true\n"); + RUN("testSelected", QString(clipboardTabName) + " 2 0 2\n"); + RUN("setCurrentTab" << tab, ""); + RUN("testSelected", tab + " 2 1 2\n"); +} + +void TestsScripts::commandsExportImport() +{ + const auto tab1 = testTab(1); + RUN("tab" << tab1 << "add" << "C" << "B" << "A", ""); + + const auto tab2 = testTab(2); + RUN("tab" << tab2 << "add" << "3" << "2", ""); + + RUN("config" << "maxitems" << "3", "3\n"); + RUN("config" << "editor" << "EDITOR1 %1", "EDITOR1 %1\n"); + + TemporaryFile tmp; + const auto fileName = tmp.fileName(); + + RUN("exportData" << fileName, ""); + + RUN("config" << "maxitems" << "1", "1\n"); + RUN("config" << "editor" << "EDITOR2 %1", "EDITOR2 %1\n"); + RUN("removetab" << tab1, ""); + RUN("tab" << tab2 << "add" << "1", ""); + + RUN("importData" << fileName, ""); + + RUN("config" << "maxitems", "3\n"); + RUN("config" << "editor", "EDITOR1 %1\n"); + + const auto suffix = " (1)"; + RUN("tab", + QString(clipboardTabName) + "\n" + + tab2 + "\n" + + clipboardTabName + suffix + "\n" + + tab1 + "\n" + + tab2 + suffix + "\n"); + + RUN("tab" << tab1 << "read" << "0" << "1" << "2", "A\nB\nC"); + RUN("tab" << tab2 + suffix << "read" << "0" << "1", "2\n3"); + RUN("tab" << tab2 << "read" << "0", "1"); +} + +void TestsScripts::commandsGetSetCommands() +{ + RUN("commands().length", "0\n"); + + RUN("setCommands([{name: 'test', cmd: 'copyq help'}])", ""); + RUN("commands().length", "1\n"); + RUN("commands()[0].name", "test\n"); + RUN("commands()[0].cmd", "copyq help\n"); + RUN("commands()[0].enable", "true\n"); + + RUN("setCommands(commands())", ""); + RUN("commands().length", "1\n"); + RUN("commands()[0].name", "test\n"); + RUN("commands()[0].enable", "true\n"); +} + +void TestsScripts::commandsImportExportCommands() +{ + const QString commands = + R"(' + [Commands] + 1\Name=Test 1 + 2\Name=Test 2 + size=2 + ')"; + RUN("eval" << "importCommands(arguments[1]).length" << "--" << commands, "2\n"); + RUN("eval" << "importCommands(arguments[1])[0].name" << "--" << commands, "Test 1\n"); + RUN("eval" << "importCommands(arguments[1])[1].name" << "--" << commands, "Test 2\n"); + + RUN("importCommands(exportCommands([{},{}])).length", "2\n"); + RUN("importCommands(exportCommands([{},{name: 'Test 2'}]))[1].name", "Test 2\n"); +} + +void TestsScripts::commandsImportExportCommandsFixIndentation() +{ + { + const QString commands = + "[Command]\n" + "Command=\"\n 1\n 2\n 3\""; + RUN("eval" << "importCommands(arguments[1])[0].cmd" << "--" << commands, "1\n2\n3\n"); + } + + { + const QString commands = + "[Command]\n" + "Command=\"\r\n 1\r\n 2\r\n 3\""; + RUN("eval" << "importCommands(arguments[1])[0].cmd" << "--" << commands, "1\n2\n3\n"); + } +} + +void TestsScripts::commandsAddCommandsRegExp() +{ + const QString commands = + "[Command]\n" + "Match=^(https?|ftps?)://\\\\$\n"; + + // Ensure there is a basic RegExp support. + RUN("/test/", "/test/\n"); + RUN("/test/.source", "test\n"); + + RUN("eval" << "exportCommands(importCommands(arguments[1]))" << "--" << commands, commands); + RUN("eval" << "Object.prototype.toString.call(importCommands(arguments[1])[0].re)" << "--" << commands, "[object RegExp]\n"); + RUN("eval" << "Object.prototype.toString.call(importCommands(arguments[1])[0].wndre)" << "--" << commands, "[object RegExp]\n"); + RUN("eval" << "importCommands(arguments[1])[0].re" << "--" << commands, "/^(https?|ftps?):\\/\\/\\$/\n"); + RUN("eval" << "importCommands(arguments[1])[0].wndre" << "--" << commands, "/(?:)/\n"); + + RUN("eval" << "addCommands(importCommands(arguments[1]))" << "--" << commands, ""); + RUN("keys" << commandDialogListId << "Enter" << clipboardBrowserId, ""); + + RUN("exportCommands(commands())", commands); + RUN("commands()[0].name", "\n"); + RUN("commands()[0].re", "/^(https?|ftps?):\\/\\/\\$/\n"); + RUN("commands()[0].wndre", "/(?:)/\n"); +} + +void TestsScripts::commandScreenshot() +{ + RUN("screenshot().size() > 0", "true\n"); +} + +void TestsScripts::commandNotification() +{ + const auto script = R"( + notification( + '.title', 'title', + '.message', 'message', + '.time', 1000, + '.id', 'test', + '.icon', 'copyq', + '.button', 'OK', '', '', + '.button', 'CANCEL', '', '' + ) + )"; + RUN(script, ""); + + RUN_EXPECT_ERROR_WITH_STDERR( + "notification('.message', 'message', 'BAD')", CommandException, "Unknown argument: BAD"); +} + +void TestsScripts::commandIcon() +{ + RUN("iconColor", QByteArray(defaultSessionColor) + "\n"); + + RUN("iconColor" << "red", ""); + RUN("iconColor", "#ff0000\n"); + + RUN_EXPECT_ERROR("iconColor" << "BAD_COLOR_NAME", CommandException); + RUN("iconColor", "#ff0000\n"); + + RUN("iconColor" << "", ""); + RUN("iconColor", QByteArray(defaultSessionColor) + "\n"); + + RUN("iconColor" << defaultSessionColor, ""); + RUN("iconColor", QByteArray(defaultSessionColor) + "\n"); +} + +void TestsScripts::commandIconTag() +{ + RUN("iconTag", "\n"); + + RUN("iconTag" << "TEST", ""); + RUN("iconTag", "TEST\n"); + + RUN("iconTag" << "", ""); + RUN("iconTag", "\n"); +} + +void TestsScripts::commandIconTagColor() +{ + RUN("iconTagColor", QByteArray(defaultTagColor) + "\n"); + + RUN("iconTagColor" << "red", ""); + RUN("iconTagColor", "#ff0000\n"); + + RUN_EXPECT_ERROR("iconTagColor" << "BAD_COLOR_NAME", CommandException); + RUN("iconTagColor", "#ff0000\n"); + + RUN("iconTagColor" << defaultTagColor, ""); + RUN("iconTagColor", QByteArray(defaultTagColor) + "\n"); +} + +void TestsScripts::commandLoadTheme() +{ + RUN_EXPECT_ERROR_WITH_STDERR( + "loadTheme" << "a non-existent file", CommandException, "ScriptError: Failed to read theme"); + RUN_EXPECT_ERROR_WITH_STDERR( + "loadTheme" << ".", CommandException, "ScriptError: Failed to read theme"); + + { + QTemporaryFile tmp; + QVERIFY(tmp.open()); + tmp.write("INVALID INI FILE"); + tmp.close(); + RUN_EXPECT_ERROR_WITH_STDERR( + "loadTheme" << tmp.fileName(), CommandException, "ScriptError: Failed to parse theme"); + } + + { + QTemporaryFile tmp; + QVERIFY(tmp.open()); + tmp.write("[General]"); + tmp.close(); + RUN("loadTheme" << tmp.fileName(), ""); + } + + // Verify default stylesheets - if there is a syntax error, + // application prints a warning which should be captured by tests. + { + QTemporaryFile tmp; + QVERIFY(tmp.open()); + tmp.write("[General]\nstyle_main_window=true"); + tmp.close(); + RUN("loadTheme" << tmp.fileName(), ""); + } +} + +void TestsScripts::commandDateString() +{ + const auto dateFormat = "TEST:yyyy-MM-dd"; + const auto dateTime = QDateTime::currentDateTime(); + const auto today = dateTime.toString(dateFormat); + RUN("dateString" << dateFormat, today + "\n"); +} + +void TestsScripts::commandAfterMilliseconds() +{ + const QString script = "afterMilliseconds(100, function(){ print('TEST'); abort(); });"; + RUN(script, ""); + RUN(script + "sleep(1)", ""); + RUN(script + "sleep(200)", "TEST"); +} + +void TestsScripts::commandAsync() +{ + RUN("afterMilliseconds(0, function() { print(currentItem()); abort(); }); dialog()", "-1"); +} + +void TestsScripts::commandFilter() +{ + RUN("filter", "\n"); + RUN("filter" << "test", ""); + RUN("filter", "test\n"); + RUN("filter" << "", ""); + RUN("filter", "\n"); + + // Empty filter() after ESC. + RUN("filter" << "test", ""); + RUN("filter", "test\n"); + RUN("keys" << "ESC", ""); + RUN("filter", "\n"); +} + +void TestsScripts::commandMimeTypes() +{ + RUN("print(mimeText)", mimeText); + RUN("print(mimeHtml)", mimeHtml); + RUN("print(mimeUriList)", mimeUriList); + RUN("print(mimeWindowTitle)", mimeWindowTitle); + RUN("print(mimeItems)", mimeItems); + RUN("print(mimeItemNotes)", mimeItemNotes); + RUN("print(mimeOwner)", mimeOwner); + RUN("print(mimeClipboardMode)", mimeClipboardMode); + RUN("print(mimeCurrentTab)", mimeCurrentTab); + RUN("print(mimeSelectedItems)", mimeSelectedItems); + RUN("print(mimeCurrentItem)", mimeCurrentItem); + RUN("print(mimeHidden)", mimeHidden); + RUN("print(mimeShortcut)", mimeShortcut); + RUN("print(mimeColor)", mimeColor); + RUN("print(mimeOutputTab)", mimeOutputTab); +} + +void TestsScripts::commandUnload() +{ + // Failure if tab is visible. + RUN("unload", ""); + + const auto tab = testTab(1); + + // Success if tab doesn't exist. + RUN("unload" << tab, tab + "\n"); + + RUN("tab" << tab << "add" << "A", ""); + // Success if tab is not visible and editor is not open. + RUN("unload" << tab, tab + "\n"); + + RUN("tab" << tab << "add" << "B", ""); + RUN("unload", tab + "\n"); + // Success if tab is not loaded. + RUN("unload", tab + "\n"); + + // Success if tab does not exist. + RUN("unload" << "missing-tab", "missing-tab\n"); +} + +void TestsScripts::commandForceUnload() +{ + RUN("forceUnload", ""); + RUN_EXPECT_ERROR_WITH_STDERR("add" << "A", CommandException, "ScriptError: Invalid tab"); + + RUN("keys" << clipboardBrowserRefreshButtonId << "Space", ""); + RUN("add" << "A", ""); + + const auto tab = testTab(1); + RUN("tab" << tab << "add" << "A", ""); + + RUN("forceUnload" << tab, ""); + + RUN("setCurrentTab" << tab, ""); + RUN_EXPECT_ERROR_WITH_STDERR( + "tab" << tab << "add" << "B", CommandException, "ScriptError: Invalid tab"); + + RUN("keys" << clipboardBrowserRefreshButtonId << "Space", ""); + RUN("add" << "B", ""); +} + +void TestsScripts::commandServerLogAndLogs() +{ + const QByteArray data1 = generateData(); + QRegularExpression re("CopyQ Note \\[[^]]+\\] : " + QRegularExpression::escape(data1)); + + QByteArray stdoutActual; + QByteArray stderrActual; + + QCOMPARE( run(Args("logs"), &stdoutActual, &stderrActual), 0 ); + QVERIFY2( testStderr(stderrActual), stderrActual ); + QVERIFY( !stdoutActual.isEmpty() ); + QVERIFY( !QString::fromUtf8(stdoutActual).contains(re) ); + + RUN("serverLog" << data1, ""); + + QCOMPARE( run(Args("logs"), &stdoutActual, &stderrActual), 0 ); + QVERIFY2( testStderr(stderrActual), stderrActual ); + QVERIFY( !stdoutActual.isEmpty() ); + QVERIFY( QString::fromUtf8(stdoutActual).contains(re) ); +} + +void TestsScripts::chainingCommands() +{ + const auto tab1 = testTab(1); + RUN("tab" << tab1 << "add" << "C" << "B" << "A", ""); + RUN("tab" << tab1 << "separator" << " " << "read" << "0" << "1" << "2", "A B C"); + RUN("tab" << tab1 << "separator" << "\\t" << "showAt" << "read" << "0" << "1" << "2", "A\tB\tC"); + + // Chain functions without arguments. + RUN("enable" << "disable" << "monitoring", "false\n"); + RUN("if (!monitoring()) enable" << "monitoring", "true\n"); + + // Don't treat arguments after "eval" as functions. + RUN("eval" << "arguments[1]" << "TEST", "TEST"); + RUN("eval" << "arguments[1]" << "--" << "TEST", "TEST"); +} + +void TestsScripts::insertRemoveItems() +{ + const Args args = Args("tab") << testTab(1) << "separator" << ","; + + RUN(args << "add" << "ghi" << "abc", ""); + RUN(args << "insert" << "1" << "def", ""); + RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "abc,def,ghi,,"); + + RUN(args << "insert" << "0" << "012", ""); + RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "012,abc,def,ghi,"); + + RUN(args << "remove" << "0" << "2", ""); + RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "abc,ghi,,,"); + + QByteArray in("ABC"); + QCOMPARE( run(Args(args) << "insert" << "1" << "-", nullptr, nullptr, in), 0); + RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "abc,ABC,ghi,,"); +} diff --git a/src/tests/tests_scripts.h b/src/tests/tests_scripts.h new file mode 100644 index 0000000000..2ede1be463 --- /dev/null +++ b/src/tests/tests_scripts.h @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "tests/tests_base.h" + +class TestsScripts : public TestsBase +{ + Q_OBJECT + +public: + explicit TestsScripts(const TestInterfacePtr &test) : TestsBase(test) {} + +private slots: + void commandExit(); + void commandEval(); + void commandEvalThrows(); + void commandEvalSyntaxError(); + void commandEvalArguments(); + void commandEvalEndingWithComment(); + void commandPrint(); + void commandAbort(); + void commandFail(); + void commandSource(); + + void commandVisible(); + void commandToggle(); + void commandHide(); + void commandShow(); + void commandShowAt(); + void commandFocused(); + + void commandsUnicode(); + + void commandsAddRead(); + void commandsWriteRead(); + void commandChange(); + + void commandSetCurrentTab(); + + void commandConfig(); + void commandToggleConfig(); + + void commandDialog(); + void commandDialogCloseOnDisconnect(); + + void commandMenuItems(); + + void commandsPackUnpack(); + void commandsBase64(); + void commandsGetSetItem(); + + void commandsChecksums(); + + void commandEscapeHTML(); + + void commandExecute(); + + void commandSettings(); + + void commandsEnvSetEnv(); + + void commandSleep(); + + void commandsData(); + + void commandCurrentWindowTitle(); + + void commandCopy(); + void commandClipboard(); + void commandHasClipboardFormat(); + + void commandEdit(); + void commandEditItem(); + + void commandGetSetCurrentPath(); + + void commandSelectItems(); + + void commandsExportImport(); + + void commandsGetSetCommands(); + + void commandsImportExportCommands(); + void commandsImportExportCommandsFixIndentation(); + + void commandsAddCommandsRegExp(); + + void commandScreenshot(); + + void commandNotification(); + + void commandIcon(); + void commandIconTag(); + void commandIconTagColor(); + void commandLoadTheme(); + + void commandDateString(); + + void commandAfterMilliseconds(); + + void commandAsync(); + + void commandFilter(); + + void commandMimeTypes(); + + void commandUnload(); + void commandForceUnload(); + + void commandServerLogAndLogs(); + + void chainingCommands(); + + void insertRemoveItems(); +}; diff --git a/src/tests/tests_tray.cpp b/src/tests/tests_tray.cpp new file mode 100644 index 0000000000..78d3d374e4 --- /dev/null +++ b/src/tests/tests_tray.cpp @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "test_utils.h" +#include "tests/tests_tray.h" + +#include "common/sleeptimer.h" + +// WORKAROUND: Checking clipboard right after closing menu gets stuck on OS X. +#define ACTIVATE_MENU_ITEM(MENU_ID, WIDGET_ID, CONTENT) \ + RUN("keys" << MENU_ID << "ENTER", ""); \ + RUN("keys" << WIDGET_ID, ""); \ + WAIT_FOR_CLIPBOARD(CONTENT) + +void TestsTray::tray() +{ + RUN("add" << "A", ""); + RUN("keys" << clipboardBrowserId, ""); + RUN("menu", ""); + ACTIVATE_MENU_ITEM(trayMenuId, clipboardBrowserId, "A"); +} + +void TestsTray::menu() +{ + const auto tab = testTab(1); + + RUN("tab" << tab << "add" << "D" << "C" << "B" << "A", ""); + + RUN("keys" << clipboardBrowserId, ""); + RUN("menu" << tab, ""); + ACTIVATE_MENU_ITEM(menuId, clipboardBrowserId, "A"); + + // Show menu with 2 items from the tab and select last one. + RUN("keys" << clipboardBrowserId, ""); + RUN("menu" << tab << "2", ""); + RUN("keys" << menuId << "END", ""); + ACTIVATE_MENU_ITEM(menuId, clipboardBrowserId, "B"); + +#ifdef Q_OS_MAC + SKIP("Number keys don't seem to work in the tray menu on macOS."); +#endif + + // Select item by row number. + RUN("tab" << tab << "add(3,2,1,0)", ""); + RUN("menu" << tab, ""); + RUN("keys" << menuId << "3" << clipboardBrowserId, ""); + WAIT_FOR_CLIPBOARD("2"); + + // Select item by index. + RUN("config" << "row_index_from_one" << "false", "false\n"); + RUN("tab" << tab << "add(3,2,1,0)", ""); + RUN("menu" << tab, ""); + RUN("keys" << menuId << "3" << clipboardBrowserId, ""); + WAIT_FOR_CLIPBOARD("3"); +} + +void TestsTray::traySearch() +{ + RUN("add" << "C" << "B" << "A", ""); + + RUN("keys" << clipboardBrowserId, ""); + RUN("menu", ""); + RUN("keys" << trayMenuId << "B", ""); + ACTIVATE_MENU_ITEM(trayMenuId, clipboardBrowserId, "B"); +} + +void TestsTray::trayPaste() +{ + RUN("config" << "tray_tab_is_current" << "false", "false\n"); + + const auto tab1 = testTab(1); + RUN("setCurrentTab" << tab1, ""); + RUN("keys" + << clipboardBrowserId << "CTRL+N" + << editorId << ":NEW ", ""); + + RUN("add" << "TEST", ""); + RUN("keys" << editorId, ""); + RUN("menu", ""); + ACTIVATE_MENU_ITEM(trayMenuId, editorId, "TEST"); + waitFor(waitMsPasteClipboard); + + RUN("keys" << editorId << "F2", ""); + RUN("tab" << tab1 << "read" << "0", "NEW TEST"); + + RUN("keys" + << clipboardBrowserId << "CTRL+N" + << editorId << ":NEW ", ""); + + RUN("config" << "tray_item_paste" << "false", "false\n"); + RUN("add" << "TEST2", ""); + RUN("keys" << editorId, ""); + RUN("menu", ""); + ACTIVATE_MENU_ITEM(trayMenuId, editorId, "TEST2"); + + RUN("keys" << editorId << "F2", ""); + RUN("tab" << tab1 << "read" << "0", "NEW "); +} + +void TestsTray::configTrayTab() +{ + const auto tab1 = testTab(1); + RUN("tab" << tab1 << "add" << "A", ""); + + const auto tab2 = testTab(2); + RUN("tab" << tab2 << "add" << "B", ""); + + RUN("config" << "tray_tab_is_current" << "false", "false\n"); + + RUN("config" << "tray_item_paste" << "false", "false\n"); + + RUN("config" << "tray_tab" << tab1, tab1 + "\n"); + + RUN("keys" << clipboardBrowserId, ""); + RUN("menu", ""); + ACTIVATE_MENU_ITEM(trayMenuId, clipboardBrowserId, "A"); + + RUN("config" << "tray_tab" << tab2, tab2 + "\n"); + + RUN("keys" << clipboardBrowserId, ""); + RUN("menu", ""); + ACTIVATE_MENU_ITEM(trayMenuId, clipboardBrowserId, "B"); +} + +void TestsTray::configMove() +{ + SKIP_ON_ENV("COPYQ_TESTS_SKIP_CONFIG_MOVE"); + + RUN("add" << "B" << "A", ""); + + RUN("config" << "tray_item_paste" << "false", "false\n"); + + RUN("config" << "move" << "true", "true\n"); + + RUN("keys" << clipboardBrowserId, ""); + RUN("menu", ""); + RUN("keys" << trayMenuId << "DOWN", ""); + ACTIVATE_MENU_ITEM(trayMenuId, clipboardBrowserId, "B"); + RUN("read" << "0" << "1", "B\nA"); + + RUN("config" << "move" << "false", "false\n"); + + RUN("keys" << clipboardBrowserId, ""); + RUN("menu", ""); + RUN("keys" << trayMenuId << "DOWN", ""); + ACTIVATE_MENU_ITEM(trayMenuId, clipboardBrowserId, "A"); + RUN("read" << "0" << "1", "B\nA"); +} + +void TestsTray::configTrayTabIsCurrent() +{ + const auto tab1 = testTab(1); + RUN("tab" << tab1 << "add" << "A", ""); + + const auto tab2 = testTab(2); + RUN("tab" << tab2 << "add" << "B", ""); + + RUN("config" << "tray_tab_is_current" << "true", "true\n"); + + RUN("setCurrentTab" << tab1, ""); + RUN("keys" << clipboardBrowserId, ""); + RUN("menu", ""); + ACTIVATE_MENU_ITEM(trayMenuId, clipboardBrowserId, "A"); + + RUN("setCurrentTab" << tab2, ""); + RUN("keys" << clipboardBrowserId, ""); + RUN("menu", ""); + ACTIVATE_MENU_ITEM(trayMenuId, clipboardBrowserId, "B"); +} diff --git a/src/tests/tests_tray.h b/src/tests/tests_tray.h new file mode 100644 index 0000000000..83287fee2b --- /dev/null +++ b/src/tests/tests_tray.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "tests/testinterface.h" +#include "tests/tests_base.h" + +class TestsTray : public TestsBase +{ + Q_OBJECT + +public: + explicit TestsTray(const TestInterfacePtr &test) : TestsBase(test) {} + +private slots: + void tray(); + void menu(); + + void traySearch(); + void trayPaste(); + + // Options for tray menu. + void configTrayTab(); + void configMove(); + void configTrayTabIsCurrent(); +};