TF-3622 Add integration test for case deformed inlined image
Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
@@ -60,10 +60,10 @@ class HtmlContentViewer extends StatefulWidget {
|
|||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _HtmlContentViewState();
|
State<StatefulWidget> createState() => HtmlContentViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HtmlContentViewState extends State<HtmlContentViewer> {
|
class HtmlContentViewState extends State<HtmlContentViewer> {
|
||||||
|
|
||||||
late InAppWebViewController _webViewController;
|
late InAppWebViewController _webViewController;
|
||||||
late double _actualHeight;
|
late double _actualHeight;
|
||||||
@@ -79,6 +79,9 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
|
|||||||
verticalScrollBarEnabled: false,
|
verticalScrollBarEnabled: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
InAppWebViewController get webViewController => _webViewController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ class HtmlInteraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeWidthHeightFromStyle(style) {
|
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(/width\\s*:\\s*[\\d.]+[a-zA-Z%]+\\s*;?/gi, '');
|
||||||
style = style.replace(/height\\s*:\\s*[\\d.]+[a-zA-Z%]+\\s*;?/gi, '');
|
style = style.replace(/height\\s*:\\s*[\\d.]+[a-zA-Z%]+\\s*;?/gi, '');
|
||||||
style = style.trim();
|
style = style.trim();
|
||||||
@@ -130,6 +131,7 @@ class HtmlInteraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function extractWidthHeightFromStyle(style) {
|
function extractWidthHeightFromStyle(style) {
|
||||||
|
// Extract width and height values with units from style string
|
||||||
const result = {};
|
const result = {};
|
||||||
const widthMatch = style.match(/width\\s*:\\s*([\\d.]+)([a-zA-Z%]+)\\s*;?/);
|
const widthMatch = style.match(/width\\s*:\\s*([\\d.]+)([a-zA-Z%]+)\\s*;?/);
|
||||||
const heightMatch = style.match(/height\\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) {
|
function normalizeStyleAttribute(attrs) {
|
||||||
|
// Normalize style attribute to ensure proper responsive behavior
|
||||||
let style = attrs['style'];
|
let style = attrs['style'];
|
||||||
|
const widthStr = attrs['width'];
|
||||||
|
|
||||||
if (!style) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,40 +192,59 @@ class HtmlInteraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure proper style string formatting
|
||||||
if (style.length && !style.endsWith(';')) {
|
if (style.length && !style.endsWith(';')) {
|
||||||
style += ';';
|
style += ';';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add responsive defaults if missing
|
||||||
if (!style.includes('max-width')) {
|
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')) {
|
if (!style.includes('display')) {
|
||||||
style += 'display:inline;';
|
style += 'display: inline;';
|
||||||
}
|
}
|
||||||
|
|
||||||
attrs['style'] = style;
|
attrs['style'] = style;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeWidthHeightAttribute(attrs) {
|
function normalizeWidthHeightAttribute(attrs) {
|
||||||
|
// Normalize width/height attributes and remove if necessary
|
||||||
const widthStr = attrs['width'];
|
const widthStr = attrs['width'];
|
||||||
const heightStr = attrs['height'];
|
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);
|
// Remove height attribute if value is null or undefined
|
||||||
if (isNaN(widthValue) || widthValue <= displayWidth) return;
|
if (heightStr === null || heightStr === undefined) {
|
||||||
|
delete attrs['height'];
|
||||||
delete attrs['width'];
|
}
|
||||||
delete attrs['height'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeImageSize(attrs) {
|
function normalizeImageSize(attrs) {
|
||||||
normalizeStyleAttribute(attrs);
|
// Apply both style and attribute normalization
|
||||||
normalizeWidthHeightAttribute(attrs);
|
normalizeWidthHeightAttribute(attrs);
|
||||||
|
normalizeStyleAttribute(attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyImageNormalization() {
|
function applyImageNormalization() {
|
||||||
|
// Process all images on the page
|
||||||
document.querySelectorAll('img').forEach(img => {
|
document.querySelectorAll('img').forEach(img => {
|
||||||
const attrs = {
|
const attrs = {
|
||||||
style: img.getAttribute('style'),
|
style: img.getAttribute('style'),
|
||||||
@@ -218,16 +254,31 @@ class HtmlInteraction {
|
|||||||
|
|
||||||
normalizeImageSize(attrs);
|
normalizeImageSize(attrs);
|
||||||
|
|
||||||
if (attrs.style != null) img.setAttribute('style', attrs.style);
|
// Handle style attribute
|
||||||
if ('width' in attrs) img.setAttribute('width', attrs.width);
|
if (attrs.style !== null && attrs.style !== undefined) {
|
||||||
else img.removeAttribute('width');
|
img.setAttribute('style', attrs.style);
|
||||||
|
} else {
|
||||||
|
img.removeAttribute('style');
|
||||||
|
}
|
||||||
|
|
||||||
if ('height' in attrs) img.setAttribute('height', attrs.height);
|
// Handle width attribute
|
||||||
else img.removeAttribute('height');
|
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() {
|
function safeApplyImageNormalization() {
|
||||||
|
// Error-safe wrapper for the normalization function
|
||||||
try {
|
try {
|
||||||
applyImageNormalization();
|
applyImageNormalization();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -235,6 +286,7 @@ class HtmlInteraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run normalization when page loads
|
||||||
window.onload = safeApplyImageNormalization;
|
window.onload = safeApplyImageNormalization;
|
||||||
</script>
|
</script>
|
||||||
''';
|
''';
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
abstract class PlatformInfo {
|
abstract class PlatformInfo {
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
static bool isTestingForWeb = false;
|
static bool isTestingForWeb = false;
|
||||||
|
static bool isIntegrationTesting = false;
|
||||||
|
|
||||||
static bool get isWeb => kIsWeb || isTestingForWeb;
|
static bool get isWeb => kIsWeb || isTestingForWeb;
|
||||||
static bool get isLinux => !isWeb && defaultTargetPlatform == TargetPlatform.linux;
|
static bool get isLinux => !isWeb && defaultTargetPlatform == TargetPlatform.linux;
|
||||||
|
|||||||
@@ -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 = '''
|
||||||
|
<img
|
||||||
|
src="https://example.com/image.jpg"
|
||||||
|
alt="inline-image-no-style"/>
|
||||||
|
<br>
|
||||||
|
<img
|
||||||
|
src="https://example.com/image.jpg"
|
||||||
|
style="width: 2000px; height: 200px;"
|
||||||
|
alt="inline-image-oversize-with-style"/>
|
||||||
|
<br>
|
||||||
|
<img
|
||||||
|
src="https://example.com/image.jpg"
|
||||||
|
width="2000"
|
||||||
|
height="200"
|
||||||
|
alt="inline-image-oversize-with-width-attribute"/>
|
||||||
|
<br>
|
||||||
|
<img
|
||||||
|
src="https://example.com/image.jpg"
|
||||||
|
style="width: 2000px; height: 200px;"
|
||||||
|
width="2000"
|
||||||
|
height="200"
|
||||||
|
alt="inline-image-oversize-with-style-and-width-attribute"/>
|
||||||
|
<br>
|
||||||
|
<img
|
||||||
|
src="https://example.com/image.jpg"
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
alt="inline-image-normal-size-with-width-attribute"/>
|
||||||
|
<br>
|
||||||
|
<img
|
||||||
|
src="https://example.com/image.jpg"
|
||||||
|
style="width: 100px; height: 100px;"
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
alt="inline-image-normal-size-with-style-and-width-attribute"/>
|
||||||
|
''';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> 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<void> _expectDisplayedEmailWithSubject() async {
|
||||||
|
await expectViewVisible(
|
||||||
|
$(EmailTileBuilder).which<EmailTileBuilder>(
|
||||||
|
(widget) => widget.presentationEmail.subject == subject
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _expectEmailViewDisplaysNormalizedInlineImages() async {
|
||||||
|
HtmlContentViewer? htmlContentViewer;
|
||||||
|
await $(HtmlContentViewer)
|
||||||
|
.which<HtmlContentViewer>((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<EmailView>((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<Map<String, dynamic>>.from(result);
|
||||||
|
|
||||||
|
expect(imageList.length, 6);
|
||||||
|
|
||||||
|
for (var img in imageList) {
|
||||||
|
final imgWidth= img['width'];
|
||||||
|
final imgHeight = img['height'];
|
||||||
|
final imgStyle = img['style'];
|
||||||
|
final imgAlt = img['alt'];
|
||||||
|
final imgOuterHTML = img['outerHTML'];
|
||||||
|
log("DeformedInlinedImageScenario::_expectEmailViewDisplaysNormalizedInlineImages:Image $imgOuterHTML");
|
||||||
|
|
||||||
|
if (imgAlt == 'inline-image-normal-size-with-width-attribute') {
|
||||||
|
expect(imgStyle, isNull);
|
||||||
|
expect(imgWidth, equals('100'));
|
||||||
|
expect(imgHeight, equals('100'));
|
||||||
|
} else if (imgAlt == 'inline-image-normal-size-with-style-and-width-attribute') {
|
||||||
|
expect(imgStyle.contains('max-width: 100%;'), isFalse);
|
||||||
|
expect(imgStyle.contains('width: 100px; height: 100px;'), isTrue);
|
||||||
|
expect(imgWidth, equals('100'));
|
||||||
|
expect(imgHeight, equals('100'));
|
||||||
|
} else {
|
||||||
|
expect(imgStyle, equals('max-width: 100%;height: auto;display: inline;'));
|
||||||
|
expect(imgWidth, isNull);
|
||||||
|
expect(imgHeight, isNull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import '../../base/test_base.dart';
|
||||||
|
import '../../scenarios/email_detailed/deformed_inlined_image_scenario.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestBase().runPatrolTest(
|
||||||
|
description: 'Should see normalized inline image when open email with content contain inline image',
|
||||||
|
scenarioBuilder: ($) => DeformedInlinedImageScenario($),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -196,6 +196,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
|||||||
final isEmailContentClipped = RxBool(false);
|
final isEmailContentClipped = RxBool(false);
|
||||||
final attendanceStatus = Rxn<AttendanceStatus>();
|
final attendanceStatus = Rxn<AttendanceStatus>();
|
||||||
|
|
||||||
|
GlobalKey<HtmlContentViewState>? htmlContentViewKey;
|
||||||
EmailId? _currentEmailId;
|
EmailId? _currentEmailId;
|
||||||
Identity? _identitySelected;
|
Identity? _identitySelected;
|
||||||
ButtonState? _printEmailButtonState;
|
ButtonState? _printEmailButtonState;
|
||||||
@@ -243,6 +244,9 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
|||||||
void onInit() {
|
void onInit() {
|
||||||
_registerObxStreamListener();
|
_registerObxStreamListener();
|
||||||
_listenDownloadAttachmentProgressState();
|
_listenDownloadAttachmentProgressState();
|
||||||
|
if (PlatformInfo.isIntegrationTesting) {
|
||||||
|
htmlContentViewKey = GlobalKey<HtmlContentViewState>();
|
||||||
|
}
|
||||||
super.onInit();
|
super.onInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +254,9 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
|||||||
void onClose() {
|
void onClose() {
|
||||||
_downloadProgressStateController.close();
|
_downloadProgressStateController.close();
|
||||||
_attachmentListScrollController.dispose();
|
_attachmentListScrollController.dispose();
|
||||||
|
if (PlatformInfo.isIntegrationTesting) {
|
||||||
|
htmlContentViewKey = null;
|
||||||
|
}
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -447,6 +447,7 @@ class EmailView extends GetWidget<SingleEmailController> {
|
|||||||
),
|
),
|
||||||
child: LayoutBuilder(builder: (context, constraints) {
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
return HtmlContentViewer(
|
return HtmlContentViewer(
|
||||||
|
key: controller.htmlContentViewKey,
|
||||||
contentHtml: allEmailContents,
|
contentHtml: allEmailContents,
|
||||||
initialWidth: constraints.maxWidth,
|
initialWidth: constraints.maxWidth,
|
||||||
direction: AppUtils.getCurrentDirection(context),
|
direction: AppUtils.getCurrentDirection(context),
|
||||||
@@ -486,6 +487,7 @@ class EmailView extends GetWidget<SingleEmailController> {
|
|||||||
),
|
),
|
||||||
child: LayoutBuilder(builder: (context, constraints) {
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
return HtmlContentViewer(
|
return HtmlContentViewer(
|
||||||
|
key: controller.htmlContentViewKey,
|
||||||
contentHtml: allEmailContents,
|
contentHtml: allEmailContents,
|
||||||
initialWidth: constraints.maxWidth,
|
initialWidth: constraints.maxWidth,
|
||||||
direction: AppUtils.getCurrentDirection(context),
|
direction: AppUtils.getCurrentDirection(context),
|
||||||
|
|||||||
Reference in New Issue
Block a user