/*********************************************************************** THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY DEVELOPER: Zihan Chen(vczh) ***********************************************************************/ #include "GacUI.UnitTest.h" /*********************************************************************** .\GUIUNITTESTPROTOCOL_RENDERING _DOCUMENT.CPP ***********************************************************************/ namespace vl::presentation::unittest { using namespace collections; using namespace remoteprotocol; /*********************************************************************** Helper Functions for Document Paragraph ***********************************************************************/ 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) { state.characterLayouts.Clear(); state.lines.Clear(); state.cachedSize = Size(0, 16); 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 = 16; // Default: 12 (font) + 4 line.baseline = 12; line.width = 0; state.lines.Add(line); return; } // First pass: calculate per-character metrics struct TempCharInfo { double x; double width; vint height; 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 = { currentX, 0, 0, {} }; // 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.inlineObjectProp = inlinePair.Value().value; } else { if (fontSize <= 0) fontSize = 12; 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 (vint i = lineStart; i < lineEnd && i < tempChars.Count(); i++) { auto& info = tempChars[i]; 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 (vint i = lineStart; i < lineEnd && i < tempChars.Count(); i++) { double endX = tempChars[i].x + tempChars[i].width; if (endX > lineWidth) lineWidth = endX; } line.width = (vint)lineWidth; // Fill inline object bounds for (vint i = lineStart; i < lineEnd && i < tempChars.Count(); i++) { auto& info = tempChars[i]; 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 (vint i = 0; i < tempChars.Count(); i++) { while (lineIdx < state.lines.Count() - 1 && i >= state.lines[lineIdx].endPos) { lineIdx++; } DocumentParagraphCharLayout cl; cl.x = tempChars[i].x; cl.width = tempChars[i].width; cl.lineIndex = lineIdx; cl.height = tempChars[i].height; state.characterLayouts.Add(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 : 16); } /*********************************************************************** 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()) { elements::AddInlineObjectRun(state->inlineObjectRuns, range, *inlineProp); } } } // 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; if (state->cachedInlineObjectBounds.Count() > 0) { response.inlineObjectBounds = Ptr(new Dictionary); CopyFrom(*response.inlineObjectBounds.Obj(), state->cachedInlineObjectBounds); } GetEvents()->RespondRendererUpdateElement_DocumentParagraph(id, response); #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]; vint caret = arguments.caret; // Handle empty text if (state->text.Length() == 0 || state->lines.Count() == 0) { auto& line = state->lines[0]; GetEvents()->RespondDocumentParagraph_GetCaretBounds(id, Rect(Point(0, line.y), Size(0, line.height))); return; } // Clamp caret to valid range if (caret < 0) caret = 0; if (caret > state->text.Length()) caret = state->text.Length(); // Find which line the caret is on 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; } if (i == state->lines.Count() - 1) { lineIdx = i; } } auto& line = state->lines[lineIdx]; // Calculate x position vint x = 0; if (caret > 0 && caret <= state->characterLayouts.Count()) { // Caret is at the end of the previous character auto& prevChar = state->characterLayouts[caret - 1]; x = (vint)(prevChar.x + prevChar.width); } else if (caret < state->characterLayouts.Count()) { // Caret is at the start of this character x = (vint)state->characterLayouts[caret].x; } // 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; } Rect bounds(Point(x + alignmentOffset, line.y), Size(0, line.height)); GetEvents()->RespondDocumentParagraph_GetCaretBounds(id, bounds); #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; 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; } } } break; case CRP::CaretMoveLeft: response.newCaret = caret > 0 ? caret - 1 : 0; break; case CRP::CaretMoveRight: response.newCaret = caret < textLen ? caret + 1 : textLen; break; case CRP::CaretMoveUp: if (lineIdx > 0) { // Calculate x offset in current line vint xOffset = 0; if (caret > 0 && caret <= state->characterLayouts.Count()) { auto& prevChar = state->characterLayouts[caret - 1]; xOffset = (vint)(prevChar.x + prevChar.width); } // Find corresponding position in previous line auto& prevLine = state->lines[lineIdx - 1]; response.newCaret = prevLine.startPos; for (vint i = prevLine.startPos; i < prevLine.endPos && i < state->characterLayouts.Count(); i++) { auto& ch = state->characterLayouts[i]; if (ch.x + ch.width / 2 > xOffset) break; response.newCaret = i + 1; } } 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 && caret <= state->characterLayouts.Count()) { auto& prevChar = state->characterLayouts[caret - 1]; xOffset = (vint)(prevChar.x + prevChar.width); } // Find corresponding position in next line auto& nextLine = state->lines[lineIdx + 1]; response.newCaret = nextLine.startPos; for (vint i = nextLine.startPos; i < nextLine.endPos && i < state->characterLayouts.Count(); i++) { auto& ch = state->characterLayouts[i]; if (ch.x + ch.width / 2 > xOffset) break; response.newCaret = i + 1; } } else { response.newCaret = caret; } break; default: response.newCaret = caret; break; } GetEvents()->RespondDocumentParagraph_GetCaret(id, response); #undef ERROR_MESSAGE_PREFIX } void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetNearestCaretFromTextPos(vint id, const GetCaretBoundsRequest& 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.caret; vint textLen = state->text.Length(); // Clamp to valid range if (textPos < 0) textPos = 0; if (textPos > textLen) textPos = textLen; // For simple implementation, text position equals caret position GetEvents()->RespondDocumentParagraph_GetNearestCaretFromTextPos(id, textPos); #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 for (vint i = line.startPos; i < line.endPos && i < state->characterLayouts.Count(); i++) { auto& ch = state->characterLayouts[i]; 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 (i >= range.caretBegin && i < 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; // Check range: valid positions are 0 to text.Length() inclusive if (caret < 0 || caret > state->text.Length()) { GetEvents()->RespondDocumentParagraph_IsValidCaret(id, false); return; } // Check if position is inside an inline object (not at the beginning) // Inline objects occupy a range [caretBegin, caretEnd) // Position at caretBegin is valid (cursor can be placed before the object) // Positions inside (caretBegin < pos < caretEnd) are invalid for (auto&& [range, _] : state->inlineObjectRuns) { if (caret > range.caretBegin && caret < range.caretEnd) { GetEvents()->RespondDocumentParagraph_IsValidCaret(id, false); return; } } GetEvents()->RespondDocumentParagraph_IsValidCaret(id, true); #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_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); } 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_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 } 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")); JsonFormatting formatting; formatting.spaceAfterColon = true; formatting.spaceAfterComma = true; formatting.crlf = true; formatting.compact = true; auto jsonLog = remoteprotocol::ConvertCustomTypeToJson(unitTestProtocol.GetLoggedTrace()); auto textLog = JsonToString(jsonLog, formatting); { remoteprotocol::UnitTest_RenderingTrace deserialized; 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."); } GacUIUnitTest_WriteSnapshotFileIfChanged(snapshotFile, textLog); #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&)#" auto previousMainProxy = guiMainProxy; 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; }