diff --git a/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart b/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart
index b4d3514c8..1ba183c76 100644
--- a/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart
+++ b/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart
@@ -60,10 +60,10 @@ class HtmlContentViewer extends StatefulWidget {
}) : super(key: key);
@override
- State createState() => _HtmlContentViewState();
+ State createState() => HtmlContentViewState();
}
-class _HtmlContentViewState extends State {
+class HtmlContentViewState extends State {
late InAppWebViewController _webViewController;
late double _actualHeight;
@@ -79,6 +79,9 @@ class _HtmlContentViewState extends State {
verticalScrollBarEnabled: false,
);
+ @visibleForTesting
+ InAppWebViewController get webViewController => _webViewController;
+
@override
void initState() {
super.initState();
diff --git a/core/lib/utils/html/html_interaction.dart b/core/lib/utils/html/html_interaction.dart
index cc51c5b3f..63a6104e7 100644
--- a/core/lib/utils/html/html_interaction.dart
+++ b/core/lib/utils/html/html_interaction.dart
@@ -120,6 +120,7 @@ class HtmlInteraction {
}
function removeWidthHeightFromStyle(style) {
+ // Remove width and height properties from style string
style = style.replace(/width\\s*:\\s*[\\d.]+[a-zA-Z%]+\\s*;?/gi, '');
style = style.replace(/height\\s*:\\s*[\\d.]+[a-zA-Z%]+\\s*;?/gi, '');
style = style.trim();
@@ -130,6 +131,7 @@ class HtmlInteraction {
}
function extractWidthHeightFromStyle(style) {
+ // Extract width and height values with units from style string
const result = {};
const widthMatch = style.match(/width\\s*:\\s*([\\d.]+)([a-zA-Z%]+)\\s*;?/);
const heightMatch = style.match(/height\\s*:\\s*([\\d.]+)([a-zA-Z%]+)\\s*;?/);
@@ -154,9 +156,24 @@ class HtmlInteraction {
}
function normalizeStyleAttribute(attrs) {
+ // Normalize style attribute to ensure proper responsive behavior
let style = attrs['style'];
+ const widthStr = attrs['width'];
+
if (!style) {
- attrs['style'] = 'display:inline;max-width:100%;';
+ if (!widthStr) {
+ attrs['style'] = 'max-width: 100%;height: auto;display: inline;';
+ } else {
+ if (!style.includes('height')) {
+ style += 'height: auto;';
+ }
+
+ if (!style.includes('display')) {
+ style += 'display: inline;';
+ }
+
+ attrs['style'] = style;
+ }
return;
}
@@ -175,40 +192,59 @@ class HtmlInteraction {
}
}
+ // Ensure proper style string formatting
if (style.length && !style.endsWith(';')) {
style += ';';
}
+ // Add responsive defaults if missing
if (!style.includes('max-width')) {
- style += 'max-width:100%;height:auto;';
+ style += 'max-width: 100%;';
+ }
+
+ if (!style.includes('height')) {
+ style += 'height: auto;';
}
if (!style.includes('display')) {
- style += 'display:inline;';
+ style += 'display: inline;';
}
attrs['style'] = style;
}
function normalizeWidthHeightAttribute(attrs) {
+ // Normalize width/height attributes and remove if necessary
const widthStr = attrs['width'];
const heightStr = attrs['height'];
- if (!widthStr || displayWidth === undefined) return;
+ // Remove attribute if value is null or undefined
+ if (widthStr === null || widthStr === undefined) {
+ delete attrs['width'];
+ } else if (displayWidth !== undefined) {
+ const widthValue = parseFloat(widthStr);
+ if (!isNaN(widthValue)) {
+ if (widthValue > displayWidth) {
+ delete attrs['width'];
+ delete attrs['height'];
+ }
+ }
+ }
- const widthValue = parseFloat(widthStr);
- if (isNaN(widthValue) || widthValue <= displayWidth) return;
-
- delete attrs['width'];
- delete attrs['height'];
+ // Remove height attribute if value is null or undefined
+ if (heightStr === null || heightStr === undefined) {
+ delete attrs['height'];
+ }
}
function normalizeImageSize(attrs) {
- normalizeStyleAttribute(attrs);
+ // Apply both style and attribute normalization
normalizeWidthHeightAttribute(attrs);
+ normalizeStyleAttribute(attrs);
}
function applyImageNormalization() {
+ // Process all images on the page
document.querySelectorAll('img').forEach(img => {
const attrs = {
style: img.getAttribute('style'),
@@ -218,16 +254,31 @@ class HtmlInteraction {
normalizeImageSize(attrs);
- if (attrs.style != null) img.setAttribute('style', attrs.style);
- if ('width' in attrs) img.setAttribute('width', attrs.width);
- else img.removeAttribute('width');
+ // Handle style attribute
+ if (attrs.style !== null && attrs.style !== undefined) {
+ img.setAttribute('style', attrs.style);
+ } else {
+ img.removeAttribute('style');
+ }
- if ('height' in attrs) img.setAttribute('height', attrs.height);
- else img.removeAttribute('height');
+ // Handle width attribute
+ if ('width' in attrs && attrs.width !== null && attrs.width !== undefined) {
+ img.setAttribute('width', attrs.width);
+ } else {
+ img.removeAttribute('width');
+ }
+
+ // Handle height attribute
+ if ('height' in attrs && attrs.height !== null && attrs.height !== undefined) {
+ img.setAttribute('height', attrs.height);
+ } else {
+ img.removeAttribute('height');
+ }
});
}
function safeApplyImageNormalization() {
+ // Error-safe wrapper for the normalization function
try {
applyImageNormalization();
} catch (e) {
@@ -235,6 +286,7 @@ class HtmlInteraction {
}
}
+ // Run normalization when page loads
window.onload = safeApplyImageNormalization;
''';
diff --git a/core/lib/utils/platform_info.dart b/core/lib/utils/platform_info.dart
index f7f0edc19..7a5ff5c91 100644
--- a/core/lib/utils/platform_info.dart
+++ b/core/lib/utils/platform_info.dart
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
abstract class PlatformInfo {
@visibleForTesting
static bool isTestingForWeb = false;
+ static bool isIntegrationTesting = false;
static bool get isWeb => kIsWeb || isTestingForWeb;
static bool get isLinux => !isWeb && defaultTargetPlatform == TargetPlatform.linux;
diff --git a/integration_test/scenarios/email_detailed/deformed_inlined_image_scenario.dart b/integration_test/scenarios/email_detailed/deformed_inlined_image_scenario.dart
new file mode 100644
index 000000000..afd608aa8
--- /dev/null
+++ b/integration_test/scenarios/email_detailed/deformed_inlined_image_scenario.dart
@@ -0,0 +1,158 @@
+import 'package:core/presentation/views/html_viewer/html_content_viewer_widget.dart';
+import 'package:core/utils/app_logger.dart';
+import 'package:core/utils/platform_info.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart';
+import 'package:tmail_ui_user/features/email/presentation/email_view.dart';
+import 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_builder.dart';
+
+import '../../base/base_test_scenario.dart';
+import '../../models/provisioning_email.dart';
+import '../../robots/thread_robot.dart';
+
+class DeformedInlinedImageScenario extends BaseTestScenario {
+ const DeformedInlinedImageScenario(super.$);
+
+ static const String subject = 'Deformed inlined image';
+ static const String content = '''
+
+
+
+
+
+
+
+
+
+
+
+ ''';
+
+ @override
+ Future runTestLogic() async {
+ PlatformInfo.isIntegrationTesting = true;
+
+ const emailUser = String.fromEnvironment('BASIC_AUTH_EMAIL');
+ final provisioningEmail = ProvisioningEmail(
+ toEmail: emailUser,
+ subject: subject,
+ content: content,
+ );
+
+ await provisionEmail([provisioningEmail], requestReadReceipt: false);
+ await $.pumpAndSettle(duration: const Duration(seconds: 3));
+ await _expectDisplayedEmailWithSubject();
+
+ final threadRobot = ThreadRobot($);
+ await threadRobot.openEmailWithSubject(subject);
+ await $.pumpAndSettle(duration: const Duration(seconds: 3));
+ await _expectEmailViewDisplaysNormalizedInlineImages();
+
+ PlatformInfo.isIntegrationTesting = false;
+ }
+
+ Future _expectDisplayedEmailWithSubject() async {
+ await expectViewVisible(
+ $(EmailTileBuilder).which(
+ (widget) => widget.presentationEmail.subject == subject
+ ),
+ );
+ }
+
+ Future _expectEmailViewDisplaysNormalizedInlineImages() async {
+ HtmlContentViewer? htmlContentViewer;
+ await $(HtmlContentViewer)
+ .which((view) {
+ htmlContentViewer = view;
+ return true;
+ })
+ .first
+ .tap();
+ expect(htmlContentViewer, isNotNull);
+ log('DeformedInlinedImageScenario::_expectEmailViewDisplaysNormalizedInlineImages:initialWidth = ${htmlContentViewer?.initialWidth}');
+ expect(htmlContentViewer?.contentHtml.isNotEmpty, isTrue);
+
+ SingleEmailController? emailController;
+ await $(EmailView)
+ .which((view) {
+ emailController = view.controller;
+ return true;
+ })
+ .first
+ .tap();
+ expect(emailController, isNotNull);
+
+ final webViewController = emailController
+ ?.htmlContentViewKey
+ ?.currentState
+ ?.webViewController;
+ expect(webViewController, isNotNull);
+
+ final result = await webViewController?.evaluateJavascript(
+ source: '''
+ (function() {
+ const images = document.querySelectorAll('img');
+ return Array.from(images).map(img => ({
+ src: img.getAttribute('src'),
+ width: img.getAttribute('width'),
+ height: img.getAttribute('height'),
+ style: img.getAttribute('style'),
+ alt: img.getAttribute('alt'),
+ outerHTML: img.outerHTML
+ }));
+ })();
+ '''
+ );
+
+ final imageList = List