/*********************************************************************** THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY DEVELOPER: Zihan Chen(vczh) ***********************************************************************/ #include "GacUI.UnitTest.h" /*********************************************************************** .\GUIUNITTESTPROTOCOL_RENDERING.CPP ***********************************************************************/ namespace vl::presentation::unittest { using namespace collections; using namespace remoteprotocol; /*********************************************************************** IGuiRemoteProtocolMessages (Rendering) ***********************************************************************/ Ptr UnitTestRemoteProtocol_Rendering::GetLastRenderingFrame() { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::GetLastRenderingCommands()#" CHECK_ERROR(lastRenderingCommandsOpening, ERROR_MESSAGE_PREFIX L"The latest frame of commands is not accepting new commands."); return loggedFrames[loggedFrames.Count() - 1]; #undef ERROR_MESSAGE_PREFIX } Ptr UnitTestRemoteProtocol_Rendering::TryGetLastRenderingFrameAndReset() { if (loggedFrames.Count() == 0) return nullptr; if (!lastRenderingCommandsOpening) return nullptr; lastRenderingCommandsOpening = false; return loggedFrames[loggedFrames.Count() - 1]; } void UnitTestRemoteProtocol_Rendering::Impl_RendererBeginRendering(const ElementBeginRendering& arguments) { receivedDomDiffMessage = false; receivedElementMessage = false; lastRenderingCommandsOpening = true; auto frame = Ptr(new UnitTestLoggedFrame); frame->frameId = arguments.frameId; loggedFrames.Add(frame); if (arguments.updatedElements && arguments.updatedElements.Obj()->Count() > 0) { // Apply element updates first so event logs remain backward-compatible: // Updated(...), Updated(...), ..., Begin() for (auto&& desc : *arguments.updatedElements.Obj()) { desc.Apply(Overloading( [&](const ElementDesc_SolidBorder& d) { Impl_RendererUpdateElement_SolidBorder(d); }, [&](const ElementDesc_SinkBorder& d) { Impl_RendererUpdateElement_SinkBorder(d); }, [&](const ElementDesc_SinkSplitter& d) { Impl_RendererUpdateElement_SinkSplitter(d); }, [&](const ElementDesc_SolidBackground& d) { Impl_RendererUpdateElement_SolidBackground(d); }, [&](const ElementDesc_GradientBackground& d) { Impl_RendererUpdateElement_GradientBackground(d); }, [&](const ElementDesc_InnerShadow& d) { Impl_RendererUpdateElement_InnerShadow(d); }, [&](const ElementDesc_Polygon& d) { Impl_RendererUpdateElement_Polygon(d); }, [&](const ElementDesc_SolidLabel& d) { Impl_RendererUpdateElement_SolidLabel(d); }, [&](const ElementDesc_ImageFrame& d) { Impl_RendererUpdateElement_ImageFrame(d); } )); } renderingDomBuilder.RequestRendererBeginRendering(); } } void UnitTestRemoteProtocol_Rendering::Impl_RendererEndRendering(vint id) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererEndRendering(vint)#" CHECK_ERROR(receivedElementMessage || receivedDomDiffMessage, ERROR_MESSAGE_PREFIX L"Either dom-diff or element message should be sent before this message."); auto lastFrame = GetLastRenderingFrame(); if (receivedElementMessage) { lastFrame->renderingDom = renderingDomBuilder.RequestRendererEndRendering(); } if (receivedDomDiffMessage) { // receivedDom will be updated in RequestRendererRenderDomDiff // store a copy to log lastFrame->renderingDom = CopyDom(receivedDom); } this->GetEvents()->RespondRendererEndRendering(id, measuringForNextRendering); measuringForNextRendering = {}; #undef ERROR_MESSAGE_PREFIX } /*********************************************************************** IGuiRemoteProtocolMessages (Rendering - Element) ***********************************************************************/ void UnitTestRemoteProtocol_Rendering::Impl_RendererBeginBoundary(const ElementBoundary& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererBeginBoundary(const ElementBoundary&)#" CHECK_ERROR(!receivedDomDiffMessage, ERROR_MESSAGE_PREFIX L"This message could not be used with dom-diff messages in the same rendering cycle."); if (!receivedElementMessage) { renderingDomBuilder.RequestRendererBeginRendering(); receivedElementMessage = true; } renderingDomBuilder.RequestRendererBeginBoundary(arguments); glr::json::JsonFormatting formatting; formatting.spaceAfterColon = true; formatting.spaceAfterComma = true; formatting.crlf = false; formatting.compact = true; GetLastRenderingFrame()->renderingCommandsLog.Add(L"RequestRendererBeginBoundary: " + stream::GenerateToStream([&](stream::TextWriter& writer) { auto jsonLog = ConvertCustomTypeToJson(arguments); writer.WriteString(glr::json::JsonToString(jsonLog, formatting)); })); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_RendererEndBoundary() { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererEndBoundary()#" CHECK_ERROR(!receivedDomDiffMessage, ERROR_MESSAGE_PREFIX L"This message could not be used with dom-diff messages in the same rendering cycle."); if (!receivedElementMessage) { renderingDomBuilder.RequestRendererBeginRendering(); receivedElementMessage = true; } renderingDomBuilder.RequestRendererEndBoundary(); GetLastRenderingFrame()->renderingCommandsLog.Add(L"RequestRendererEndBoundary"); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::EnsureRenderedElement(vint id, Rect bounds) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::EnsureRenderedElement(id&)#" vint index = loggedTrace.createdElements->Keys().IndexOf(id); CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created."); auto rendererType = loggedTrace.createdElements->Values()[index]; if (rendererType == RendererType::FocusRectangle || rendererType == RendererType::Raw) { // FocusRectangle or Raw does not have a ElementDesc return; } index = lastElementDescs.Keys().IndexOf(id); CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been updated after created."); lastElementDescs.Values()[index].Apply(Overloading( [](RendererType) { CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been updated after created."); }, [&](const ElementDesc_SolidLabel& solidLabel) { CalculateSolidLabelSizeIfNecessary(bounds.Width(), bounds.Height(), solidLabel); }, [&](const auto& element) { })); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_RendererRenderElement(const ElementRendering& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererRenderElement(const ElementRendering&)#" CHECK_ERROR(!receivedDomDiffMessage, ERROR_MESSAGE_PREFIX L"This message could not be used with dom-diff messages in the same rendering cycle."); if (!receivedElementMessage) { renderingDomBuilder.RequestRendererBeginRendering(); receivedElementMessage = true; } { renderingDomBuilder.RequestRendererRenderElement(arguments); glr::json::JsonFormatting formatting; formatting.spaceAfterColon = true; formatting.spaceAfterComma = true; formatting.crlf = false; formatting.compact = true; GetLastRenderingFrame()->renderingCommandsLog.Add(L"RequestRendererRenderElement: " + stream::GenerateToStream([&](stream::TextWriter& writer) { auto jsonLog = ConvertCustomTypeToJson(arguments); writer.WriteString(glr::json::JsonToString(jsonLog, formatting)); })); } EnsureRenderedElement(arguments.id, arguments.bounds); #undef ERROR_MESSAGE_PREFIX } /*********************************************************************** IGuiRemoteProtocolMessages (Rendering - Dom) ***********************************************************************/ void UnitTestRemoteProtocol_Rendering::CalculateSolidLabelSizesIfNecessary(Ptr dom) { if (dom->content.element) { EnsureRenderedElement(dom->content.element.Value(), dom->content.bounds); } if (dom->children) { for (auto child : *dom->children.Obj()) { CalculateSolidLabelSizesIfNecessary(child); } } } void UnitTestRemoteProtocol_Rendering::Impl_RendererRenderDom(const Ptr& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererRenderElement(const RenderingDom&)#" CHECK_ERROR(!receivedElementMessage, ERROR_MESSAGE_PREFIX L"This message could not be used with element messages in the same rendering cycle."); if (!receivedDomDiffMessage) { receivedDomDiffMessage = true; } receivedDom = arguments; BuildDomIndex(receivedDom, receivedDomIndex); CalculateSolidLabelSizesIfNecessary(receivedDom); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_RendererRenderDomDiff(const RenderingDom_DiffsInOrder& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererRenderElement(const RenderingDom_DiffsInOrder&)#" CHECK_ERROR(!receivedElementMessage, ERROR_MESSAGE_PREFIX L"This message could not be used with element messages in the same rendering cycle."); if (!receivedDomDiffMessage) { receivedDomDiffMessage = true; } UpdateDomInplace(receivedDom, receivedDomIndex, arguments); GetLastRenderingFrame()->renderingDiffs = arguments; CalculateSolidLabelSizesIfNecessary(receivedDom); #undef ERROR_MESSAGE_PREFIX } /*********************************************************************** IGuiRemoteProtocolMessages (Elements) ***********************************************************************/ void UnitTestRemoteProtocol_Rendering::Impl_RendererCreated(const Ptr>& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererCreated(const Ptr>&)#" if (arguments) { for (auto creation : *arguments.Obj()) { CHECK_ERROR(!loggedTrace.createdElements->Keys().Contains(creation.id), ERROR_MESSAGE_PREFIX L"Renderer with the specified id has been created or used."); loggedTrace.createdElements->Add(creation.id, creation.type); // Create paragraph state for DocumentParagraph elements if (creation.type == RendererType::DocumentParagraph) { auto state = Ptr(new DocumentParagraphState()); paragraphStates.Add(creation.id, state); } } } #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_RendererDestroyed(const Ptr>& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererDestroyed(const Ptr>&)#" if (arguments) { for (auto id : *arguments.Obj()) { CHECK_ERROR(loggedTrace.createdElements->Keys().Contains(id), ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created."); CHECK_ERROR(!removedElementIds.Contains(id), ERROR_MESSAGE_PREFIX L"Renderer with the specified id has been destroyed."); removedElementIds.Add(id); lastElementDescs.Remove(id); // Remove paragraph state if this was a DocumentParagraph paragraphStates.Remove(id); } } #undef ERROR_MESSAGE_PREFIX } #define REQUEST_RENDERER_UPDATE_ELEMENT2(ARGUMENTS, RENDERER_TYPE)\ RequestRendererUpdateElement(\ ARGUMENTS,\ ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created.",\ ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type."\ ) #define REQUEST_RENDERER_UPDATE_ELEMENT(RENDERER_TYPE) REQUEST_RENDERER_UPDATE_ELEMENT2(arguments, RENDERER_TYPE) void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidBorder(const ElementDesc_SolidBorder& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidBorder(const ElementDesc_SolidBorder&)#" REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::SolidBorder); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SinkBorder(const ElementDesc_SinkBorder& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SinkBorder(const ElementDesc_SinkBorder&)#" REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::SinkBorder); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SinkSplitter(const ElementDesc_SinkSplitter& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SinkSplitter(const ElementDesc_SinkSplitter&)#" REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::SinkSplitter); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidBackground(const ElementDesc_SolidBackground& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidBackground(const ElementDesc_SolidBackground&)#" REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::SolidBackground); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_GradientBackground(const ElementDesc_GradientBackground& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_GradientBackground(const ElementDesc_GradientBackground&)#" REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::GradientBackground); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_InnerShadow(const ElementDesc_InnerShadow& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_InnerShadow(const ElementDesc_InnerShadow&)#" REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::InnerShadow); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_Polygon(const ElementDesc_Polygon& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_Polygon(const ElementDesc_Polygon&)#" REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::Polygon); #undef ERROR_MESSAGE_PREFIX } /*********************************************************************** IGuiRemoteProtocolMessages (Elements - SolidLabel) ***********************************************************************/ void UnitTestRemoteProtocol_Rendering::CalculateSolidLabelSizeIfNecessary(vint width, vint height, const ElementDesc_SolidLabel& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::CalculateSolidLabelSizeIfNecessary(vint, vint, const ElementDesc_SolidLabel&)#" if (arguments.measuringRequest) { switch (arguments.measuringRequest.Value()) { case ElementSolidLabelMeasuringRequest::FontHeight: CHECK_ERROR(arguments.font, ERROR_MESSAGE_PREFIX L"Font is missing for calculating font height."); if (!measuringForNextRendering.fontHeights) { measuringForNextRendering.fontHeights = Ptr(new List); } { ElementMeasuring_FontHeight measuring; measuring.fontFamily = arguments.font.Value().fontFamily; measuring.fontSize = arguments.font.Value().size; measuring.height = measuring.fontSize + 4; measuringForNextRendering.fontHeights->Add(measuring); } break; case ElementSolidLabelMeasuringRequest::TotalSize: { // font and text has already been verified exist in RequestRendererUpdateElement_SolidLabel vint size = arguments.font.Value().size; auto text = arguments.text.Value(); vint textWidth = 0; vint textHeight = 0; List lines; { List> matches; regexCrLf.Split(text, true, matches); if (matches.Count() == 0) { // when there is no text, measure a space lines.Add(WString::Unmanaged(L" ")); } else if (arguments.multiline) { // add all lines, and if any line is empty, measure a space for (auto match : matches) { auto line = match->Result().Value(); lines.Add(line.Length() ? line : WString::Unmanaged(L" ")); } } else { lines.Add(stream::GenerateToStream([&](stream::TextWriter& writer) { for (auto [match, index] : indexed(matches)) { if (index > 0) writer.WriteChar(L' '); auto line = match->Result().Value(); writer.WriteString(line); } })); if(lines[0].Length() == 0) { // when there is no text, measure a space lines[0] = WString::Unmanaged(L" "); } } } if (arguments.wrapLine) { // width of the text is 0 // insert a line break when there is no space horizontally vint totalLines = 0; for (auto&& line : lines) { if (line.Length() == 0) { totalLines++; continue; } double accumulatedWidth = 0; for (vint i = 0; i < line.Length(); i++) { auto c = line[i]; auto w = (c < 128 ? 0.6 : 1) * size; if (accumulatedWidth + w > width) { if (accumulatedWidth == 0) { totalLines++; } else { totalLines++; accumulatedWidth = w; } } else { accumulatedWidth += w; } } if (accumulatedWidth > 0) { totalLines++; } } textHeight = 4 + size * totalLines; } else { // width of the text is width of the longest line textWidth = (vint)(size * From(lines) .Select([](const WString& line) { double sum = 0; for (vint i = 0; i < line.Length(); i++) { auto c = line[i]; sum += (c < 128 ? 0.6 : 1); } return sum; }) .Max()); textHeight = 4 + size * lines.Count(); } if (!measuringForNextRendering.minSizes) { measuringForNextRendering.minSizes = Ptr(new List); } { ElementMeasuring_ElementMinSize measuring; measuring.id = arguments.id; measuring.minSize = { textWidth,textHeight }; measuringForNextRendering.minSizes->Add(measuring); } } break; default: CHECK_FAIL(L"Unknown value of ElementSolidLabelMeasuringRequest."); } } #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidLabel(const ElementDesc_SolidLabel& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidLabel(const ElementDesc_SolidLabel&)#" auto element = arguments; if (!element.font || !element.text) { vint index = loggedTrace.createdElements->Keys().IndexOf(element.id); CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created."); auto rendererType = loggedTrace.createdElements->Values()[index]; CHECK_ERROR(rendererType == RendererType::SolidLabel, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type."); index = lastElementDescs.Keys().IndexOf(arguments.id); if (index != -1) { auto solidLabel = lastElementDescs.Values()[index].TryGet(); CHECK_ERROR(solidLabel, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type."); if (!element.font) element.font = solidLabel->font; if (!element.text) element.text = solidLabel->text; } else { if (!element.font) element.font = FontProperties(); if (!element.text) element.text = WString::Empty; } } REQUEST_RENDERER_UPDATE_ELEMENT2(element, RendererType::SolidLabel); #undef ERROR_MESSAGE_PREFIX } /*********************************************************************** IGuiRemoteProtocolMessages (Elements - Image) ***********************************************************************/ WString UnitTestRemoteProtocol_Rendering::GetBinaryKeyFromBinary(stream::IStream& binary) { stream::MemoryStream base64WStringStream; { stream::UtfGeneralEncoder utf8ToWCharEncoder; stream::EncoderStream utf8ToWCharStream(base64WStringStream, utf8ToWCharEncoder); stream::Utf8Base64Encoder binaryToBase64Utf8Encoder; stream::EncoderStream binaryToBase64Utf8Stream(utf8ToWCharStream, binaryToBase64Utf8Encoder); binary.SeekFromBegin(0); stream::CopyStream(binary, binaryToBase64Utf8Stream); } { base64WStringStream.SeekFromBegin(0); stream::StreamReader reader(base64WStringStream); return reader.ReadToEnd(); } } WString UnitTestRemoteProtocol_Rendering::GetBinaryKeyFromImage(Ptr image) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::GetBinaryKeyFromImage(Ptr)#" auto remoteImage = image.Cast(); CHECK_ERROR(remoteImage, ERROR_MESSAGE_PREFIX L"The image object must be GuiRemoteGraphicsImage."); return GetBinaryKeyFromBinary(remoteImage->GetBinaryData()); #undef ERROR_MESSAGE_PREFIX } ImageMetadata UnitTestRemoteProtocol_Rendering::MakeImageMetadata(const ImageCreation& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::MakeImageMetadata(const remoteprotocol::ImageCreation)#" if (!cachedImageMetadatas) { cachedImageMetadatas = Ptr(new Base64ToImageMetadataMap); for (auto resource : GetResourceManager()->GetLoadedResources()) { if (auto xmlImageData = resource->GetValueByPath(WString::Unmanaged(L"UnitTestConfig/ImageData")).Cast()) { for (auto elementImage : glr::xml::XmlGetElements(xmlImageData->rootElement, WString::Unmanaged(L"Image"))) { WString path, format, frames = WString::Unmanaged(L"1"), width, height; auto attPath = glr::xml::XmlGetAttribute(elementImage.Obj(), WString::Unmanaged(L"Path")); auto attFormat = glr::xml::XmlGetAttribute(elementImage.Obj(), WString::Unmanaged(L"Format")); auto attFrames = glr::xml::XmlGetAttribute(elementImage.Obj(), WString::Unmanaged(L"Frames")); auto attWidth = glr::xml::XmlGetAttribute(elementImage.Obj(), WString::Unmanaged(L"Width")); auto attHeight = glr::xml::XmlGetAttribute(elementImage.Obj(), WString::Unmanaged(L"Height")); CHECK_ERROR(attPath, ERROR_MESSAGE_PREFIX L"Missing Path attribute in Image element in an UnitTestConfig/ImageData."); CHECK_ERROR(attFormat, ERROR_MESSAGE_PREFIX L"Missing Format attribute in Image element in an UnitTestConfig/ImageData."); CHECK_ERROR(attWidth, ERROR_MESSAGE_PREFIX L"Missing Width attribute in Image element in an UnitTestConfig/ImageData."); CHECK_ERROR(attHeight, ERROR_MESSAGE_PREFIX L"Missing Height attribute in Image element in an UnitTestConfig/ImageData."); path = attPath->value.value; format = attFormat->value.value; width = attWidth->value.value; height = attHeight->value.value; if (attFrames) frames = attFrames->value.value; vint valueFrames = wtoi(frames); vint valueWidth = wtoi(width); vint valueHeight = wtoi(height); CHECK_ERROR(itow(valueFrames) == frames, ERROR_MESSAGE_PREFIX L"Frames attribute must be an integer in Image element in an UnitTestConfig/ImageData."); CHECK_ERROR(itow(valueWidth) == width, ERROR_MESSAGE_PREFIX L"Width attribute must be an integer in Image element in an UnitTestConfig/ImageData."); CHECK_ERROR(itow(valueHeight) == height, ERROR_MESSAGE_PREFIX L"Height attribute must be an integer in Image element in an UnitTestConfig/ImageData."); auto imageData = resource->GetImageByPath(path); WString binaryKey = GetBinaryKeyFromImage(imageData->GetImage()); if (!cachedImageMetadatas->Keys().Contains(binaryKey)) { ImageMetadata imageMetadata; imageMetadata.id = -1; imageMetadata.frames = Ptr(new List); { auto node = Ptr(new glr::json::JsonString); node->content.value = format; ConvertJsonToCustomType(node, imageMetadata.format); } for (vint frame = 0; frame < valueFrames; frame++) { imageMetadata.frames->Add({ {valueWidth,valueHeight} }); } cachedImageMetadatas->Add(binaryKey, imageMetadata); } } } } } auto binaryKey = GetBinaryKeyFromBinary(*arguments.imageData.Obj()); vint binaryIndex = cachedImageMetadatas->Keys().IndexOf(binaryKey); CHECK_ERROR(binaryIndex != -1, ERROR_MESSAGE_PREFIX L"The image is not registered in any UnitTestConfig/ImageData."); auto metadata = cachedImageMetadatas->Values()[binaryIndex]; metadata.id = arguments.id; loggedTrace.imageCreations->Add(arguments); loggedTrace.imageMetadatas->Add(metadata); return metadata; #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_ImageCreated(vint id, const ImageCreation& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_ImageCreated(vint, const vint&)#" CHECK_ERROR(!loggedTrace.imageMetadatas->Keys().Contains(arguments.id), ERROR_MESSAGE_PREFIX L"Image with the specified id has been created or used."); this->GetEvents()->RespondImageCreated(id, MakeImageMetadata(arguments)); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_ImageDestroyed(const vint& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_ImageDestroyed(const vint&)#" CHECK_ERROR(loggedTrace.imageMetadatas->Keys().Contains(arguments), ERROR_MESSAGE_PREFIX L"Image with the specified id has not been created."); CHECK_ERROR(!removedImageIds.Contains(arguments), ERROR_MESSAGE_PREFIX L"Image with the specified id has been destroyed."); removedImageIds.Add(arguments); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_ImageFrame(const ElementDesc_ImageFrame& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_ImageFrame(const ElementDesc_ImageFrame&)#" if (arguments.imageCreation) { auto&& imageCreation = arguments.imageCreation.Value(); if (!imageCreation.imageDataOmitted) { CHECK_ERROR(arguments.imageId && arguments.imageId.Value() != !imageCreation.id, ERROR_MESSAGE_PREFIX L"It should satisfy that (arguments.imageId.Value()id == imageCreation.id)."); CHECK_ERROR(!loggedTrace.imageMetadatas->Keys().Contains(imageCreation.id), ERROR_MESSAGE_PREFIX L"Image with the specified id has been created."); CHECK_ERROR(imageCreation.imageData, ERROR_MESSAGE_PREFIX L"When imageDataOmitted == false, imageData should not be null."); if (!measuringForNextRendering.createdImages) { measuringForNextRendering.createdImages = Ptr(new List); } measuringForNextRendering.createdImages->Add(MakeImageMetadata(imageCreation)); } else { CHECK_ERROR(!imageCreation.imageData, ERROR_MESSAGE_PREFIX L"When imageDataOmitted == true, imageData should be null."); } } else if (arguments.imageId) { CHECK_ERROR(loggedTrace.imageMetadatas->Keys().Contains(arguments.imageId.Value()), ERROR_MESSAGE_PREFIX L"Image with the specified id has not been created."); } auto element = arguments; element.imageCreation.Reset(); REQUEST_RENDERER_UPDATE_ELEMENT2(element, RendererType::ImageFrame); #undef ERROR_MESSAGE_PREFIX } #undef REQUEST_RENDERER_UPDATE_ELEMENT #undef REQUEST_RENDERER_UPDATE_ELEMENT2 } /*********************************************************************** .\GUIUNITTESTPROTOCOL_RENDERING_DOCUMENT.CPP ***********************************************************************/ namespace vl::presentation::unittest { using namespace collections; using namespace remoteprotocol; /*********************************************************************** Helper Functions for Document Paragraph ***********************************************************************/ constexpr vint DefaultFontSize = 12; constexpr vint DefaultLineHeight = 16; void ClearCaretLayouts(DocumentParagraphState& state) { state.caretLayouts.Clear(); state.caretLayoutKeys.Clear(); } void AddCaretLayout(DocumentParagraphState& state, vint caret, const DocumentParagraphCharLayout& layout) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::AddCaretLayout(DocumentParagraphState&, vint, const DocumentParagraphCharLayout&)#" if (state.caretLayoutKeys.Count() > 0) { CHECK_ERROR(state.caretLayoutKeys[state.caretLayoutKeys.Count() - 1] < caret, ERROR_MESSAGE_PREFIX L"Duplicate or out-of-order caret."); } state.caretLayoutKeys.Add(caret); state.caretLayouts.Add(caret, layout); #undef ERROR_MESSAGE_PREFIX } vint FindLayoutCaretLE(const DocumentParagraphState& state, vint textPos) { if (state.caretLayoutKeys.Count() == 0) return -1; vint index = -1; auto comparer = [&](const vint& key, const vint& search) -> std::strong_ordering { return key <=> search; }; auto hit = BinarySearchLambda(&state.caretLayoutKeys[0], state.caretLayoutKeys.Count(), textPos, index, comparer); if (hit != -1) return state.caretLayoutKeys[hit]; if (index < 0) return -1; if (state.caretLayoutKeys[index] < textPos) return state.caretLayoutKeys[index]; if (index == 0) return -1; return state.caretLayoutKeys[index - 1]; } vint FindLayoutCaretGT(const DocumentParagraphState& state, vint textPos) { if (state.caretLayoutKeys.Count() == 0) return -1; vint index = -1; auto comparer = [&](const vint& key, const vint& search) -> std::strong_ordering { return key <=> search; }; auto hit = BinarySearchLambda(&state.caretLayoutKeys[0], state.caretLayoutKeys.Count(), textPos, index, comparer); if (hit != -1) { if (hit + 1 < state.caretLayoutKeys.Count()) return state.caretLayoutKeys[hit + 1]; return -1; } if (index < 0) return state.caretLayoutKeys[0]; if (state.caretLayoutKeys[index] > textPos) return state.caretLayoutKeys[index]; if (index + 1 < state.caretLayoutKeys.Count()) return state.caretLayoutKeys[index + 1]; return -1; } vint FindFirstCaretKeyAtOrAfter(const DocumentParagraphState& state, vint textPos) { if (state.caretLayoutKeys.Count() == 0) return -1; vint index = -1; auto comparer = [&](const vint& key, const vint& search) -> std::strong_ordering { return key <=> search; }; auto hit = BinarySearchLambda(&state.caretLayoutKeys[0], state.caretLayoutKeys.Count(), textPos, index, comparer); if (hit != -1) return hit; if (index < 0) return 0; if (state.caretLayoutKeys[index] < textPos) { if (index + 1 < state.caretLayoutKeys.Count()) return index + 1; return -1; } return index; } bool TryGetLayoutAtCaret(const DocumentParagraphState& state, vint caret, DocumentParagraphCharLayout& layout) { if (!state.caretLayoutKeys.Contains(caret)) return false; layout = state.caretLayouts[caret]; return true; } bool TryGetInlineObjectRangeContaining(const DocumentParagraphState& state, vint caret, elements::CaretRange& range) { for (auto&& [inlineRange, _] : state.inlineObjectRuns) { if (caret > inlineRange.caretBegin && caret < inlineRange.caretEnd) { range = inlineRange; return true; } } return false; } bool TryGetInlineObjectRangeAtBegin(const DocumentParagraphState& state, vint caret, elements::CaretRange& range) { for (auto&& [inlineRange, _] : state.inlineObjectRuns) { if (caret == inlineRange.caretBegin) { range = inlineRange; return true; } } return false; } bool TryGetInlineObjectRangeAtEnd(const DocumentParagraphState& state, vint caret, elements::CaretRange& range) { for (auto&& [inlineRange, _] : state.inlineObjectRuns) { if (caret == inlineRange.caretEnd) { range = inlineRange; return true; } } return false; } bool IsValidCaretPosition(const DocumentParagraphState& state, vint caret) { if (caret < 0 || caret > state.text.Length()) return false; for (auto&& [range, _] : state.inlineObjectRuns) { if (caret > range.caretBegin && caret < range.caretEnd) { return false; } } return true; } vint GetFontSizeForPosition( const DocumentParagraphState& state, vint pos, Nullable>& inlineProp) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::GetFontSizeForPosition(...)#" inlineProp.Reset(); for (auto [range, prop] : state.mergedRuns) { if (pos >= range.caretBegin && pos < range.caretEnd) { if (auto textProp = prop.TryGet()) { return textProp->fontProperties.size; } if (auto objProp = prop.TryGet()) { inlineProp = { range.caretEnd - range.caretBegin,*objProp }; return 0; } } } CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Every character is expected to have a font."); #undef ERROR_MESSAGE_PREFIX } double GetCharacterWidth(wchar_t c, vint fontSize) { return (c < 128 ? 0.6 : 1.0) * fontSize; } void CalculateParagraphLayout(DocumentParagraphState& state) { ClearCaretLayouts(state); state.lines.Clear(); state.cachedSize = Size(0, DefaultLineHeight); state.cachedInlineObjectBounds.Clear(); const WString& text = state.text; if (text.Length() == 0) { // Empty paragraph has default size DocumentParagraphLineInfo line; line.startPos = 0; line.endPos = 0; line.y = 0; line.height = DefaultLineHeight; // Default: 12 (font) + 4 line.baseline = DefaultFontSize; line.width = 0; state.lines.Add(line); return; } // First pass: calculate per-character metrics struct TempCharInfo { vint caret; double x; double width; vint height; vint length; Nullable inlineObjectProp; }; List tempChars; List> lineRanges; // [start, end) for each line double currentX = 0; vint currentLineStart = 0; for (vint i = 0; i < text.Length(); i++) { wchar_t c = text[i]; TempCharInfo info = { i, currentX, 0, 0, 1, {} }; // Handle \r - zero width, no line break if (c == L'\r') { tempChars.Add(info); continue; } // Get character properties Nullable> inlinePair; vint fontSize = GetFontSizeForPosition(state, i, inlinePair); if (inlinePair) { auto& prop = inlinePair.Value().value; info.width = (double)prop.size.x; info.height = prop.size.y; info.length = inlinePair.Value().key; info.inlineObjectProp = inlinePair.Value().value; } else { if (fontSize <= 0) fontSize = DefaultFontSize; info.width = GetCharacterWidth(c, fontSize); info.height = fontSize; } // Handle \n - always break line if (c == L'\n') { info.width = 0; tempChars.Add(info); lineRanges.Add({ currentLineStart, i + 1 }); currentLineStart = i + 1; currentX = 0; continue; } // Check word wrap if (state.wrapLine && state.maxWidth > 0 && currentX > 0) { if (currentX + info.width > state.maxWidth) { lineRanges.Add({ currentLineStart, i }); currentLineStart = i; currentX = 0; } } info.x = currentX; tempChars.Add(info); currentX += info.width; if(inlinePair) { i += inlinePair.Value().key - 1; } } // Add final line if (currentLineStart <= text.Length()) { lineRanges.Add({ currentLineStart, text.Length() }); } // Handle empty case if (lineRanges.Count() == 0) { lineRanges.Add({ 0, 0 }); } // Second pass: calculate line heights using baseline alignment vint currentY = 0; for (auto [lineStart, lineEnd] : lineRanges) { vint maxAboveBaseline = 0; vint maxBelowBaseline = 0; for (auto&& info : tempChars) { if (info.caret < lineStart || info.caret >= lineEnd) continue; if (info.inlineObjectProp) { auto&& prop = info.inlineObjectProp.Value(); vint baseline = prop.baseline; if (baseline == -1) baseline = info.height; vint above = baseline; vint below = info.height - baseline; if (above < 0) above = 0; if (below < 0) below = 0; if (maxAboveBaseline < above) maxAboveBaseline = above; if (maxBelowBaseline < below) maxBelowBaseline = below; } else { if (maxAboveBaseline < info.height) maxAboveBaseline = info.height; } } DocumentParagraphLineInfo line; line.startPos = lineStart; line.endPos = lineEnd; line.y = currentY; line.height = maxAboveBaseline + maxBelowBaseline + 4; line.baseline = maxAboveBaseline; // Calculate line width double lineWidth = 0; for (auto&& info : tempChars) { if (info.caret < lineStart || info.caret >= lineEnd) continue; double endX = info.x + info.width; if (endX > lineWidth) lineWidth = endX; } line.width = (vint)lineWidth; // Fill inline object bounds for (auto&& info : tempChars) { if (info.caret < lineStart || info.caret >= lineEnd) continue; if (info.inlineObjectProp) { auto&& prop = info.inlineObjectProp.Value(); if (prop.callbackId != -1) { vint baseline = prop.baseline; if (baseline == -1) baseline = info.height; vint y = line.y + 2 + line.baseline - baseline; state.cachedInlineObjectBounds.Add(prop.callbackId, Rect(Point((vint)info.x, y), prop.size)); } } } state.lines.Add(line); currentY += line.height; } // Third pass: create final character layouts with line indices vint lineIdx = 0; for (auto&& info : tempChars) { while (lineIdx < state.lines.Count() - 1 && info.caret >= state.lines[lineIdx].endPos) { lineIdx++; } DocumentParagraphCharLayout cl; cl.x = info.x; cl.width = info.width; cl.lineIndex = lineIdx; cl.height = info.height; cl.length = info.length; cl.baseline = 0; cl.isInlineObject = (bool)info.inlineObjectProp; AddCaretLayout(state, info.caret, cl); } // Calculate total size vint maxWidth = 0; for (auto&& line : state.lines) { if (line.width > maxWidth) maxWidth = line.width; } state.cachedSize = Size(maxWidth, currentY > 0 ? currentY : DefaultLineHeight); } /*********************************************************************** IGuiRemoteProtocolMessages (Elements - Document) ***********************************************************************/ void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_DocumentParagraph(vint id, const ElementDesc_DocumentParagraph& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_DocumentParagraph(vint, const ElementDesc_DocumentParagraph&)#" vint index = paragraphStates.Keys().IndexOf(arguments.id); CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Paragraph not created."); auto state = paragraphStates.Values()[index]; // Apply text if provided (distinguish null vs empty string) if (arguments.text) { state->text = arguments.text.Value(); // Text changed - clear run maps since positions may be invalid state->textRuns.Clear(); state->inlineObjectRuns.Clear(); } // Always update these state->wrapLine = arguments.wrapLine; state->maxWidth = arguments.maxWidth; state->alignment = arguments.alignment; // Process removed inline objects first if (arguments.removedInlineObjects) { for (auto callbackId : *arguments.removedInlineObjects.Obj()) { // Find the range for this inline object and reset it for (auto [range, prop] : state->inlineObjectRuns) { if (prop.callbackId == callbackId) { elements::ResetInlineObjectRun(state->inlineObjectRuns, range); break; } } } } // Apply runsDiff using helper functions if (arguments.runsDiff) { for (auto run : *arguments.runsDiff.Obj()) { elements::CaretRange range{ run.caretBegin, run.caretEnd }; if (auto textProp = run.props.TryGet()) { elements::DocumentTextRunPropertyOverrides overrides; overrides.textColor = textProp->textColor; overrides.backgroundColor = textProp->backgroundColor; overrides.fontFamily = textProp->fontProperties.fontFamily; overrides.size = textProp->fontProperties.size; // Convert bool flags back to TextStyle elements::IGuiGraphicsParagraph::TextStyle style = (elements::IGuiGraphicsParagraph::TextStyle)0; if (textProp->fontProperties.bold) style = (elements::IGuiGraphicsParagraph::TextStyle)((vint)style | (vint)elements::IGuiGraphicsParagraph::TextStyle::Bold); if (textProp->fontProperties.italic) style = (elements::IGuiGraphicsParagraph::TextStyle)((vint)style | (vint)elements::IGuiGraphicsParagraph::TextStyle::Italic); if (textProp->fontProperties.underline) style = (elements::IGuiGraphicsParagraph::TextStyle)((vint)style | (vint)elements::IGuiGraphicsParagraph::TextStyle::Underline); if (textProp->fontProperties.strikeline) style = (elements::IGuiGraphicsParagraph::TextStyle)((vint)style | (vint)elements::IGuiGraphicsParagraph::TextStyle::Strikeline); overrides.textStyle = style; elements::AddTextRun(state->textRuns, range, overrides); } else if (auto inlineProp = run.props.TryGet()) { bool result = elements::AddInlineObjectRun(state->inlineObjectRuns, range, *inlineProp); TEST_ASSERT(result); } } } // Merge runs to create final result state->mergedRuns.Clear(); elements::MergeRuns(state->textRuns, state->inlineObjectRuns, state->mergedRuns); // Recalculate layout CalculateParagraphLayout(*state.Obj()); { index = loggedTrace.createdElements->Keys().IndexOf(arguments.id); CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created."); auto rendererType = loggedTrace.createdElements->Values()[index]; CHECK_ERROR(rendererType == RendererType::DocumentParagraph, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type."); index = lastElementDescs.Keys().IndexOf(arguments.id); ElementDesc_DocumentParagraphFull element; if (index != -1) { auto paragraphRef = lastElementDescs.Values()[index].TryGet(); CHECK_ERROR(paragraphRef, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type."); element = *paragraphRef; } element.paragraph = arguments; element.paragraph.text = state->text; element.paragraph.createdInlineObjects = {}; element.paragraph.removedInlineObjects = {}; element.paragraph.runsDiff = Ptr(new List); for (auto [range, props] : state->mergedRuns) { remoteprotocol::DocumentRun run; run.caretBegin = range.caretBegin; run.caretEnd = range.caretEnd; run.props = props; element.paragraph.runsDiff->Add(run); } lastElementDescs.Set(arguments.id, element); } // Send response with calculated size and inline object bounds UpdateElement_DocumentParagraphResponse response; response.documentSize = state->cachedSize; GetEvents()->RespondRendererUpdateElement_DocumentParagraph(id, response); // Store inlineObjectBounds if (state->cachedInlineObjectBounds.Count() > 0) { if (!measuringForNextRendering.inlineObjectBounds) { measuringForNextRendering.inlineObjectBounds = Ptr(new List); } for (vint i = 0; i < state->cachedInlineObjectBounds.Count(); i++) { ElementMeasuring_InlineObjectBounds bounds; bounds.elementId = arguments.id; bounds.callbackId = state->cachedInlineObjectBounds.Keys()[i]; bounds.bounds = state->cachedInlineObjectBounds.Values()[i]; measuringForNextRendering.inlineObjectBounds.Obj()->Add(bounds); } } #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetCaretBounds(vint id, const GetCaretBoundsRequest& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetCaretBounds(vint, const GetCaretBoundsRequest&)#" vint index = paragraphStates.Keys().IndexOf(arguments.id); CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"No active paragraph."); auto state = paragraphStates.Values()[index]; GetCaretBoundsResponse response; response.frontSideBounds = Ptr(new List); response.backSideBounds = Ptr(new List); if (state->text.Length() == 0) { Rect bounds = { 0,0,0,DefaultLineHeight }; response.frontSideBounds->Add(bounds); response.backSideBounds->Add(bounds); GetEvents()->RespondDocumentParagraph_GetCaretBounds(id, response); return; } for (vint caret = 0; caret <= state->text.Length(); caret++) { vint anchorCaret = FindLayoutCaretLE(*state.Obj(), caret); if (anchorCaret == -1) anchorCaret = FindLayoutCaretGT(*state.Obj(), caret); if (anchorCaret == -1) { Rect bounds = { 0,0,0,DefaultLineHeight }; response.frontSideBounds->Add(bounds); response.backSideBounds->Add(bounds); continue; } auto anchorLayout = state->caretLayouts[anchorCaret]; vint lineIndex = anchorLayout.lineIndex; vint y1 = state->lines[lineIndex].y; vint y2 = y1 + state->lines[lineIndex].height; vint x = (vint)anchorLayout.x; if (caret >= anchorCaret + anchorLayout.length) { x = (vint)(anchorLayout.x + anchorLayout.width); } response.backSideBounds->Add(Rect(x, y1, x, y2)); response.frontSideBounds->Add(Rect(x, y1, x, y2)); } GetEvents()->RespondDocumentParagraph_GetCaretBounds(id, response); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetCaret(vint id, const GetCaretRequest& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetCaret(vint, const GetCaretRequest&)#" vint index = paragraphStates.Keys().IndexOf(arguments.id); CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"No active paragraph."); auto state = paragraphStates.Values()[index]; vint caret = arguments.caret; auto relPos = arguments.relativePosition; GetCaretResponse response; response.preferFrontSide = true; vint textLen = state->text.Length(); // Clamp caret if (caret < 0) caret = 0; if (caret > textLen) caret = textLen; // Find current line vint lineIdx = 0; for (vint i = 0; i < state->lines.Count(); i++) { if (caret >= state->lines[i].startPos && caret <= state->lines[i].endPos) { lineIdx = i; break; } } using CRP = elements::IGuiGraphicsParagraph::CaretRelativePosition; switch (relPos) { case CRP::CaretFirst: response.newCaret = 0; break; case CRP::CaretLast: response.newCaret = textLen; break; case CRP::CaretLineFirst: response.newCaret = state->lines[lineIdx].startPos; if (!IsValidCaretPosition(*state.Obj(), response.newCaret)) { auto candidate = FindLayoutCaretGT(*state.Obj(), response.newCaret); if (candidate != -1 && candidate < state->lines[lineIdx].endPos) { response.newCaret = candidate; } } break; case CRP::CaretLineLast: response.newCaret = state->lines[lineIdx].endPos; if (response.newCaret > state->lines[lineIdx].startPos && response.newCaret > 0) { // Don't include CR/LF at end of line while (response.newCaret > state->lines[lineIdx].startPos && response.newCaret > 0) { auto ch = state->text[response.newCaret - 1]; if (ch == L'\r' || ch == L'\n') { response.newCaret--; } else { break; } } } if (!IsValidCaretPosition(*state.Obj(), response.newCaret)) { auto candidate = FindLayoutCaretLE(*state.Obj(), response.newCaret); if (candidate != -1 && candidate >= state->lines[lineIdx].startPos) { response.newCaret = candidate; } } break; case CRP::CaretMoveLeft: if (caret > 0) { elements::CaretRange inlineRange; if (TryGetInlineObjectRangeAtEnd(*state.Obj(), caret, inlineRange)) { response.newCaret = inlineRange.caretBegin; } else if (TryGetInlineObjectRangeContaining(*state.Obj(), caret - 1, inlineRange)) { response.newCaret = inlineRange.caretBegin; } else { response.newCaret = caret - 1; } } else { response.newCaret = 0; } break; case CRP::CaretMoveRight: if (caret < textLen) { elements::CaretRange inlineRange; if (TryGetInlineObjectRangeAtBegin(*state.Obj(), caret, inlineRange)) { response.newCaret = inlineRange.caretEnd; } else if (TryGetInlineObjectRangeContaining(*state.Obj(), caret + 1, inlineRange)) { response.newCaret = inlineRange.caretEnd; } else { response.newCaret = caret + 1; } } else { response.newCaret = textLen; } break; case CRP::CaretMoveUp: if (lineIdx > 0) { // Calculate x offset in current line vint xOffset = 0; if (caret > 0) { DocumentParagraphCharLayout prevLayout; auto layoutCaret = FindLayoutCaretLE(*state.Obj(), caret - 1); if (layoutCaret != -1 && TryGetLayoutAtCaret(*state.Obj(), layoutCaret, prevLayout)) { xOffset = (vint)(prevLayout.x + prevLayout.width); } } // Find corresponding position in previous line auto& prevLine = state->lines[lineIdx - 1]; response.newCaret = prevLine.startPos; auto keyIndex = FindFirstCaretKeyAtOrAfter(*state.Obj(), prevLine.startPos); if (keyIndex != -1) { for (vint i = keyIndex; i < state->caretLayoutKeys.Count(); i++) { auto caretKey = state->caretLayoutKeys[i]; if (caretKey >= prevLine.endPos) break; auto& ch = state->caretLayouts[caretKey]; if (ch.x + ch.width / 2 > xOffset) break; response.newCaret = caretKey + ch.length; } } } else { response.newCaret = caret; } break; case CRP::CaretMoveDown: if (lineIdx < state->lines.Count() - 1) { // Calculate x offset in current line vint xOffset = 0; if (caret > 0) { DocumentParagraphCharLayout prevLayout; auto layoutCaret = FindLayoutCaretLE(*state.Obj(), caret - 1); if (layoutCaret != -1 && TryGetLayoutAtCaret(*state.Obj(), layoutCaret, prevLayout)) { xOffset = (vint)(prevLayout.x + prevLayout.width); } } // Find corresponding position in next line auto& nextLine = state->lines[lineIdx + 1]; response.newCaret = nextLine.startPos; auto keyIndex = FindFirstCaretKeyAtOrAfter(*state.Obj(), nextLine.startPos); if (keyIndex != -1) { for (vint i = keyIndex; i < state->caretLayoutKeys.Count(); i++) { auto caretKey = state->caretLayoutKeys[i]; if (caretKey >= nextLine.endPos) break; auto& ch = state->caretLayouts[caretKey]; if (ch.x + ch.width / 2 > xOffset) break; response.newCaret = caretKey + ch.length; } } } else { response.newCaret = caret; } break; default: response.newCaret = caret; break; } while (!IsValidCaretPosition(*state.Obj(), response.newCaret)) { if (response.newCaret < caret) { auto candidate = FindLayoutCaretLE(*state.Obj(), response.newCaret); if (candidate == -1) break; if (candidate == response.newCaret) break; response.newCaret = candidate; } else if (response.newCaret > caret) { auto candidate = FindLayoutCaretGT(*state.Obj(), response.newCaret); if (candidate == -1) break; if (candidate == response.newCaret) break; response.newCaret = candidate; } else { break; } } if (!IsValidCaretPosition(*state.Obj(), response.newCaret)) { response.newCaret = caret; } GetEvents()->RespondDocumentParagraph_GetCaret(id, response); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetNearestCaretFromTextPos(vint id, const GetNearestCaretFromTextPosRequest& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetNearestCaretFromTextPos(vint, const GetCaretBoundsRequest&)#" vint index = paragraphStates.Keys().IndexOf(arguments.id); CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"No active paragraph."); auto state = paragraphStates.Values()[index]; vint textPos = arguments.textPos; vint textLen = state->text.Length(); // Clamp to valid range if (textPos < 0) textPos = 0; if (textPos > textLen) textPos = textLen; if (IsValidCaretPosition(*state.Obj(), textPos)) { GetEvents()->RespondDocumentParagraph_GetNearestCaretFromTextPos(id, textPos); return; } vint caret = -1; if (arguments.frontSide && textPos == textLen) { caret = textLen; } else if (arguments.frontSide) { caret = FindLayoutCaretLE(*state.Obj(), textPos); if (caret == -1) { caret = FindLayoutCaretGT(*state.Obj(), textPos); } } else { caret = FindLayoutCaretGT(*state.Obj(), textPos); if (caret == -1) { caret = FindLayoutCaretLE(*state.Obj(), textPos); } } if (caret == -1) caret = 0; GetEvents()->RespondDocumentParagraph_GetNearestCaretFromTextPos(id, caret); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetInlineObjectFromPoint(vint id, const GetInlineObjectFromPointRequest& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetInlineObjectFromPoint(vint, const GetInlineObjectFromPointRequest&)#" vint index = paragraphStates.Keys().IndexOf(arguments.id); CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"No active paragraph."); auto state = paragraphStates.Values()[index]; Point pt = arguments.point; Nullable result; // Find the line containing the Y coordinate vint lineIdx = -1; for (vint i = 0; i < state->lines.Count(); i++) { if (pt.y >= state->lines[i].y && pt.y < state->lines[i].y + state->lines[i].height) { lineIdx = i; break; } } if (lineIdx >= 0) { auto& line = state->lines[lineIdx]; // Apply alignment offset vint alignmentOffset = 0; if (state->alignment == ElementHorizontalAlignment::Center) { alignmentOffset = (state->cachedSize.x - line.width) / 2; } else if (state->alignment == ElementHorizontalAlignment::Right) { alignmentOffset = state->cachedSize.x - line.width; } vint relativeX = pt.x - alignmentOffset; // Check each character in the line auto keyIndex = FindFirstCaretKeyAtOrAfter(*state.Obj(), line.startPos); if (keyIndex != -1) { for (vint i = keyIndex; i < state->caretLayoutKeys.Count(); i++) { auto caretKey = state->caretLayoutKeys[i]; if (caretKey >= line.endPos) break; auto& ch = state->caretLayouts[caretKey]; if (ch.isInlineObject && relativeX >= ch.x && relativeX < ch.x + ch.width) { // Found an inline object - look up its properties for (auto [range, prop] : state->mergedRuns) { if (caretKey >= range.caretBegin && caretKey < range.caretEnd) { if (auto inlineProp = prop.TryGet()) { remoteprotocol::DocumentRun run; run.caretBegin = range.caretBegin; run.caretEnd = range.caretEnd; run.props = *inlineProp; result = run; break; } } } break; } } } } GetEvents()->RespondDocumentParagraph_GetInlineObjectFromPoint(id, result); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_IsValidCaret(vint id, const IsValidCaretRequest& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_IsValidCaret(vint, const IsValidCaretRequest&)#" vint index = paragraphStates.Keys().IndexOf(arguments.id); if (index == -1) { GetEvents()->RespondDocumentParagraph_IsValidCaret(id, false); return; } auto state = paragraphStates.Values()[index]; vint caret = arguments.caret; GetEvents()->RespondDocumentParagraph_IsValidCaret(id, IsValidCaretPosition(*state.Obj(), caret)); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_OpenCaret(const OpenCaretRequest& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_OpenCaret(const OpenCaretRequest&)#" vint index = loggedTrace.createdElements->Keys().IndexOf(arguments.id); CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created."); auto rendererType = loggedTrace.createdElements->Values()[index]; CHECK_ERROR(rendererType == RendererType::DocumentParagraph, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type."); index = lastElementDescs.Keys().IndexOf(arguments.id); ElementDesc_DocumentParagraphFull element; if (index != -1) { auto paragraphRef = lastElementDescs.Values()[index].TryGet(); CHECK_ERROR(paragraphRef, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type."); element = *paragraphRef; } element.caret = arguments; lastElementDescs.Set(arguments.id, element); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_CloseCaret(const vint& arguments) { #define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_CloseCaret(const vint&)#" vint index = loggedTrace.createdElements->Keys().IndexOf(arguments); CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created."); auto rendererType = loggedTrace.createdElements->Values()[index]; CHECK_ERROR(rendererType == RendererType::DocumentParagraph, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type."); index = lastElementDescs.Keys().IndexOf(arguments); ElementDesc_DocumentParagraphFull element; if (index != -1) { auto paragraphRef = lastElementDescs.Values()[index].TryGet(); CHECK_ERROR(paragraphRef, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type."); element = *paragraphRef; } element.caret.Reset(); lastElementDescs.Set(arguments, element); #undef ERROR_MESSAGE_PREFIX } } /*********************************************************************** .\GUIUNITTESTPROTOCOL_SHARED.CPP ***********************************************************************/ namespace vl::presentation::unittest { using namespace vl::collections; /*********************************************************************** UnitTestScreenConfig ***********************************************************************/ void UnitTestScreenConfig::FastInitialize(vint width, vint height, vint taskBarHeight) { executablePath = WString::Unmanaged(L"/GacUI/Remote/Protocol/UnitTest.exe"); customFramePadding = { 8,8,8,8 }; fontConfig.defaultFont.fontFamily = WString::Unmanaged(L"GacUI Default Font"); fontConfig.defaultFont.size = 12; fontConfig.supportedFonts = Ptr(new List()); fontConfig.supportedFonts->Add(fontConfig.defaultFont.fontFamily); screenConfig.bounds = { 0,0,width,height }; screenConfig.clientBounds = { 0,0,width,(height - taskBarHeight) }; screenConfig.scalingX = 1; screenConfig.scalingY = 1; } } /*********************************************************************** .\GUIUNITTESTUTILITIES.CPP ***********************************************************************/ namespace vl::presentation::controls { extern bool GACUI_UNITTEST_ONLY_SKIP_THREAD_LOCAL_STORAGE_DISPOSE_STORAGES; extern bool GACUI_UNITTEST_ONLY_SKIP_TYPE_AND_PLUGIN_LOAD_UNLOAD; } namespace vl::presentation::unittest { const UnitTestFrameworkConfig* globalUnitTestFrameworkConfig = nullptr; const UnitTestFrameworkConfig& GetUnitTestFrameworkConfig() { CHECK_ERROR(globalUnitTestFrameworkConfig, L"vl::presentation::unittest::GetUnitTestFrameworkConfig()#GacUIUnitTest_Initialize has not been called."); return *globalUnitTestFrameworkConfig; } } namespace vl::presentation::GuiHostedController_UnitTestHelper { extern bool ExceptionOccuredUnderUnitTestReleaseMode(); } using namespace vl; using namespace vl::collections; using namespace vl::filesystem; using namespace vl::reflection::description; using namespace vl::glr::json; using namespace vl::presentation; using namespace vl::presentation::remoteprotocol; using namespace vl::presentation::controls; using namespace vl::presentation::unittest; using namespace vl::presentation::GuiHostedController_UnitTestHelper; class UnitTestContextImpl : public Object, public virtual IUnitTestContext { UnitTestRemoteProtocol* protocol = nullptr; public: UnitTestContextImpl(UnitTestRemoteProtocol* _protocol) : protocol(_protocol) { } UnitTestRemoteProtocol* GetProtocol() { return protocol; } }; UnitTestMainFunc guiMainProxy; UnitTestContextImpl* guiMainUnitTestContext = nullptr; void GacUIUnitTest_Initialize(const UnitTestFrameworkConfig* config) { CHECK_ERROR(config, L"GacUIUnitTest_Initialize()#Argument config should not be null."); globalUnitTestFrameworkConfig = config; GACUI_UNITTEST_ONLY_SKIP_THREAD_LOCAL_STORAGE_DISPOSE_STORAGES = true; GACUI_UNITTEST_ONLY_SKIP_TYPE_AND_PLUGIN_LOAD_UNLOAD = true; GetGlobalTypeManager()->Load(); GetPluginManager()->Load(true, false); } void GacUIUnitTest_Finalize() { ResetGlobalTypeManager(); GetPluginManager()->Unload(true, false); DestroyPluginManager(); ThreadLocalStorage::DisposeStorages(); GACUI_UNITTEST_ONLY_SKIP_THREAD_LOCAL_STORAGE_DISPOSE_STORAGES = false; GACUI_UNITTEST_ONLY_SKIP_TYPE_AND_PLUGIN_LOAD_UNLOAD = false; globalUnitTestFrameworkConfig = nullptr; } void GacUIUnitTest_SetGuiMainProxy(const UnitTestMainFunc& proxy) { guiMainProxy = proxy; } void GacUIUnitTest_LinkGuiMainProxy(const UnitTestLinkFunc& proxy) { auto previousMainProxy = guiMainProxy; GacUIUnitTest_SetGuiMainProxy([=](UnitTestRemoteProtocol* protocol, IUnitTestContext* context) { proxy(protocol, context, previousMainProxy); }); } File GacUIUnitTest_PrepareSnapshotFile(const WString& appName, const WString& extension) { #define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_PrepareSnapshotFile(const WString&, const WString&)#" Folder snapshotFolder = GetUnitTestFrameworkConfig().snapshotFolder; CHECK_ERROR(snapshotFolder.Exists(), ERROR_MESSAGE_PREFIX L"UnitTestFrameworkConfig::snapshotFolder does not point to an existing folder."); File snapshotFile = snapshotFolder.GetFilePath() / (appName + extension); { auto pathPrefix = snapshotFolder.GetFilePath().GetFullPath() + WString::FromChar(FilePath::Delimiter); auto snapshotPath = snapshotFile.GetFilePath().GetFullPath(); CHECK_ERROR( snapshotPath.Length() > pathPrefix.Length() && snapshotPath.Left(pathPrefix.Length()) == pathPrefix, ERROR_MESSAGE_PREFIX L"Argument appName should specify a file that is inside UnitTestFrameworkConfig::snapshotFolder" ); Folder snapshotFileFolder = snapshotFile.GetFilePath().GetFolder(); if (!snapshotFileFolder.Exists()) { CHECK_ERROR(snapshotFileFolder.Create(true), ERROR_MESSAGE_PREFIX L"Failed to create the folder to contain the snapshot file specified by argument appName."); } } return snapshotFile; #undef ERROR_MESSAGE_PREFIX } Folder GacUIUnitTest_PrepareSnapshotFramesFolder(const WString& appName) { #define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_PrepareSnapshotFile(const WString&, const WString&)#" Folder snapshotRootFolder = GetUnitTestFrameworkConfig().snapshotFolder; CHECK_ERROR(snapshotRootFolder.Exists(), ERROR_MESSAGE_PREFIX L"UnitTestFrameworkConfig::snapshotFolder does not point to an existing folder."); Folder snapshotFramesFolder = snapshotRootFolder.GetFilePath() / appName; if (!snapshotFramesFolder.Exists()) { CHECK_ERROR(snapshotFramesFolder.Create(true), ERROR_MESSAGE_PREFIX L"Failed to create the folder containing frame snapshots."); } return snapshotFramesFolder; #undef ERROR_MESSAGE_PREFIX } void GacUIUnitTest_WriteSnapshotFileIfChanged(File& snapshotFile, const WString& textLog) { #define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_WriteSnapshotFileIfChanged(File&, const WString&)#" bool skipWriting = false; if (snapshotFile.Exists()) { auto previousLog = snapshotFile.ReadAllTextByBom(); if (previousLog == textLog) { skipWriting = true; } } if (!skipWriting) { bool succeeded = snapshotFile.WriteAllText(textLog, true, stream::BomEncoder::Utf8); CHECK_ERROR(succeeded, ERROR_MESSAGE_PREFIX L"Failed to write the snapshot file."); } #undef ERROR_MESSAGE_PREFIX } void GacUIUnitTest_LogUI(const WString& appName, UnitTestRemoteProtocol& unitTestProtocol) { #define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_LogUI(const WString&, UnitTestRemoteProtocol&)#" File snapshotFile = GacUIUnitTest_PrepareSnapshotFile(appName, WString::Unmanaged(L".json")); Folder snapshotFramesFolder = GacUIUnitTest_PrepareSnapshotFramesFolder(appName); JsonFormatting formatting; formatting.spaceAfterColon = true; formatting.spaceAfterComma = true; formatting.crlf = true; formatting.compact = true; auto renderingTrace = unitTestProtocol.GetLoggedTrace(); List frameTextLogs; SortedList frameFileNames; remoteprotocol::UnitTest_RenderingTrace deserialized; auto jsonLog = remoteprotocol::ConvertCustomTypeToJson(unitTestProtocol.GetLoggedTrace()); { auto textLog = JsonToString(jsonLog, formatting); remoteprotocol::ConvertJsonToCustomType(jsonLog, deserialized); auto jsonLog2 = remoteprotocol::ConvertCustomTypeToJson(deserialized); auto textLog2 = JsonToString(jsonLog2, formatting); CHECK_ERROR(textLog == textLog2, ERROR_MESSAGE_PREFIX L"Serialization and deserialization doesn't match."); } if (renderingTrace.frames) { for (vint i = 0; i < renderingTrace.frames->Count(); i++) { auto&& frame = renderingTrace.frames->Get(i); jsonLog = remoteprotocol::ConvertCustomTypeToJson(frame); auto textLog = JsonToString(jsonLog, formatting); WString frameFileName = L"frame_" + itow(i) + L".json"; frameFileNames.Add(frameFileName); File snapshotFrameFile = snapshotFramesFolder.GetFilePath() / frameFileName; GacUIUnitTest_WriteSnapshotFileIfChanged(snapshotFrameFile, textLog); renderingTrace.frames->Set(i, { .frameId = frame.frameId, .frameName = frame.frameName }); } } { jsonLog = remoteprotocol::ConvertCustomTypeToJson(unitTestProtocol.GetLoggedTrace()); auto textLog = JsonToString(jsonLog, formatting); GacUIUnitTest_WriteSnapshotFileIfChanged(snapshotFile, textLog); } List existingFrameFiles; snapshotFramesFolder.GetFiles(existingFrameFiles); for (auto&& file : existingFrameFiles) { if (!frameFileNames.Contains(file.GetFilePath().GetName())) { CHECK_ERROR(file.Delete(), ERROR_MESSAGE_PREFIX L"Failed to delete unnecessary frame file."); } } #undef ERROR_MESSAGE_PREFIX } void GacUIUnitTest_LogCommands(const WString& appName, UnitTestRemoteProtocol& unitTestProtocol) { File snapshotFile = GacUIUnitTest_PrepareSnapshotFile(appName, WString::Unmanaged(L"[commands].txt")); JsonFormatting formatting; formatting.spaceAfterColon = true; formatting.spaceAfterComma = true; formatting.crlf = false; formatting.compact = true; auto textLog = stream::GenerateToStream([&unitTestProtocol, &formatting](stream::TextWriter& writer) { auto&& loggedFrames = unitTestProtocol.GetLoggedFrames(); for (auto loggedFrame : loggedFrames) { writer.WriteLine(L"========================================"); writer.WriteLine(itow(loggedFrame->frameId)); writer.WriteLine(L"========================================"); for (auto&& commandLog : loggedFrame->renderingCommandsLog) { writer.WriteLine(commandLog); } }; }); GacUIUnitTest_WriteSnapshotFileIfChanged(snapshotFile, textLog); } void GacUIUnitTest_LogDiffs(const WString& appName, UnitTestRemoteProtocol& unitTestProtocol) { File snapshotFile = GacUIUnitTest_PrepareSnapshotFile(appName, WString::Unmanaged(L"[diffs].txt")); JsonFormatting formatting; formatting.spaceAfterColon = true; formatting.spaceAfterComma = true; formatting.crlf = false; formatting.compact = true; auto textLog = stream::GenerateToStream([&unitTestProtocol, &formatting](stream::TextWriter& writer) { Ptr dom; DomIndex domIndex; auto&& loggedFrames = unitTestProtocol.GetLoggedFrames(); for (auto loggedFrame : loggedFrames) { writer.WriteLine(L"========================================"); writer.WriteLine(itow(loggedFrame->frameId)); writer.WriteLine(L"========================================"); if (!dom) { dom = loggedFrame->renderingDom; BuildDomIndex(dom, domIndex); List>> lines; lines.Add({ 0,dom }); for (vint i = 0; i < lines.Count(); i++) { for (vint j = 0; j < lines[i].key; j++) { writer.WriteString(L" "); } auto line = lines[i].value; writer.WriteString(itow(line->id)); writer.WriteString(L": "); auto jsonLog = remoteprotocol::ConvertCustomTypeToJson(line->content); writer.WriteLine(JsonToString(jsonLog, formatting)); if (line->children) { for (auto child : *line->children.Obj()) { lines.Add({ lines[i].key + 1,child }); } } } } else { DomIndex nextDomIndex; BuildDomIndex(loggedFrame->renderingDom, nextDomIndex); Ptr> diffList; if (loggedFrame->renderingDiffs) { diffList = loggedFrame->renderingDiffs.Value().diffsInOrder; } else { RenderingDom_DiffsInOrder diffs; DiffDom(dom, domIndex, loggedFrame->renderingDom, nextDomIndex, diffs); diffList = diffs.diffsInOrder; auto copiedDom = CopyDom(dom); DomIndex copiedDomIndex; BuildDomIndex(copiedDom, copiedDomIndex); UpdateDomInplace(copiedDom, copiedDomIndex, diffs); auto expectedJson = JsonToString(remoteprotocol::ConvertCustomTypeToJson(loggedFrame->renderingDom)); auto actualJson = JsonToString(remoteprotocol::ConvertCustomTypeToJson(copiedDom)); TEST_ASSERT(actualJson == expectedJson); } if (diffList) { for (auto&& diff : *diffList.Obj()) { auto jsonLog = remoteprotocol::ConvertCustomTypeToJson(diff); writer.WriteLine(JsonToString(jsonLog, formatting)); } } dom = loggedFrame->renderingDom; domIndex = std::move(nextDomIndex); } }; }); GacUIUnitTest_WriteSnapshotFileIfChanged(snapshotFile, textLog); } void GacUIUnitTest_Start(const WString& appName, Nullable config) { UnitTestScreenConfig globalConfig; if (config) { globalConfig = config.Value(); } else { globalConfig.FastInitialize(1024, 768); } // Renderer UnitTestRemoteProtocol unitTestProtocol(appName, globalConfig); auto jsonParser = Ptr(new glr::json::Parser); // Data Processing in Renderer channeling::GuiRemoteJsonChannelFromProtocol channelReceiver(unitTestProtocol.GetProtocol()); channeling::GuiRemoteJsonChannelStringDeserializer channelJsonDeserializer(&channelReceiver, jsonParser); channeling::GuiRemoteUtfStringChannelDeserializer channelUtf8Deserializer(&channelJsonDeserializer); // Boundary between Binaries // Data Processing in Core channeling::GuiRemoteUtfStringChannelSerializer channelUtf8Serializer(&channelUtf8Deserializer); channeling::GuiRemoteJsonChannelStringSerializer channelJsonSerializer(&channelUtf8Serializer, jsonParser); // Boundary between threads channeling::GuiRemoteProtocolFromJsonChannel channelSender(&channelJsonSerializer); // Core repeatfiltering::GuiRemoteProtocolFilterVerifier verifierProtocol( globalConfig.useChannel == UnitTestRemoteChannel::None ? unitTestProtocol.GetProtocol() : &channelSender ); repeatfiltering::GuiRemoteProtocolFilter filteredProtocol(&verifierProtocol); GuiRemoteProtocolDomDiffConverter diffConverterProtocol(&filteredProtocol); UnitTestContextImpl unitTestContext(&unitTestProtocol); guiMainUnitTestContext = &unitTestContext; SetupRemoteNativeController( globalConfig.useDomDiff ? static_cast(&diffConverterProtocol) : &filteredProtocol ); GacUIUnitTest_SetGuiMainProxy({}); TEST_ASSERT(!ExceptionOccuredUnderUnitTestReleaseMode()); GacUIUnitTest_LogUI(appName, unitTestProtocol); if (!globalConfig.useDomDiff) { GacUIUnitTest_LogCommands(appName, unitTestProtocol); } GacUIUnitTest_LogDiffs(appName, unitTestProtocol); } template void RunInNewThread(T&& threadProc) { Thread::CreateAndStart([threadProc]() { try { threadProc(); } catch (const Exception& e) { (void)e; throw; } catch (const Error& e) { (void)e; throw; } }); } void GacUIUnitTest_StartAsync(const WString& appName, Nullable config) { TEST_ASSERT(config && config.Value().useChannel == UnitTestRemoteChannel::Async); // Renderer UnitTestRemoteProtocol unitTestProtocol(appName, config.Value()); auto jsonParser = Ptr(new glr::json::Parser); // Data Processing in Renderer channeling::GuiRemoteJsonChannelFromProtocol channelReceiver(unitTestProtocol.GetProtocol()); channeling::GuiRemoteJsonChannelStringDeserializer channelJsonDeserializer(&channelReceiver, jsonParser); channeling::GuiRemoteUtfStringChannelDeserializer channelUtf8Deserializer(&channelJsonDeserializer); // Boundary between Binaries // Data Processing in Core channeling::GuiRemoteUtfStringChannelSerializer channelUtf8Serializer(&channelUtf8Deserializer); channeling::GuiRemoteJsonChannelStringSerializer channelJsonSerializer(&channelUtf8Serializer, jsonParser); // Boundary between threads channeling::GuiRemoteProtocolAsyncJsonChannelSerializer asyncChannelSender; asyncChannelSender.Start( &channelJsonSerializer, [&unitTestProtocol, config](channeling::GuiRemoteProtocolAsyncJsonChannelSerializer* channel) { channeling::GuiRemoteProtocolFromJsonChannel channelSender(channel); // Core repeatfiltering::GuiRemoteProtocolFilterVerifier verifierProtocol(&channelSender); repeatfiltering::GuiRemoteProtocolFilter filteredProtocol(&verifierProtocol); GuiRemoteProtocolDomDiffConverter diffConverterProtocol(&filteredProtocol); UnitTestContextImpl unitTestContext(&unitTestProtocol); guiMainUnitTestContext = &unitTestContext; SetupRemoteNativeController( config.Value().useDomDiff ? static_cast(&diffConverterProtocol) : &filteredProtocol ); GacUIUnitTest_SetGuiMainProxy({}); }, []( channeling::GuiRemoteProtocolAsyncJsonChannelSerializer::TChannelThreadProc channelThreadProc, channeling::GuiRemoteProtocolAsyncJsonChannelSerializer::TUIThreadProc uiThreadProc ) { RunInNewThread(channelThreadProc); RunInNewThread(uiThreadProc); }); asyncChannelSender.WaitForStopped(); TEST_ASSERT(!ExceptionOccuredUnderUnitTestReleaseMode()); GacUIUnitTest_LogUI(appName, unitTestProtocol); if (!config.Value().useDomDiff) { GacUIUnitTest_LogCommands(appName, unitTestProtocol); } GacUIUnitTest_LogDiffs(appName, unitTestProtocol); } void GacUIUnitTest_Start_WithResourceAsText(const WString& appName, Nullable config, const WString& resourceText) { #define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_Start_WithResourceAsText(const WString&, Nullable, const WString&)#" GacUIUnitTest_LinkGuiMainProxy([=](UnitTestRemoteProtocol* protocol, IUnitTestContext* context, const UnitTestMainFunc& previousMainProxy) { auto resource = GacUIUnitTest_CompileAndLoad(resourceText); { auto workflow = resource->GetStringByPath(L"UnitTest/Workflow"); File snapshotFile = GacUIUnitTest_PrepareSnapshotFile( appName, #ifdef VCZH_64 WString::Unmanaged(L"[x64].txt") #else WString::Unmanaged(L"[x86].txt") #endif ); bool skipWriting = false; if (snapshotFile.Exists()) { auto previousLog = snapshotFile.ReadAllTextByBom(); if (previousLog == workflow) { skipWriting = true; } } if (!skipWriting) { bool succeeded = snapshotFile.WriteAllText(workflow, true, stream::BomEncoder::Utf8); CHECK_ERROR(succeeded, ERROR_MESSAGE_PREFIX L"Failed to write the snapshot file."); } } previousMainProxy(protocol, context); }); if (config && config.Value().useChannel == UnitTestRemoteChannel::Async) { GacUIUnitTest_StartAsync(appName, config); } else { GacUIUnitTest_Start(appName, config); } #undef ERROR_MESSAGE_PREFIX } void GacUIUnitTest_PrintErrors(GuiResourceError::List& errors) { for (auto&& error : errors) { TEST_PRINT(L"Error in resource: " + error.location.resourcePath); TEST_PRINT(L" ROW: " + itow(error.position.row) + L", COLUMN: " + itow(error.position.column)); TEST_PRINT(L" REASON: " + error.message); } } Ptr GacUIUnitTest_CompileAndLoad(const WString& xmlResource) { #define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_CompileAndLoad(const WString&)#" Ptr resource; GuiResourceError::List errors; { auto resourcePath = (GetUnitTestFrameworkConfig().resourceFolder / L"Resource.xml").GetFullPath(); auto parser = GetParserManager()->GetParser(L"XML"); auto xml = parser->Parse({ WString::Empty,resourcePath }, xmlResource, errors); if(!xml || errors.Count()> 0) { GacUIUnitTest_PrintErrors(errors); CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Failed to parse XML resource."); } resource = GuiResource::LoadFromXml(xml, resourcePath, GetFolderPath(resourcePath), errors); if (!resource || errors.Count() > 0) { GacUIUnitTest_PrintErrors(errors); CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Failed to load XML resource."); } } auto precompiledFolder = resource->Precompile( #ifdef VCZH_64 GuiResourceCpuArchitecture::x64, #else GuiResourceCpuArchitecture::x86, #endif nullptr, errors ); if (!precompiledFolder || errors.Count() > 0) { GacUIUnitTest_PrintErrors(errors); CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Failed to precompile XML resource."); } auto compiledWorkflow = precompiledFolder->GetValueByPath(WString::Unmanaged(L"Workflow/InstanceClass")).Cast(); CHECK_ERROR(compiledWorkflow, ERROR_MESSAGE_PREFIX L"Failed to compile generated Workflow script."); CHECK_ERROR(compiledWorkflow->assembly, ERROR_MESSAGE_PREFIX L"Failed to load Workflow assembly."); { WString text; auto& codes = compiledWorkflow->assembly->insAfterCodegen->moduleCodes; for (auto [code, codeIndex] : indexed(codes)) { text += L"================================(" + itow(codeIndex + 1) + L"/" + itow(codes.Count()) + L")================================\r\n"; text += code + L"\r\n"; } resource->CreateValueByPath( WString::Unmanaged(L"UnitTest/Workflow"), WString::Unmanaged(L"Text"), Ptr(new GuiTextData(text)) ); } GetResourceManager()->SetResource(resource, errors, GuiResourceUsage::InstanceClass); CHECK_ERROR(errors.Count() == 0, ERROR_MESSAGE_PREFIX L"Failed to load compiled XML resource."); return resource; #undef ERROR_MESSAGE_PREFIX } void GuiMain() { if (guiMainUnitTestContext) { guiMainProxy(guiMainUnitTestContext->GetProtocol(), guiMainUnitTestContext); } else { guiMainProxy(nullptr, nullptr); } guiMainUnitTestContext = nullptr; }