#include "TMxpSendTagHandler.h" #include "TMxpStubClient.h" #include #include "TMxpProcessor.h" #include "TMxpTagParser.h" #include "TMxpTagProcessor.h" class TMxpSendTagHandlerTest : public QObject { Q_OBJECT private: QSharedPointer parseNode(const QString &tagText) const { auto nodes = TMxpTagParser::parseToMxpNodeList(tagText); return !nodes.isEmpty() ? nodes.first() : nullptr; } private slots: void testSendHrefUTF8FromMxpProcessor() { // issue #4368 TMxpStubClient stub; TMxpProcessor processor(&stub); processor.setMode(MXP_MODE_CODE_LOCK_SECURE); std::string input = "test link: áéíóúñ"; for (char &ch : input) { processor.processMxpInput(ch, true); } QCOMPARE(stub.mHrefs.size(), 1); QCOMPARE(stub.mHrefs[0], "send([[áéíóúñ]])"); QCOMPARE(stub.mHints.size(), 1); QCOMPARE(stub.mHints[0], "áéíóúñ"); } // Attribute values must be decoded with the session encoding, not hardcoded // UTF-8. On a Latin1 (ISO 8859-1) session, the href bytes "se\xF1or" are // "señor" - decoding them as UTF-8 would corrupt the 0xF1 byte. void testSendHrefNonUtf8FromMxpProcessor() { TMxpStubClient stub; stub.mEncoding = QByteArrayLiteral("ISO 8859-1"); TMxpProcessor processor(&stub); processor.setMode(MXP_MODE_CODE_LOCK_SECURE); std::string input = "link"; for (char &ch : input) { processor.processMxpInput(ch, true); } QCOMPARE(stub.mHrefs.size(), 1); QCOMPARE(stub.mHrefs[0], "send([[señor]])"); } // With no encoding negotiated yet (empty session encoding) attribute values // must still decode as UTF-8, matching the historical default. void testSendHrefEmptyEncodingDefaultsToUtf8() { TMxpStubClient stub; stub.mEncoding = QByteArray(); TMxpProcessor processor(&stub); processor.setMode(MXP_MODE_CODE_LOCK_SECURE); std::string input = "link"; for (char &ch : input) { processor.processMxpInput(ch, true); } QCOMPARE(stub.mHrefs.size(), 1); QCOMPARE(stub.mHrefs[0], "send([[áéíóúñ]])"); } void testSendHrefUTF8() { // issue #4368 QString input = "test link: áéíóúñ"; TMxpTagProcessor processor; TMxpStubClient stub; auto nodes = TMxpTagParser::parseToMxpNodeList(input, false); QCOMPARE(nodes.size(), 3); for (const auto &node : std::as_const(nodes)) { processor.handleNode(processor, stub, node.get()); } QCOMPARE(stub.mHrefs.size(), 1); QCOMPARE(stub.mHrefs[0], "send([[áéíóúñ]])"); QCOMPARE(stub.mHints.size(), 1); QCOMPARE(stub.mHints[0], "áéíóúñ"); } void testStaticText() { // Zugg TMxpStubContext ctx; TMxpStubClient stub; auto startTag = parseNode(""); auto endTag = parseNode(""); QVERIFY(startTag); QVERIFY(endTag); TMxpSendTagHandler sendTagHandler; TMxpTagHandler &tagHandler = sendTagHandler; tagHandler.handleTag(ctx, stub, startTag->asStartTag()); tagHandler.handleContent("Zugg"); tagHandler.handleTag(ctx, stub, endTag->asEndTag()); QCOMPARE(stub.mHrefs.size(), 1); QCOMPARE(stub.mHrefs[0], "printCmdLine([[tell Zugg ]])"); QCOMPARE(stub.mHints.size(), 1); QCOMPARE(stub.mHints[0], "tell Zugg "); } void testSimpleSend() { // north TMxpStubContext ctx; TMxpStubClient stub; MxpStartTag startTag("SEND"); MxpEndTag endTag("SEND"); TMxpSendTagHandler sendTagHandler; TMxpTagHandler &tagHandler = sendTagHandler; tagHandler.handleTag(ctx, stub, &startTag); tagHandler.handleContent("north"); tagHandler.handleTag(ctx, stub, &endTag); QCOMPARE(stub.mHrefs.size(), 1); QCOMPARE(stub.mHrefs[0], "send([[north]])"); QCOMPARE(stub.mHints.size(), 1); QCOMPARE(stub.mHints[0], "north"); } void testSendPrompt() { // north TMxpStubContext ctx; TMxpStubClient stub; auto startTag = parseNode(""); auto endTag = parseNode(""); QVERIFY(startTag); QVERIFY(endTag); TMxpSendTagHandler sendTagHandler; TMxpTagHandler &tagHandler = sendTagHandler; tagHandler.handleTag(ctx, stub, startTag->asStartTag()); tagHandler.handleContent("north"); tagHandler.handleTag(ctx, stub, endTag->asEndTag()); QCOMPARE(stub.mHrefs.size(), 1); QCOMPARE(stub.mHrefs[0], "printCmdLine([[north]])"); QCOMPARE(stub.mHints.size(), 1); QCOMPARE(stub.mHints[0], "north"); } void testSendHrefTextEntity() { // Example from Age of Elements QString input = "button"; TMxpTagProcessor processor; TMxpStubClient stub; auto nodes = TMxpTagParser::parseToMxpNodeList(input, false); for (const auto &node : std::as_const(nodes)) { processor.handleNode(processor, stub, node.get()); } QCOMPARE(stub.mHrefs.size(), 1); QCOMPARE(stub.mHrefs[0], "send([[push button]])"); QCOMPARE(stub.mHints.size(), 1); QCOMPARE(stub.mHints[0], "push button"); } void testResolvingEntity() { TMxpStubContext ctx; TMxpStubClient stub; ctx.getEntityResolver().registerEntity("&charName;", "Gandalf"); auto startTag = parseNode(""); auto endTag = parseNode(""); QVERIFY(startTag); QVERIFY(endTag); TMxpSendTagHandler sendTagHandler; TMxpTagHandler &tagHandler = sendTagHandler; tagHandler.handleTag(ctx, stub, startTag->asStartTag()); tagHandler.handleContent("TAG CONTENT"); tagHandler.handleTag(ctx, stub, endTag->asEndTag()); QCOMPARE(stub.mHrefs.size(), 1); QCOMPARE(stub.mHrefs[0], "send([[say I am Gandalf]])"); QCOMPARE(stub.mHints.size(), 1); QCOMPARE(stub.mHints[0], "say I am Gandalf"); } void testResolvingEntityWithPipe() { TMxpStubContext ctx; TMxpStubClient stub; ctx.getEntityResolver().registerEntity("&frontHint;", ""); ctx.getEntityResolver().registerEntity("&frontHref;", ""); ctx.getEntityResolver().registerEntity("&backHints;", ""); ctx.getEntityResolver().registerEntity("&backHrefs;", ""); TMxpSendTagHandler sendTagHandler; TMxpTagHandler &tagHandler = sendTagHandler; // First check the SEND TAG with empty entities auto startTag = parseNode(""); auto endTag = parseNode(""); QVERIFY(startTag); QVERIFY(endTag); tagHandler.handleTag(ctx, stub, startTag->asStartTag()); tagHandler.handleContent("TAG CONTENT"); tagHandler.handleTag(ctx, stub, endTag->asEndTag()); QCOMPARE(stub.mHrefs.size(), 2); QCOMPARE(stub.mHrefs[0], "send([[look]])"); QCOMPARE(stub.mHrefs[1], "send([[say hello]])"); QCOMPARE(stub.mHints.size(), 2); QCOMPARE(stub.mHints[0], "LOOK AROUND"); QCOMPARE(stub.mHints[1], "SAY HELLO"); // Now add top menu entries ctx.getEntityResolver().registerEntity("&frontHint;", "WHO IS ONLINE?|"); ctx.getEntityResolver().registerEntity("&frontHref;", "who|"); tagHandler.handleTag(ctx, stub, startTag->asStartTag()); tagHandler.handleContent("TAG CONTENT"); tagHandler.handleTag(ctx, stub, endTag->asEndTag()); QCOMPARE(stub.mHrefs.size(), 3); QCOMPARE(stub.mHrefs[0], "send([[who]])"); QCOMPARE(stub.mHrefs[1], "send([[look]])"); QCOMPARE(stub.mHrefs[2], "send([[say hello]])"); QCOMPARE(stub.mHints.size(), 3); QCOMPARE(stub.mHints[0], "WHO IS ONLINE?"); QCOMPARE(stub.mHints[1], "LOOK AROUND"); QCOMPARE(stub.mHints[2], "SAY HELLO"); // Finally add something to the end of the menu ctx.getEntityResolver().registerEntity("&backHints;", "|KNOCK AT THE DOOR|BREAK THE DOOR"); ctx.getEntityResolver().registerEntity("&backHrefs;", "|knock at door|break door"); tagHandler.handleTag(ctx, stub, startTag->asStartTag()); tagHandler.handleContent("TAG CONTENT"); tagHandler.handleTag(ctx, stub, endTag->asEndTag()); QCOMPARE(stub.mHrefs.size(), 5); QCOMPARE(stub.mHrefs[0], "send([[who]])"); QCOMPARE(stub.mHrefs[1], "send([[look]])"); QCOMPARE(stub.mHrefs[2], "send([[say hello]])"); QCOMPARE(stub.mHrefs[3], "send([[knock at door]])"); QCOMPARE(stub.mHrefs[4], "send([[break door]])"); QCOMPARE(stub.mHints.size(), 5); QCOMPARE(stub.mHints[0], "WHO IS ONLINE?"); QCOMPARE(stub.mHints[1], "LOOK AROUND"); QCOMPARE(stub.mHints[2], "SAY HELLO"); QCOMPARE(stub.mHints[3], "KNOCK AT THE DOOR"); QCOMPARE(stub.mHints[4], "BREAK THE DOOR"); } void testSendHrefHintMismatch() { // Example from starmourn on WARES command from NPCs // 30901 TMxpStubContext ctx; TMxpStubClient stub; auto startTag = parseNode( R"()"); auto endTag = parseNode(""); QVERIFY(startTag); QVERIFY(endTag); TMxpSendTagHandler sendTagHandler; TMxpTagHandler &tagHandler = sendTagHandler; tagHandler.handleTag(ctx, stub, startTag->asStartTag()); tagHandler.handleContent("3091"); tagHandler.handleTag(ctx, stub, endTag->asEndTag()); QCOMPARE(stub.mHrefs.size(), 2); QCOMPARE(stub.mHrefs[0], "send([[PROBE SUSPENDERS30901]])"); QCOMPARE(stub.mHrefs[1], "send([[BUY SUSPENDERS30901]])"); QCOMPARE(stub.mHints.size(), 2); QCOMPARE(stub.mHints[0], "PROBE SUSPENDERS30901"); QCOMPARE(stub.mHints[1], "BUY SUSPENDERS30901"); } void testSendExpireHref() { // Test case for issue #8383 // When EXPIRE comes before HREF, the tooltip should show the HREF value, // not the literal string "HREF" East TMxpStubContext ctx; TMxpStubClient stub; auto startTag = parseNode(R"()"); auto endTag = parseNode(""); QVERIFY(startTag); QVERIFY(endTag); TMxpSendTagHandler sendTagHandler; TMxpTagHandler &tagHandler = sendTagHandler; tagHandler.handleTag(ctx, stub, startTag->asStartTag()); tagHandler.handleContent("East"); tagHandler.handleTag(ctx, stub, endTag->asEndTag()); QCOMPARE(stub.mHrefs.size(), 1); QCOMPARE(stub.mHrefs[0], "send([[east]])"); QCOMPARE(stub.mHints.size(), 1); QCOMPARE(stub.mHints[0], "east"); // Should be "east", not "HREF" } void testSendExpireHrefWithoutHint() { // Test case for standard order: HREF then EXPIRE // West TMxpStubContext ctx; TMxpStubClient stub; auto startTag = parseNode(R"()"); auto endTag = parseNode(""); QVERIFY(startTag); QVERIFY(endTag); TMxpSendTagHandler sendTagHandler; TMxpTagHandler &tagHandler = sendTagHandler; tagHandler.handleTag(ctx, stub, startTag->asStartTag()); tagHandler.handleContent("West"); tagHandler.handleTag(ctx, stub, endTag->asEndTag()); QCOMPARE(stub.mHrefs.size(), 1); QCOMPARE(stub.mHrefs[0], "send([[west]])"); QCOMPARE(stub.mHints.size(), 1); QCOMPARE(stub.mHints[0], "west"); // Should be "west" } }; #include "TMxpSendTagHandlerTest.moc" QTEST_MAIN(TMxpSendTagHandlerTest)