Skip to content

Commit 5a6b382

Browse files
#6111 Fix secure reply buttons (#6112)
* run live tests * skip some live tests * update “download all attachments” button selector * update selector for “more” message button * fix data-tooltip * fix test * fix: secure reply buttons insert in gmail page * fix: test * fix: test * reenable tests * fix: serial * fix: styling * fix: test * fix: only * fix: style * fix: test * fix: test --------- Co-authored-by: Ioan Moldovan <[email protected]>
1 parent a6ea692 commit 5a6b382

File tree

5 files changed

+79
-54
lines changed

5 files changed

+79
-54
lines changed

extension/css/webmail.css

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
outline: 0;
3030
}
3131

32+
.btDi4d {
33+
position: initial !important;
34+
}
35+
3236
.small #flowcrypt_secure_compose_button_icon {
3337
height: 40px;
3438
}
@@ -190,8 +194,7 @@ body.cryptup_gmail .inserted div.reply_message_button {
190194
display: inline-flex;
191195
padding: 20px 12px 0 10px;
192196
opacity: 0.75;
193-
margin-left: 18px;
194-
margin-right: 8px;
197+
margin: 0;
195198
height: auto;
196199
}
197200

@@ -207,17 +210,21 @@ body.cryptup_gmail div.action_menu_message_button {
207210
align-items: center;
208211
text-transform: capitalize;
209212
cursor: pointer;
210-
padding: 4px;
213+
padding: 8px 4px;
214+
font-size: 14px;
215+
font-weight: 400;
216+
color: #1f1f1f;
217+
line-height: 16px;
211218
}
212219

213220
body.cryptup_gmail div.action_menu_message_button:hover {
214221
background-color: #eee; /* mimic Gmail hover. uses exact color pallete from Gmail */
215222
}
216223

217224
body.cryptup_gmail div.action_menu_message_button > img {
218-
height: 20px;
219-
width: 20px;
220-
padding: 0 12px;
225+
height: auto;
226+
width: 18px;
227+
padding: 0 11px;
221228
object-fit: contain;
222229
}
223230

extension/js/content_scripts/webmail/gmail/gmail-element-replacer.ts

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ export class GmailElementReplacer extends WebmailElementReplacer {
5656
msgInner: 'div.a3s:visible:not(.undefined), .message_inner_body:visible',
5757
msgInnerText: 'table.cf.An',
5858
msgInnerContainingPgp: "div.a3s:not(.undefined):contains('" + PgpArmor.headers('null').begin + "')",
59-
msgActionsBtn: '.J-J5-Ji.aap',
60-
msgActionsMenu: '.b7.J-M',
59+
msgActionsBtn: '.Wsq5Cf',
60+
msgActionsMenu: '.tB5Jxf-M-S5Cmsd, ul.aqdrmf-Kf[role="menu"]',
6161
attachmentsContainerOuter: 'div.hq.gt',
6262
attachmentsContainerInner: 'div.aQH',
6363
translatePrompt: '.adI, .wl4W9b',
@@ -67,7 +67,7 @@ export class GmailElementReplacer extends WebmailElementReplacer {
6767
standardComposeRecipient: 'div.az9 span[email][data-hovercard-id]',
6868
numberOfAttachments: '.aVW',
6969
numberOfAttachmentsLabel: '.aVW span:first-child',
70-
attachmentsButtons: '.aZi',
70+
downloadAllAttachmentsButton: '.aZi, .pYTkkf-JX-ano',
7171
draftsList: '.ae4',
7272
};
7373

@@ -297,8 +297,8 @@ export class GmailElementReplacer extends WebmailElementReplacer {
297297
return !!$('iframe.pgp_block').filter(':visible').length;
298298
};
299299

300-
private addMenuButton = (replyOption: ReplyOption, gmailContextMenuBtn: string) => {
301-
if ($(gmailContextMenuBtn).is(':visible') && !document.querySelector(`.action_${replyOption.replace('a_', '')}_message_button`)) {
300+
private addMenuButton = (replyOption: ReplyOption, gmailContextMenuBtn: Element | null) => {
301+
if (gmailContextMenuBtn && $(gmailContextMenuBtn).is(':visible') && !document.querySelector(`.action_${replyOption.replace('a_', '')}_message_button`)) {
302302
const button = $(this.factory.btnSecureMenuBtn(replyOption)).insertAfter(gmailContextMenuBtn); // xss-safe-factory
303303
button.on(
304304
'click',
@@ -323,7 +323,7 @@ export class GmailElementReplacer extends WebmailElementReplacer {
323323
// only replace the last one FlowCrypt reply button if does not have any buttons replaced yet, and only replace the last one
324324
for (const elem of convoReplyBtnsArr) {
325325
$(elem).addClass('inserted');
326-
const gmailReplyBtn = $(elem).find('.aaq.L3');
326+
const gmailReplyBtn = $(elem).find('.DILLkc');
327327
const secureReplyBtn = $(this.factory.btnSecureReply()).insertAfter(gmailReplyBtn); // xss-safe-factory
328328
secureReplyBtn.addClass(gmailReplyBtn.attr('class') || '');
329329
secureReplyBtn.off();
@@ -391,7 +391,7 @@ export class GmailElementReplacer extends WebmailElementReplacer {
391391
return;
392392
}
393393
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
394-
const messageContainer = secureReplyInvokedFromMenu ? $('.T-I-JO.T-I-Kq').closest('.h7') : $(btn.closest('.h7')!);
394+
const messageContainer = secureReplyInvokedFromMenu ? $('.ii.gt').closest('div.h7') : $(btn.closest('.h7')!);
395395
if (messageContainer.is(':last-child')) {
396396
if (this.isEncrypted()) {
397397
await this.setReplyBoxEditable(replyOption);
@@ -402,8 +402,7 @@ export class GmailElementReplacer extends WebmailElementReplacer {
402402
this.insertEncryptedReplyBox(messageContainer, replyOption);
403403
}
404404
if (secureReplyInvokedFromMenu) {
405-
$(this.sel.msgActionsBtn).removeClass('T-I-JO T-I-Kq');
406-
$(this.sel.msgActionsMenu).hide();
405+
$(this.sel.msgActionsBtn).trigger('click');
407406
}
408407
};
409408

@@ -559,7 +558,7 @@ export class GmailElementReplacer extends WebmailElementReplacer {
559558
}
560559
if (nRenderedAttachments !== attachments.length) {
561560
// according to #4200, no point in showing "download all" button if at least one attachment is encrypted etc.
562-
$(this.sel.attachmentsButtons).hide();
561+
$(this.sel.downloadAllAttachmentsButton).hide();
563562
}
564563
if (nRenderedAttachments === 0) {
565564
attachmentsContainerInner.parents(this.sel.attachmentsContainerOuter).first().hide();
@@ -638,7 +637,23 @@ export class GmailElementReplacer extends WebmailElementReplacer {
638637
};
639638

640639
private getConvoRootEl = (anyInnerElement: HTMLElement) => {
641-
return $(anyInnerElement).closest('div.if, div.aHU, td.Bu').first();
640+
const ancestorConvoRoot = $(anyInnerElement).closest('div.if, div.aHU, td.Bu');
641+
if (ancestorConvoRoot.length) {
642+
return ancestorConvoRoot.first();
643+
}
644+
645+
// New Gmail structure: conversation root might be a sibling
646+
// Navigate to common parent container and find conversation root
647+
const commonParent = $(anyInnerElement).closest('.nH.a98.iY, .if');
648+
if (commonParent.length) {
649+
const siblingConvoRoot = commonParent.find('div.if, div.aHU, td.Bu').first();
650+
if (siblingConvoRoot.length) {
651+
return siblingConvoRoot;
652+
}
653+
}
654+
655+
// Fallback: return empty jQuery object to maintain consistent return type
656+
return $();
642657
};
643658

644659
private insertEncryptedReplyBox = (messageContainer: JQuery<Element>, replyOption: ReplyOption) => {
@@ -655,7 +670,7 @@ export class GmailElementReplacer extends WebmailElementReplacer {
655670

656671
private replaceStandardReplyBox = async (msgId?: string, force = false) => {
657672
const legacyDraftReplyRegex = new RegExp(/\[(flowcrypt|cryptup):link:draft_reply:([0-9a-fr\-]+)]/);
658-
const newReplyBoxes = $('div.nr.tMHS5d, td.amr > div.nr, div.gA td.I5').not('.reply_message_evaluated').filter(':visible').get();
673+
const newReplyBoxes = $('div.nr.tMHS5d, div.gA td.I5, .amr > div.nr').not('.reply_message_evaluated').filter(':visible').get();
659674
if (newReplyBoxes.length) {
660675
// removing this line will cause unexpected draft creation bug reappear
661676
// https://github.com/FlowCrypt/flowcrypt-browser/issues/5616#issuecomment-1972897692
@@ -946,27 +961,41 @@ export class GmailElementReplacer extends WebmailElementReplacer {
946961
*
947962
* Issue: https://github.com/FlowCrypt/flowcrypt-browser/issues/5933
948963
*/
949-
const messageContainer = $('.T-I-JO.T-I-Kq').closest('.h7');
964+
// Find the active message menu
965+
const visibleMenu = Array.from(document.querySelectorAll(this.sel.msgActionsMenu)).find(
966+
menu => window.getComputedStyle(menu).display !== 'none' && (menu as HTMLElement).offsetParent
967+
);
968+
if (!visibleMenu) {
969+
return;
970+
}
971+
972+
// Find the message container from the menu's position or context
973+
const messageContainer = $('div.h7:visible').last(); // Get the last visible message container
950974
const msgIdElement = messageContainer.find('[data-legacy-message-id], [data-message-id]');
951975
const msgId = msgIdElement.attr('data-legacy-message-id') || msgIdElement.attr('data-message-id');
952-
const replyAllMenuButton = document.querySelector('#r2');
976+
977+
// New Gmail uses data-action-type attributes
978+
const replyButton = visibleMenu.querySelector('li[data-action-type="94"]');
979+
const forwardButton = visibleMenu.querySelector('li[data-action-type="25"]');
980+
const replyAllButton = visibleMenu.querySelector('li[data-action-type="24"]'); // Estimated based on pattern
981+
953982
// Cannot use jQuery $('#r2').is(':visible') because the element is considered invisible if its parent has display: none.
954-
if (replyAllMenuButton && window.getComputedStyle(replyAllMenuButton).display !== 'none') {
955-
this.addMenuButton('a_reply_all', '#r2');
983+
if (replyAllButton && window.getComputedStyle(replyAllButton).display !== 'none') {
984+
this.addMenuButton('a_reply_all', replyAllButton);
956985
} else if (msgId) {
957986
try {
958987
const gmailMsg = await this.emailProvider.msgGet(msgId, 'metadata');
959988
const replyMeta = GmailParser.determineReplyMeta(this.acctEmail, [], gmailMsg);
960989

961990
if (replyMeta.to.length > 1) {
962-
this.addMenuButton('a_reply_all', '#r');
991+
this.addMenuButton('a_reply_all', replyButton);
963992
}
964993
} catch (error) {
965994
console.error(`Failed to retrieve message metadata for ID ${msgId}:`, error);
966995
}
967996
}
968997

969-
this.addMenuButton('a_reply', '#r');
970-
this.addMenuButton('a_forward', '#r3');
998+
this.addMenuButton('a_reply', replyButton);
999+
this.addMenuButton('a_forward', forwardButton);
9711000
};
9721001
}

package-lock.json

Lines changed: 0 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/source/tests/gmail.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
180180
await gotoGmailPage(gmailPage, '/KtbxLvHkSWwbVHxgCbWNvXVKGjFgqMbGQq');
181181
await Util.sleep(5);
182182
await gmailPage.waitAll('iframe');
183-
await gmailPage.waitAll(['.aZi'], { visible: true });
183+
await gmailPage.waitAll(['.pYTkkf-JX-ano'], { visible: true });
184184

185185
// attachments which contain emoji in filename are rendered correctly
186186
await gotoGmailPage(gmailPage, '/FMfcgzGtwqFGhMwWtLRjkPJlQlZHSlrW');
@@ -190,7 +190,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
190190
const urls2 = await gmailPage.getFramesUrls(['/chrome/elements/attachment.htm']);
191191
expect(urls2.length).to.equal(2);
192192
expect(await gmailPage.waitForContent('.aVW span:first-child', '2'));
193-
expect(await gmailPage.waitForContent('.aVW span.a2H', 'Scanned by Gmail'));
193+
expect(await gmailPage.waitForContent('.aVW span.a2H', '  •  Scanned by Gmail'));
194194
await gmailPage.close();
195195
})
196196
);
@@ -405,10 +405,10 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
405405
testWithBrowser(async (t, browser) => {
406406
await BrowserRecipe.setUpCommonAcct(t, browser, 'ci.tests.gmail');
407407
let gmailPage = await openGmailPage(t, browser, '/FMfcgzGkbDRNgcQxLmkhBCKVSFwkfdvV'); // plain convo
408-
await gmailPage.waitAndClick('[data-tooltip="Reply"]', { delay: 1 });
408+
await gmailPage.waitAndClick('div.adn [aria-label="Reply"]', { delay: 1 });
409409
await gmailPage.close();
410410
gmailPage = await openGmailPage(t, browser, '/181d226b4e69f172'); // go to encrypted convo
411-
await gmailPage.waitAndClick('[data-tooltip="Reply"]', { delay: 1 });
411+
await gmailPage.waitAndClick('div.adn [aria-label="Reply"]', { delay: 1 });
412412
await gmailPage.waitTillGone('.reply_message');
413413
await gmailPage.waitAll('[data-tooltip^="Send"]'); // The Send button from the Standard reply box
414414
await gmailPage.waitForContent(
@@ -419,7 +419,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
419419
await gmailPage.waitAll('.reply_message');
420420
await pageHasSecureReplyContainer(t, browser, gmailPage, { isReplyPromptAccepted: true });
421421
await gotoGmailPage(gmailPage, '/FMfcgzGkbDRNpjDdNvCrwzqvXspZZxvh'); // go to signed convo
422-
await gmailPage.waitAndClick('[data-tooltip="Reply"]', { delay: 1 });
422+
await gmailPage.waitAndClick('div.adn [aria-label="Reply"]', { delay: 1 });
423423
await gmailPage.waitTillGone('.reply_message');
424424
await gmailPage.waitAll('[data-tooltip^="Send"]'); // The Send button from the Standard reply box
425425
await gmailPage.notPresent('.reply_message_evaluated .error_notification'); // should not show the warning about switching to encrypted reply
@@ -438,7 +438,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
438438
await gmailPage.waitAndClick('[role="listitem"] .adf.ads', { delay: 1 }); // click first message of thread
439439
await Util.sleep(3);
440440
const messages = await gmailPage.target.$$('[role="listitem"] .adn.ads');
441-
const plainReplyButton = await messages[0].$('[data-tooltip="Reply"]');
441+
const plainReplyButton = await messages[0].$('[aria-label="Reply"]');
442442
await Util.sleep(1);
443443
await plainReplyButton!.click();
444444
await gmailPage.waitAndClick('#switch_to_encrypted_reply'); // Switch to encrypted compose
@@ -467,7 +467,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
467467
const messages = await gmailPage.target.$$('[role="listitem"] .adn.ads');
468468
expect(messages.length).to.equal(2);
469469

470-
const plainReplyButton = await messages[0].$('[data-tooltip="Reply"]');
470+
const plainReplyButton = await messages[0].$('[aria-label="Reply"]');
471471
expect(plainReplyButton).to.be.ok;
472472
await Util.sleep(1);
473473
await plainReplyButton!.click();
@@ -491,11 +491,11 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
491491
await BrowserRecipe.setUpCommonAcct(t, browser, 'ci.tests.gmail');
492492
const gmailPage = await openGmailPage(t, browser);
493493
await gotoGmailPage(gmailPage, '/FMfcgzGkbDRNgcQxLmkhBCKVSFwkfdvV'); // plain convo
494-
await gmailPage.waitAndClick('[data-tooltip="Reply"]', { delay: 1 });
494+
await gmailPage.waitAndClick('div.adn [aria-label="Reply"]', { delay: 1 });
495495
await gotoGmailPage(gmailPage, '/FMfcgzGtwgfMhWTlgRwwKWzRhqNZzwXz'); // go to encrypted convo
496496
await Util.sleep(5);
497-
await gmailPage.waitAndClick('.adn [data-tooltip="More"]', { delay: 1 });
498-
await gmailPage.waitAndClick('[act="24"]', { delay: 1 }); // click reply-all
497+
await gmailPage.waitAndClick('.adn [aria-label="More message options"]', { delay: 1 });
498+
await gmailPage.waitAndClick('li[data-action-type="24"]', { delay: 1 }); // click reply-all
499499
await Util.sleep(3);
500500
await gmailPage.waitAll('[data-tooltip^="Send"]'); // The Send button from the Standard reply box
501501
await gmailPage.waitForContent(
@@ -518,7 +518,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
518518
await BrowserRecipe.setUpCommonAcct(t, browser, 'ci.tests.gmail');
519519
const gmailPage = await openGmailPage(t, browser);
520520
await gotoGmailPage(gmailPage, '/FMfcgzQZTMHNLflWQjRcSvWlMsKbLhpr');
521-
await gmailPage.waitAndClick('.adn [data-tooltip="More"]', { delay: 1 });
521+
await gmailPage.waitAndClick('.adn [aria-label="More message options"]', { delay: 1 });
522522
await gmailPage.waitAll('.action_reply_all_message_button');
523523
})
524524
);
@@ -531,8 +531,8 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
531531
const gmailPage = await openGmailPage(t, browser);
532532
await gotoGmailPage(gmailPage, '/FMfcgzGtwgfMhWTlgRwwKWzRhqNZzwXz'); // go to encrypted convo
533533
await Util.sleep(5);
534-
await gmailPage.waitAndClick('.adn [data-tooltip="More"]', { delay: 1 });
535-
await gmailPage.waitAndClick('[act="25"]', { delay: 1 }); // click forward
534+
await gmailPage.waitAndClick('.adn [aria-label="More message options"]', { delay: 1 });
535+
await gmailPage.waitAndClick('li[data-action-type="25"]', { delay: 1 }); // click forward
536536
await Util.sleep(3);
537537
await gmailPage.waitAll('[data-tooltip^="Send"]'); // The Send button from the Standard reply box
538538
await gmailPage.waitForContent(
@@ -556,7 +556,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
556556
await BrowserRecipe.setUpCommonAcct(t, browser, 'ci.tests.gmail');
557557
const gmailPage = await openGmailPage(t, browser);
558558
await gotoGmailPage(gmailPage, '/FMfcgzGtwgfMhWTlgRwwKWzRhqNZzwXz'); // go to encrypted convo
559-
const gmailContextMenu = '.J-J5-Ji.aap';
559+
const gmailContextMenu = '.adn [aria-label="More message options"]';
560560
await gmailPage.waitAndClick(gmailContextMenu);
561561
await Util.sleep(1);
562562
expect(await gmailPage.isElementPresent('@action-reply-message-button'));
@@ -589,7 +589,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
589589
const threadId = '181d226b4e69f172'; // 1st message -- thread id
590590
const gmailPage = await openGmailPage(t, browser, `/${threadId}`);
591591
await GmailPageRecipe.trimConvo(gmailPage, threadId);
592-
await gmailPage.waitAndClick('[data-tooltip="Reply"]', { delay: 5 });
592+
await gmailPage.waitAndClick('div.adn [aria-label="Reply"]', { delay: 5 });
593593
t.timeout(minutes(2)); // extend ava's timeout
594594
await Util.sleep(5);
595595
await gmailPage.waitTillFocusIsIn('div[aria-label="Message Body"]', { timeout: 10 });
@@ -621,7 +621,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
621621
await Util.sleep(5);
622622
await gmailPage.waitAll('iframe');
623623
expect(await gmailPage.isElementPresent('@container-attachments')).to.equal(false);
624-
await gmailPage.waitAll(['.aZi'], { visible: false });
624+
await gmailPage.waitAll(['.pYTkkf-JX-ano'], { visible: false });
625625
expect(await gmailPage.isElementVisible('.aQH')).to.equal(false); // original attachment container(s) should be hidden
626626
await gmailPage.close();
627627
})
@@ -642,7 +642,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test
642642
await Util.sleep(5);
643643
await gmailPage.waitAll('iframe');
644644
expect(await gmailPage.isElementPresent('@container-attachments')).to.equal(false);
645-
await gmailPage.waitAll(['.aZi'], { visible: false });
645+
await gmailPage.waitAll(['.pYTkkf-JX-ano'], { visible: false });
646646
await gmailPage.close();
647647
})
648648
);

test/source/tests/setup.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,7 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg==
853853
await addKeyPopup.waitAndClick('.action_add_private_key', { delay: 1 });
854854
await Util.sleep(1);
855855
await gmailPage.page.reload();
856+
await Util.sleep(3);
856857
await gmailPage.waitTillGone('@webmail-notification-notify_expiring_keys');
857858
// remove added key and observe warning appears again
858859
await settingsPage.waitAndClick('@action-remove-key-1');

0 commit comments

Comments
 (0)