#include #include #include #include #include "../templates/simple_templates.h" #include "../templates/placeholder_templates.h" #include "../templates/nested_templates.h" #include "../utils/test_utils.h" // Test RAM data getters static String testTitle = "Test Title"; static String testContent = "Test Content"; static String testFooter = "Test Footer"; static String testTheme = "dark"; static String testRssi = "-41"; static String testHeap = "51200"; static String testUptime = "01:23:45"; static const char* getTestTitle() { return testTitle.c_str(); } static const char* getTestContent() { return testContent.c_str(); } static const char* getTestFooter() { return testFooter.c_str(); } static const char* getTestTheme() { return testTheme.c_str(); } static const char* getTestRssi() { return testRssi.c_str(); } static const char* getTestHeap() { return testHeap.c_str(); } static const char* getTestUptime() { return testUptime.c_str(); } static const char PROGMEM simulated_logo_base64[] = "VEVTVF9MT0dPX0RBVEE="; static const char ram_root_template[] = "Status: %TITLE%"; static const char ram_root_text_only[] = "RAM root template"; struct DynamicPlaceholderState { String value; }; static DynamicPlaceholderState dynamicGreetingState; static const char* dynamicGreetingGetter(void* userData) { DynamicPlaceholderState* state = static_cast(userData); static String buffer; buffer = state->value; return buffer.c_str(); } static size_t dynamicGreetingLength(const char* data, void* userData) { (void)userData; return strlen(data); } static DynamicTemplateDescriptor dynamicGreetingDescriptor = { dynamicGreetingGetter, dynamicGreetingLength, &dynamicGreetingState }; struct ConditionalTestState { ConditionalBranchResult result; }; static ConditionalTestState conditionalState; static ConditionalBranchResult evaluateConditionalState(void* userData) { ConditionalTestState* state = static_cast(userData); return state->result; } static ConditionalDescriptor conditionalDescriptor = { evaluateConditionalState, "%COND_TRUE%", "%COND_FALSE%", &conditionalState }; static ConditionalDescriptor skipConditionalDescriptor = { evaluateConditionalState, "%COND_TRUE%", "%COND_FALSE%", &conditionalState }; static ConditionalDescriptor iteratorConditionalDescriptor = { evaluateConditionalState, "%WIFI_LIST%", nullptr, &conditionalState }; struct WifiIteratorState { size_t index; size_t count; bool closeCalled; bool useDynamicTemplate; bool includeOverrides; bool produceError; size_t errorAfter; }; static WifiIteratorState wifiIteratorState; static void resetWifiIteratorState(size_t count) { wifiIteratorState.index = 0; wifiIteratorState.count = count; wifiIteratorState.closeCalled = false; wifiIteratorState.useDynamicTemplate = false; wifiIteratorState.includeOverrides = false; wifiIteratorState.produceError = false; wifiIteratorState.errorAfter = 0; } static void* wifiIteratorOpen(void* userData) { WifiIteratorState* state = static_cast(userData); state->index = 0; state->closeCalled = false; return state; } static IteratorStepResult wifiIteratorNext(void* handle, IteratorItemView& view); static void wifiIteratorClose(void* handle); static IteratorDescriptor wifiIteratorDescriptor = { wifiIteratorOpen, wifiIteratorNext, wifiIteratorClose, &wifiIteratorState }; static const char PROGMEM wifiItemTemplate[] = "
  • %SSID% (%RSSI%)
  • "; static const char PROGMEM wifiTrueBadgeTemplate[] = "[WiFi ENABLED]"; static const char PROGMEM wifiFalseBadgeTemplate[] = "[WiFi DISABLED]"; static const char* wifiDynamicTemplates[] = { "
  • Net-A [-41dBm]
  • ", "
  • Net-B [-55dBm]
  • ", "
  • Net-C [-70dBm]
  • " }; static const char PROGMEM wifiOverrideSsid0[] = "Net-A"; static const char PROGMEM wifiOverrideSsid1[] = "Net-B"; static const char PROGMEM wifiOverrideSsid2[] = "Net-C"; static const char PROGMEM wifiOverrideRssi0[] = "-41"; static const char PROGMEM wifiOverrideRssi1[] = "-55"; static const char PROGMEM wifiOverrideRssi2[] = "-70"; static PlaceholderEntry wifiOverrideEntries[3][2]; static bool wifiOverridesInitialized = false; static void initializeWifiOverrides() { if (wifiOverridesInitialized) { return; } const char* placeholderNames[2] = {"%SSID%", "%RSSI%"}; const char* const progmemData[3][2] = { {wifiOverrideSsid0, wifiOverrideRssi0}, {wifiOverrideSsid1, wifiOverrideRssi1}, {wifiOverrideSsid2, wifiOverrideRssi2} }; for (size_t item = 0; item < 3; ++item) { for (size_t field = 0; field < 2; ++field) { PlaceholderEntry& entry = wifiOverrideEntries[item][field]; memset(entry.name, 0, sizeof(entry.name)); strncpy(entry.name, placeholderNames[field], sizeof(entry.name) - 1); entry.type = PlaceholderType::PROGMEM_DATA; entry.data = progmemData[item][field]; entry.getLength = DeviceFrameworkPlaceholderRegistry::getProgmemLength; } } wifiOverridesInitialized = true; } static IteratorStepResult wifiIteratorNext(void* handle, IteratorItemView& view) { WifiIteratorState* state = static_cast(handle); if (!state || state->index >= state->count) { return IteratorStepResult::COMPLETE; } if (state->produceError && state->index >= state->errorAfter) { return IteratorStepResult::ERROR; } size_t idx = state->index++; if (state->useDynamicTemplate) { static String dynamicBuffer; dynamicBuffer = wifiDynamicTemplates[idx % 3]; view.templateData = dynamicBuffer.c_str(); view.templateLength = dynamicBuffer.length(); view.templateIsProgmem = false; view.placeholders = nullptr; view.placeholderCount = 0; } else { view.templateData = wifiItemTemplate; view.templateLength = strlen_P(wifiItemTemplate); view.templateIsProgmem = true; if (state->includeOverrides) { initializeWifiOverrides(); view.placeholders = wifiOverrideEntries[idx % 3]; view.placeholderCount = 2; } else { view.placeholders = nullptr; view.placeholderCount = 0; } } return IteratorStepResult::ITEM_READY; } static void wifiIteratorClose(void* handle) { WifiIteratorState* state = static_cast(handle); if (state) { state->closeCalled = true; } } static const char PROGMEM dynamicWrapperTemplate[] = "Greeting: %DYNAMIC_GREETING%"; static const char PROGMEM dynamicMixedTemplate[] = "Start-%DYNAMIC_GREETING%-End"; static const char PROGMEM conditionalWrapperTemplate[] = "Status:%STATUS_BLOCK%:Done"; static const char PROGMEM conditionalSkipTemplate[] = "Value[%STATUS_BLOCK%]"; static const char PROGMEM iteratorWrapperTemplate[] = "
      %WIFI_LIST%
    "; static const char PROGMEM iteratorConditionalWrapperTemplate[] = "Badge:
      %WIFI_BADGE%
    "; // Test TemplateRenderer basic rendering void test_template_renderer_basic() { Serial.println("[TEST:test_template_renderer_basic] Testing TemplateRenderer basic rendering..."); PlaceholderRegistry registry(10); TemplateContext ctx; ctx.setRegistry(®istry); // Test simple template (no placeholders) TemplateRenderer::initializeContext(ctx, plain_text_template); String result = captureRenderedOutput(ctx); TEST_ASSERT_EQUAL_STRING_MESSAGE("Hello, World!", result.c_str(), "Should render plain text template"); TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), "Should be complete after rendering"); // Test empty template ctx.reset(); TemplateRenderer::initializeContext(ctx, empty_template); String result2 = captureRenderedOutput(ctx); TEST_ASSERT_EQUAL_STRING_MESSAGE("", result2.c_str(), "Should render empty template"); TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), "Should be complete after rendering empty template"); // Test template with single PROGMEM_DATA placeholder // Template uses %STYLES%, so register that registry.registerProgmemData("%STYLES%", test_css_data); ctx.reset(); ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, progmem_data_template); String result3 = captureRenderedOutput(ctx); // Expected: "CSS: " + test_css_data content TEST_ASSERT_TRUE_MESSAGE(result3.indexOf("CSS:") >= 0, "Should contain CSS prefix"); // Verify we got more than just "CSS: " (should have actual CSS data) size_t cssPrefixLen = strlen("CSS: "); TEST_ASSERT_GREATER_THAN_MESSAGE(cssPrefixLen, result3.length(), "Should contain CSS data"); // Test template with single RAM_DATA placeholder // Template uses %PAGE_TITLE%, so register that registry.registerRamData("%PAGE_TITLE%", getTestTitle); ctx.reset(); ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, ram_data_template); String result4 = captureRenderedOutput(ctx); String expected4 = String("Title: ") + testTitle; TEST_ASSERT_EQUAL_STRING_MESSAGE(expected4.c_str(), result4.c_str(), "Should render template with RAM_DATA placeholder"); // Test template with multiple placeholders registry.registerRamData("%TITLE%", getTestTitle); registry.registerRamData("%CONTENT%", getTestContent); registry.registerRamData("%FOOTER%", getTestFooter); ctx.reset(); ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, multiple_placeholders_template); String result5 = captureRenderedOutput(ctx); String expected5 = String("Title: ") + testTitle + "\nContent: " + testContent + "\nFooter: " + testFooter; TEST_ASSERT_EQUAL_STRING_MESSAGE(expected5.c_str(), result5.c_str(), "Should render template with multiple placeholders"); Serial.println("[TEST:test_template_renderer_basic] TemplateRenderer basic rendering tests completed successfully"); } // Test TemplateRenderer chunked rendering void test_template_renderer_chunked() { Serial.println("[TEST:test_template_renderer_chunked] Testing TemplateRenderer chunked rendering..."); PlaceholderRegistry registry(10); registry.registerProgmemData("%STYLES%", test_css_data); TemplateContext ctx; ctx.setRegistry(®istry); // Test rendering with small buffer (1 byte chunks) TemplateRenderer::initializeContext(ctx, progmem_data_template); String result1 = captureRenderedOutput(ctx, 1); TEST_ASSERT_TRUE_MESSAGE(result1.indexOf("CSS:") >= 0, "Should render correctly with 1 byte buffer"); // Test rendering with medium buffer (64 bytes) ctx.reset(); ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, progmem_data_template); String result2 = captureRenderedOutput(ctx, 64); TEST_ASSERT_TRUE_MESSAGE(result2.indexOf("CSS:") >= 0, "Should render correctly with 64 byte buffer"); // Test rendering with large buffer (512 bytes) ctx.reset(); ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, progmem_data_template); String result3 = captureRenderedOutput(ctx, 512); TEST_ASSERT_TRUE_MESSAGE(result3.indexOf("CSS:") >= 0, "Should render correctly with 512 byte buffer"); // Test rendering with buffer larger than template ctx.reset(); ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, plain_text_template); String result4 = captureRenderedOutput(ctx, 1024); TEST_ASSERT_EQUAL_STRING_MESSAGE("Hello, World!", result4.c_str(), "Should render correctly with buffer larger than template"); // Test that all chunks combined equal full template // Register placeholder for long_text_template registry.registerProgmemData("%PLACEHOLDER%", test_css_data); Serial.print("[DEBUG:test_template_renderer_chunked] Registered placeholders: "); Serial.println(registry.getCount()); const PlaceholderEntry* debugEntry = registry.getPlaceholder("%PLACEHOLDER%"); Serial.print("[DEBUG:test_template_renderer_chunked] Placeholder %PLACEHOLDER% found: "); Serial.println(debugEntry != nullptr ? "YES" : "NO"); if (debugEntry) { Serial.print("[DEBUG:test_template_renderer_chunked] Placeholder type: "); Serial.println((int)debugEntry->type); } ctx.reset(); ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, long_text_template); RenderingContext* currentCtx = ctx.getCurrentContext(); if (currentCtx) { Serial.print("[DEBUG:test_template_renderer_chunked] Template length: "); Serial.println(currentCtx->context.templateCtx.templateLen); } Serial.print("[DEBUG:test_template_renderer_chunked] Initial state: "); Serial.println(ctx.getStateString()); String fullResult = captureRenderedOutput(ctx, 512); Serial.print("[DEBUG:test_template_renderer_chunked] Full result length: "); Serial.println(fullResult.length()); Serial.print("[DEBUG:test_template_renderer_chunked] Full result: ["); Serial.print(fullResult); Serial.println("]"); Serial.print("[DEBUG:test_template_renderer_chunked] Full result final state: "); Serial.println(ctx.getStateString()); currentCtx = ctx.getCurrentContext(); if (currentCtx && currentCtx->type == RenderingContextType::TEMPLATE) { Serial.print("[DEBUG:test_template_renderer_chunked] Full result template pos: "); Serial.println(currentCtx->context.templateCtx.position); } ctx.reset(); ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, long_text_template); Serial.print("[DEBUG:test_template_renderer_chunked] Chunked initial state: "); Serial.println(ctx.getStateString()); String chunkedResult = captureRenderedOutput(ctx, 10); Serial.print("[DEBUG:test_template_renderer_chunked] Chunked result length: "); Serial.println(chunkedResult.length()); Serial.print("[DEBUG:test_template_renderer_chunked] Chunked result: ["); Serial.print(chunkedResult); Serial.println("]"); Serial.print("[DEBUG:test_template_renderer_chunked] Chunked result final state: "); Serial.println(ctx.getStateString()); currentCtx = ctx.getCurrentContext(); if (currentCtx && currentCtx->type == RenderingContextType::TEMPLATE) { Serial.print("[DEBUG:test_template_renderer_chunked] Chunked result template pos: "); Serial.println(currentCtx->context.templateCtx.position); } TEST_ASSERT_EQUAL_STRING_MESSAGE(fullResult.c_str(), chunkedResult.c_str(), "Chunked rendering should match full rendering"); Serial.println("[TEST:test_template_renderer_chunked] TemplateRenderer chunked rendering tests completed successfully"); } // Test TemplateRenderer state transitions void test_template_renderer_state_transitions() { Serial.println("[TEST:test_template_renderer_state_transitions] Testing TemplateRenderer state transitions..."); PlaceholderRegistry registry(10); registry.registerProgmemData("%CSS%", test_css_data); TemplateContext ctx; ctx.setRegistry(®istry); // Test TEXT -> BUILDING_PLACEHOLDER -> RENDERING_CONTEXT -> TEXT TemplateRenderer::initializeContext(ctx, progmem_data_template); TEST_ASSERT_EQUAL_MESSAGE(TemplateRenderState::TEXT, ctx.state, "Initial state should be TEXT"); // Render a small chunk to trigger state transition uint8_t buffer[10]; size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, sizeof(buffer)); TEST_ASSERT_GREATER_THAN_MESSAGE(0, written, "Should write some data"); // Continue rendering until complete while (!TemplateRenderer::isComplete(ctx)) { TemplateRenderer::renderNextChunk(ctx, buffer, sizeof(buffer)); } TEST_ASSERT_EQUAL_MESSAGE(TemplateRenderState::COMPLETE, ctx.state, "Final state should be COMPLETE"); Serial.println("[TEST:test_template_renderer_state_transitions] TemplateRenderer state transition tests completed successfully"); } // Test TemplateRenderer placeholder processing void test_template_renderer_placeholders() { Serial.println("[TEST:test_template_renderer_placeholders] Testing TemplateRenderer placeholder processing..."); PlaceholderRegistry registry(10); registry.registerProgmemData("%CSS%", test_css_data); registry.registerRamData("%TITLE%", getTestTitle); TemplateContext ctx; ctx.setRegistry(®istry); // Test placeholder at start // Register placeholder first registry.registerProgmemData("%PLACEHOLDER%", test_css_data); Serial.print("[DEBUG:test_template_renderer_placeholders] Registered %PLACEHOLDER%: "); Serial.println(registry.getPlaceholder("%PLACEHOLDER%") != nullptr ? "YES" : "NO"); ctx.reset(); ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, placeholder_start_template); Serial.print("[DEBUG:test_template_renderer_placeholders] Template: ["); Serial.print(placeholder_start_template); Serial.println("]"); Serial.print("[DEBUG:test_template_renderer_placeholders] Initial state: "); Serial.println(ctx.getStateString()); RenderingContext* currentCtx = ctx.getCurrentContext(); if (currentCtx && currentCtx->type == RenderingContextType::TEMPLATE) { Serial.print("[DEBUG:test_template_renderer_placeholders] Template length: "); Serial.println(currentCtx->context.templateCtx.templateLen); Serial.print("[DEBUG:test_template_renderer_placeholders] Template pos: "); Serial.println(currentCtx->context.templateCtx.position); } Serial.print("[DEBUG:test_template_renderer_placeholders] Buffer pos: "); Serial.println(ctx.bufferPos); Serial.print("[DEBUG:test_template_renderer_placeholders] Buffer len: "); Serial.println(ctx.bufferLen); String result1 = captureRenderedOutput(ctx); Serial.print("[DEBUG:test_template_renderer_placeholders] Final state: "); Serial.println(ctx.getStateString()); currentCtx = ctx.getCurrentContext(); if (currentCtx && currentCtx->type == RenderingContextType::TEMPLATE) { Serial.print("[DEBUG:test_template_renderer_placeholders] Final template pos: "); Serial.println(currentCtx->context.templateCtx.position); } Serial.print("[DEBUG:test_template_renderer_placeholders] Result length: "); Serial.println(result1.length()); Serial.print("[DEBUG:test_template_renderer_placeholders] Result: ["); Serial.print(result1); Serial.println("]"); Serial.print("[DEBUG:test_template_renderer_placeholders] Contains ' text': "); Serial.println(result1.indexOf(" text") >= 0 ? "YES" : "NO"); Serial.print("[DEBUG:test_template_renderer_placeholders] Contains 'body': "); Serial.println(result1.indexOf("body") >= 0 ? "YES" : "NO"); // Should render placeholder and text // PROGMEM data is "body { color: red; }", so check for that or " text" TEST_ASSERT_TRUE_MESSAGE(result1.indexOf(" text") >= 0 || result1.indexOf("body") >= 0, "Should handle placeholder at start"); // Test placeholder at end ctx.reset(); ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, placeholder_end_template); String result2 = captureRenderedOutput(ctx); // Placeholder is registered, so it should be rendered // Template: "text %PLACEHOLDER%" should render as "text body { color: red; }" TEST_ASSERT_TRUE_MESSAGE(result2.indexOf("text ") >= 0, "Should contain text before placeholder"); TEST_ASSERT_TRUE_MESSAGE(result2.indexOf("body") >= 0, "Should render placeholder at end"); // Test consecutive placeholders registry.registerProgmemData("%A%", test_css_data); registry.registerRamData("%B%", getTestTitle); registry.registerRamData("%C%", getTestContent); ctx.reset(); ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, consecutive_placeholders_template); String result3 = captureRenderedOutput(ctx); // Should render all placeholders TEST_ASSERT_TRUE_MESSAGE(result3.indexOf(testTitle) >= 0, "Should contain title"); TEST_ASSERT_TRUE_MESSAGE(result3.indexOf(testContent) >= 0, "Should contain content"); TEST_ASSERT_TRUE_MESSAGE(result3.length() > testTitle.length() + testContent.length(), "Should contain all placeholder data"); // Test unknown placeholder (should be skipped) ctx.reset(); ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, single_placeholder_template); String result4 = captureRenderedOutput(ctx); TEST_ASSERT_EQUAL_STRING_MESSAGE("Hello, !", result4.c_str(), "Should skip unknown placeholder"); Serial.println("[TEST:test_template_renderer_placeholders] TemplateRenderer placeholder processing tests completed successfully"); } // Test TemplateRenderer nested templates void test_template_renderer_nested() { Serial.println("[TEST:test_template_renderer_nested] Testing TemplateRenderer nested templates..."); PlaceholderRegistry registry(10); // Test one level nesting registry.registerProgmemTemplate("%INNER%", nested_one_level_inner); TemplateContext ctx; ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, nested_one_level_outer); String result1 = captureRenderedOutput(ctx); // Expected: "Outer: Inner content" TEST_ASSERT_TRUE_MESSAGE(result1.indexOf("Outer:") >= 0, "Should contain outer text"); TEST_ASSERT_TRUE_MESSAGE(result1.indexOf("Inner content") >= 0, "Should contain inner template content"); // Test two levels nesting registry.registerProgmemTemplate("%LEVEL2%", nested_two_levels_mid); registry.registerProgmemTemplate("%LEVEL3%", nested_two_levels_inner); ctx.reset(); ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, nested_two_levels_outer); String result2 = captureRenderedOutput(ctx); // Expected: "Level1: Level2: Level3 content" TEST_ASSERT_TRUE_MESSAGE(result2.indexOf("Level1:") >= 0, "Should contain level1 text"); TEST_ASSERT_TRUE_MESSAGE(result2.indexOf("Level2:") >= 0, "Should contain level2 text"); TEST_ASSERT_TRUE_MESSAGE(result2.indexOf("Level3 content") >= 0, "Should contain level3 content"); // Test three levels nesting registry.registerProgmemTemplate("%MID%", nested_three_levels_mid); registry.registerProgmemTemplate("%INNER%", nested_three_levels_inner); registry.registerProgmemTemplate("%DEEP%", nested_three_levels_deep); ctx.reset(); ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, nested_three_levels_outer); String result3 = captureRenderedOutput(ctx); // Expected: "Outer: Mid: Inner: Deep content" Serial.println("\n[TEST:test_template_renderer_nested] ========== THREE-LEVEL NESTED TEST =========="); Serial.print("[TEST:test_template_renderer_nested] Result: ["); Serial.print(result3); Serial.println("]"); Serial.print("[TEST:test_template_renderer_nested] Length: "); Serial.println(result3.length()); Serial.print("[TEST:test_template_renderer_nested] Contains 'Outer:': "); Serial.println(result3.indexOf("Outer:") >= 0 ? "YES" : "NO"); Serial.print("[TEST:test_template_renderer_nested] Contains 'Mid:': "); Serial.println(result3.indexOf("Mid:") >= 0 ? "YES" : "NO"); Serial.print("[TEST:test_template_renderer_nested] Contains 'Inner:': "); Serial.println(result3.indexOf("Inner:") >= 0 ? "YES" : "NO"); Serial.print("[TEST:test_template_renderer_nested] Contains 'Deep content': "); Serial.println(result3.indexOf("Deep content") >= 0 ? "YES" : "NO"); Serial.println("[TEST:test_template_renderer_nested] ============================================\n"); TEST_ASSERT_TRUE_MESSAGE(result3.indexOf("Outer:") >= 0, "Should contain outer text"); TEST_ASSERT_TRUE_MESSAGE(result3.indexOf("Mid:") >= 0, "Should contain mid text"); TEST_ASSERT_TRUE_MESSAGE(result3.indexOf("Inner:") >= 0, "Should contain inner text"); TEST_ASSERT_TRUE_MESSAGE(result3.indexOf("Deep content") >= 0, "Should contain deep content"); // Test nested template with placeholders // nested_with_placeholder_outer uses %NESTED% // nested_with_placeholder_inner uses %PLACEHOLDER% registry.registerRamData("%PLACEHOLDER%", getTestTitle); registry.registerProgmemTemplate("%NESTED%", nested_with_placeholder_inner); Serial.print("[DEBUG:test_template_renderer_nested] Registered %PLACEHOLDER%: "); Serial.println(registry.getPlaceholder("%PLACEHOLDER%") != nullptr ? "YES" : "NO"); Serial.print("[DEBUG:test_template_renderer_nested] Registered %NESTED%: "); Serial.println(registry.getPlaceholder("%NESTED%") != nullptr ? "YES" : "NO"); ctx.reset(); ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, nested_with_placeholder_outer); String result4 = captureRenderedOutput(ctx); Serial.print("[DEBUG:test_template_renderer_nested] Nested result length: "); Serial.println(result4.length()); Serial.print("[DEBUG:test_template_renderer_nested] Nested result: ["); Serial.print(result4); Serial.println("]"); Serial.print("[DEBUG:test_template_renderer_nested] Contains 'Outer:': "); Serial.println(result4.indexOf("Outer:") >= 0 ? "YES" : "NO"); Serial.print("[DEBUG:test_template_renderer_nested] Contains 'Inner:': "); Serial.println(result4.indexOf("Inner:") >= 0 ? "YES" : "NO"); Serial.print("[DEBUG:test_template_renderer_nested] Contains testTitle '"); Serial.print(testTitle); Serial.print("': "); Serial.println(result4.indexOf(testTitle) >= 0 ? "YES" : "NO"); // Expected: "Outer: Inner: " + testTitle TEST_ASSERT_TRUE_MESSAGE(result4.indexOf("Outer:") >= 0, "Should contain outer text"); TEST_ASSERT_TRUE_MESSAGE(result4.indexOf("Inner:") >= 0, "Should contain inner text"); TEST_ASSERT_TRUE_MESSAGE(result4.indexOf(testTitle) >= 0, "Should contain placeholder value"); // Test deep nesting (up to MAX_RENDERING_DEPTH) registry.registerProgmemTemplate("%L2%", deep_nest_level2); registry.registerProgmemTemplate("%L3%", deep_nest_level3); registry.registerProgmemTemplate("%L4%", deep_nest_level4); registry.registerProgmemTemplate("%L5%", deep_nest_level5); registry.registerProgmemTemplate("%L6%", deep_nest_level6); registry.registerProgmemTemplate("%L7%", deep_nest_level7); registry.registerProgmemTemplate("%L8%", deep_nest_level8); registry.registerProgmemTemplate("%L9%", deep_nest_level9); registry.registerProgmemTemplate("%L10%", deep_nest_level10); registry.registerProgmemTemplate("%L11%", deep_nest_level11); registry.registerProgmemTemplate("%L12%", deep_nest_level12); registry.registerProgmemTemplate("%L13%", deep_nest_level13); registry.registerProgmemTemplate("%L14%", deep_nest_level14); registry.registerProgmemTemplate("%L15%", deep_nest_level15); registry.registerProgmemTemplate("%L16%", deep_nest_level16); ctx.reset(); ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, deep_nest_level1); String result5 = captureRenderedOutput(ctx); TEST_ASSERT_TRUE_MESSAGE(result5.length() > 0, "Should render deep nested template"); TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), "Should complete deep nested template"); Serial.println("[TEST:test_template_renderer_nested] TemplateRenderer nested template tests completed successfully"); } void test_template_renderer_nested_four_levels() { Serial.println("[TEST:test_template_renderer_nested_four_levels] Ensuring four-level nesting with chunked output..."); PlaceholderRegistry registry(8); registry.registerProgmemTemplate("%L2%", nested_four_levels_level2); registry.registerProgmemTemplate("%L3%", nested_four_levels_level3); registry.registerProgmemTemplate("%L4%", nested_four_levels_level4); TemplateContext ctx; ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, nested_four_levels_outer); const size_t chunkSize = 4; uint8_t buffer[chunkSize]; String rendered; size_t iterations = 0; while (!TemplateRenderer::isComplete(ctx) && !ctx.hasError()) { size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, chunkSize); iterations++; TEST_ASSERT_FALSE_MESSAGE(ctx.hasError(), "Four-level nested rendering should not error"); TEST_ASSERT_TRUE_MESSAGE(written > 0 || TemplateRenderer::isComplete(ctx), "Renderer stalled before completion"); for (size_t i = 0; i < written; ++i) { rendered += static_cast(buffer[i]); } } TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), "Four-level nested render should complete"); TEST_ASSERT_GREATER_THAN_MESSAGE((unsigned int)1, iterations, "Four-level nested render should require multiple chunks"); const String expected = "L1: L2: L3: L4 content"; TEST_ASSERT_EQUAL_STRING_MESSAGE(expected.c_str(), rendered.c_str(), "Four-level nested render should match expected output"); } void test_template_renderer_nested_chunk_progress() { Serial.println("[TEST:test_template_renderer_nested_chunk_progress] Verifying nested templates with tiny chunks..."); PlaceholderRegistry registry(10); registry.registerProgmemTemplate("%MID%", nested_three_levels_mid); registry.registerProgmemTemplate("%INNER%", nested_three_levels_inner); registry.registerProgmemTemplate("%DEEP%", nested_three_levels_deep); TemplateContext ctx; ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, nested_three_levels_outer); const size_t chunkSize = 3; uint8_t buffer[chunkSize]; String rendered; while (!TemplateRenderer::isComplete(ctx) && !ctx.hasError()) { size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, chunkSize); TEST_ASSERT_FALSE_MESSAGE(ctx.hasError(), "Context should not enter error state"); TEST_ASSERT_MESSAGE(written > 0 || ctx.hasError() || TemplateRenderer::isComplete(ctx), "Renderer stalled before completion"); for (size_t i = 0; i < written; ++i) { rendered += static_cast(buffer[i]); } } TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), "Nested render should complete"); TEST_ASSERT_TRUE_MESSAGE(rendered.indexOf("Outer:") >= 0, "Rendered string should include outer text"); TEST_ASSERT_TRUE_MESSAGE(rendered.indexOf("Deep content") >= 0, "Rendered string should include deepest content"); } void test_template_renderer_large_templates() { Serial.println("[TEST:test_template_renderer_large_templates] Rendering large web-style templates with small chunks..."); PlaceholderRegistry registry(20); registry.registerProgmemTemplate("%HEADER%", simulated_web_header); registry.registerProgmemTemplate("%CONTENT%", simulated_web_content); registry.registerProgmemTemplate("%FOOTER%", simulated_web_footer); registry.registerProgmemTemplate("%NAV%", simulated_web_nav); registry.registerProgmemTemplate("%SIDEBAR%", simulated_web_sidebar); registry.registerRamData("%TITLE%", getTestTitle); registry.registerRamData("%THEME%", getTestTheme); registry.registerRamData("%RSSI%", getTestRssi); registry.registerRamData("%HEAP%", getTestHeap); registry.registerRamData("%UPTIME%", getTestUptime); registry.registerProgmemData("%STYLES%", test_css_data); registry.registerProgmemData("%SCRIPTS%", simulated_web_scripts); registry.registerProgmemData("%LOGO_BASE64%", simulated_logo_base64); TemplateContext ctx; ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, simulated_web_outer); const size_t chunkSize = 32; uint8_t buffer[chunkSize]; String rendered; size_t iterations = 0; while (!TemplateRenderer::isComplete(ctx) && !ctx.hasError()) { size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, chunkSize); iterations++; TEST_ASSERT_FALSE_MESSAGE(ctx.hasError(), "Large template render should not error"); TEST_ASSERT_TRUE_MESSAGE(written > 0 || TemplateRenderer::isComplete(ctx), "Renderer stalled while processing large template"); for (size_t i = 0; i < written; ++i) { rendered += static_cast(buffer[i]); } } TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), "Large template render should complete"); TEST_ASSERT_GREATER_THAN_MESSAGE((unsigned int)1, iterations, "Large template should require multiple render iterations"); TEST_ASSERT_TRUE_MESSAGE(rendered.length() > chunkSize, "Rendered output should be larger than a single chunk"); TEST_ASSERT_TRUE_MESSAGE(rendered.indexOf("
    ") >= 0, "Rendered output should include header section"); TEST_ASSERT_TRUE_MESSAGE(rendered.indexOf("