From 830f4f36a09d7df2255b3457f36472bcdbd0d12c Mon Sep 17 00:00:00 2001 From: bhulovatyi Date: Fri, 13 Feb 2026 16:49:45 +0200 Subject: [PATCH] AXON-1896: mproved E2E to catch the regression for adding comments to Jira issues HAXON-1567: Fix for Jira DC comment --- e2e/compose.yml | 4 ++ e2e/helpers/index.ts | 1 + e2e/helpers/setup-mock.ts | 40 ++++++++++++++++++ e2e/page-objects/fragments/IssueComments.ts | 42 ++++++++++++++++++- e2e/scenarios/jira/addComment.spec.ts | 36 ++++++++++++++-- .../addComment-dc-reject-object-body.json | 17 ++++++++ src/commands/jira/postComment.ts | 19 +++++++-- 7 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 e2e/wiremock-mappings/mockedteams/addComment-dc-reject-object-body.json diff --git a/e2e/compose.yml b/e2e/compose.yml index c0268a53f..831a0796a 100644 --- a/e2e/compose.yml +++ b/e2e/compose.yml @@ -7,6 +7,7 @@ services: command: > --https-port 443 --verbose + --max-request-journal-entries 50 --https-keystore /home/wiremock_ssl_certs/wiremock-mockedteams.p12 volumes: - ./wiremock-mappings/mockedteams:/home/wiremock/mappings @@ -47,6 +48,9 @@ services: - ONLYSERVER - TARGET - DISABLE_ANALYTICS=1 + # Enable Atlaskit editor so add-comment E2E uses same path as when bug occurs (sends ADF); + # test catches "body must be string" if postComment normalization is removed. + - ATLASCODE_FF_OVERRIDES=atlascode-use-new-atlaskit-editor=true depends_on: - wiremock-mockedteams - wiremock-bitbucket diff --git a/e2e/helpers/index.ts b/e2e/helpers/index.ts index c9d0d8fab..062d6058e 100644 --- a/e2e/helpers/index.ts +++ b/e2e/helpers/index.ts @@ -12,4 +12,5 @@ export { setupPullrequestsDC, setupPRComments, setupPRCommentPost, + getLastCommentPostBody, } from './setup-mock'; diff --git a/e2e/helpers/setup-mock.ts b/e2e/helpers/setup-mock.ts index e62742c2c..fa1389557 100644 --- a/e2e/helpers/setup-mock.ts +++ b/e2e/helpers/setup-mock.ts @@ -73,6 +73,46 @@ export const cleanupWireMockMapping = async (request: APIRequestContext, mapping await request.delete(`http://wiremock-mockedteams:8080/__admin/mappings/${mappingId}`); }; +const WIREMOCK_JIRA = 'http://wiremock-mockedteams:8080'; + +/** + * Returns the parsed request body of the most recent POST to .../issue/.../comment from Wiremock journal. + * Used by the add-comment E2E for Jira DC only: DC expects body.body to be a string (wiki markup), not ADF. + * Cloud accepts ADF. Requires Wiremock to run with --max-request-journal-entries (see e2e/compose.yml). + */ +export async function getLastCommentPostBody( + request: APIRequestContext, +): Promise<{ body: unknown; rawRequest?: { method: string; url: string } } | null> { + const res = await request.get(`${WIREMOCK_JIRA}/__admin/requests`); + if (!res.ok) { + return null; + } + const data = (await res.json()) as { + requests?: Array<{ request: { method: string; url?: string; body?: string } }>; + }; + const requests = data?.requests ?? []; + const commentPost = requests + .filter( + (r) => + r.request?.method === 'POST' && + r.request?.url?.includes('/issue/') && + r.request?.url?.includes('/comment'), + ) + .pop(); + if (!commentPost?.request?.body) { + return null; + } + try { + const parsed = JSON.parse(commentPost.request.body) as { body?: unknown }; + return { + body: parsed.body, + rawRequest: { method: commentPost.request.method, url: commentPost.request.url ?? '' }, + }; + } catch { + return null; + } +} + export async function setupSearchMock(request: APIRequestContext, status: string, type: JiraTypes) { const file = type === JiraTypes.DC ? 'search-dc.json' : 'search.json'; const searchJSON = JSON.parse(fs.readFileSync(`e2e/wiremock-mappings/mockedteams/${file}`, 'utf-8')); diff --git a/e2e/page-objects/fragments/IssueComments.ts b/e2e/page-objects/fragments/IssueComments.ts index 85b44f5ed..a8e3f184c 100644 --- a/e2e/page-objects/fragments/IssueComments.ts +++ b/e2e/page-objects/fragments/IssueComments.ts @@ -24,9 +24,22 @@ export class IssueComments { async fillComment(commentText: string) { const input = this.newComment.getByPlaceholder('Add a comment...'); await input.click(); + // Support both simple editor (textarea) and Atlaskit editor (contenteditable) const textarea = this.newComment.locator('textarea').first(); - await expect(textarea).toBeVisible(); - await textarea.fill(commentText); + const contentEditable = this.newComment.locator('[contenteditable="true"]').first(); + await Promise.race([ + textarea.waitFor({ state: 'visible', timeout: 3000 }).then(() => 'textarea' as const), + contentEditable.waitFor({ state: 'visible', timeout: 3000 }).then(() => 'contenteditable' as const), + ]).catch(() => { + throw new Error('Add-comment editor (textarea or contenteditable) did not appear within 3s'); + }); + const useTextarea = await textarea.isVisible().catch(() => false); + if (useTextarea) { + await textarea.fill(commentText); + } else { + await contentEditable.click(); + await contentEditable.fill(commentText); + } } async saveNew() { @@ -39,4 +52,29 @@ export class IssueComments { const comment = this.commentsSection.getByText(commentText); await expect(comment).toBeVisible(); } + + /** + * Jira DC (and legacy) expects comment body as wiki markup string. If the app sends ADF object, + * the server returns: "Can not deserialize... START_OBJECT" or "Cannot deserialize... JsonToken.START_OBJECT". + * Returns true if the error banner shows this body-type error. + */ + async hasCommentBodyTypeError(): Promise { + const bodyTypeErrorPattern = + /Error posting comment|START_OBJECT|Can not deserialize|Cannot deserialize|JsonToken\.START_OBJECT|java\.lang\.String/; + const errorEl = this.frame.getByText(bodyTypeErrorPattern); + return errorEl + .first() + .isVisible() + .catch(() => false); + } + + async getCommentBodyTypeErrorText(): Promise { + const bodyTypeErrorPattern = + /Error posting comment|START_OBJECT|Can not deserialize|Cannot deserialize|JsonToken\.START_OBJECT|java\.lang\.String/; + const errorEl = this.frame.getByText(bodyTypeErrorPattern).first(); + if (await errorEl.isVisible().catch(() => false)) { + return (await errorEl.textContent()) ?? ''; + } + return ''; + } } diff --git a/e2e/scenarios/jira/addComment.spec.ts b/e2e/scenarios/jira/addComment.spec.ts index a0afab0e1..be6aec963 100644 --- a/e2e/scenarios/jira/addComment.spec.ts +++ b/e2e/scenarios/jira/addComment.spec.ts @@ -1,10 +1,15 @@ -import { Page } from '@playwright/test'; -import { getIssueFrame } from 'e2e/helpers'; +import { APIRequestContext, Page } from '@playwright/test'; +import { getIssueFrame, getLastCommentPostBody } from 'e2e/helpers'; +import { JiraTypes } from 'e2e/helpers/types'; import { AtlascodeDrawer, AtlassianSettings, JiraIssuePage } from 'e2e/page-objects'; const COMMENT_TEXT = 'This is a test comment added via e2e test'; -export async function addComment(page: Page) { +const BODY_TYPE_ERROR = + 'Jira (especially DC) expects comment body as wiki markup string, not ADF object. ' + + 'Add or keep the normalization in postComment.ts (ensureCommentBodyString).'; + +export async function addComment(page: Page, request: APIRequestContext, type: JiraTypes) { await new AtlascodeDrawer(page).jira.openIssue('BTS-1 - User Interface Bugs'); await new AtlassianSettings(page).closeSettingsPage(); @@ -12,7 +17,30 @@ export async function addComment(page: Page) { const issuePage = new JiraIssuePage(issueFrame); await issuePage.comments.addNew(COMMENT_TEXT); - await page.waitForTimeout(1_000); + await page.waitForTimeout(2_000); + + // DC only: Jira DC expects comment body as string (wiki markup), not ADF object. Cloud accepts ADF. + if (type === JiraTypes.DC) { + const lastComment = await getLastCommentPostBody(request); + if (lastComment !== null && lastComment !== undefined) { + const { body } = lastComment; + if (typeof body !== 'string') { + const preview = + typeof body === 'object' && body !== null && 'type' in body + ? `ADF object (type: ${(body as { type?: string }).type})` + : String(body).slice(0, 100); + throw new Error( + `${BODY_TYPE_ERROR} Request sent body as: ${preview}. ` + + 'Ensure ensureCommentBodyString() in postComment.ts converts ADF to string before calling the API.', + ); + } + } + + if (await issuePage.comments.hasCommentBodyTypeError()) { + const errText = await issuePage.comments.getCommentBodyTypeErrorText(); + throw new Error(`${BODY_TYPE_ERROR} Error shown: ${errText.slice(0, 300)}`); + } + } await issuePage.comments.expectExists(COMMENT_TEXT); } diff --git a/e2e/wiremock-mappings/mockedteams/addComment-dc-reject-object-body.json b/e2e/wiremock-mappings/mockedteams/addComment-dc-reject-object-body.json new file mode 100644 index 000000000..bfbf3a0c5 --- /dev/null +++ b/e2e/wiremock-mappings/mockedteams/addComment-dc-reject-object-body.json @@ -0,0 +1,17 @@ +{ + "priority": 2, + "request": { + "method": "POST", + "urlPath": "/rest/api/2/issue/BTS-1/comment", + "bodyPatterns": [ + { + "matches": ".*\"body\":\\s*\\{.*" + } + ] + }, + "response": { + "status": 400, + "body": "Comment body must be a string (wiki markup or plain text), not an ADF object. Jira DC does not accept ADF.", + "headers": { "Content-Type": "text/plain" } + } +} diff --git a/src/commands/jira/postComment.ts b/src/commands/jira/postComment.ts index a9d65c54c..6e4390e90 100644 --- a/src/commands/jira/postComment.ts +++ b/src/commands/jira/postComment.ts @@ -3,19 +3,32 @@ import { Comment, CommentVisibility, IssueKeyAndSite } from '@atlassianlabs/jira import { issueCommentEvent } from '../../analytics'; import { DetailedSiteInfo } from '../../atlclients/authInfo'; import { Container } from '../../container'; +import { convertAdfToWikimarkup } from '../../webviews/components/issue/common/adfToWikimarkup'; + +/** + * Jira DC (and legacy) expects comment body as a string (wiki markup). If the UI sends ADF (object), + * the API returns "Cannot deserialize value of type java.lang.String from Object value". Normalize + * so we always send a string; Cloud accepts string too. + */ +function ensureCommentBodyString(comment: string | Record): string { + return typeof comment === 'string' + ? comment + : convertAdfToWikimarkup(comment as unknown as Parameters[0]); +} export async function postComment( issue: IssueKeyAndSite, - comment: string, + comment: string | Record, commentId?: string, restriction?: CommentVisibility, ): Promise { const client = await Container.clientManager.jiraClient(issue.siteDetails); + const body = ensureCommentBodyString(comment); const resp = commentId === undefined - ? await client.addComment(issue.key, comment, restriction) - : await client.updateComment(issue.key, commentId, comment, restriction); + ? await client.addComment(issue.key, body, restriction) + : await client.updateComment(issue.key, commentId, body, restriction); issueCommentEvent(issue.siteDetails).then((e) => { Container.analyticsClient.sendTrackEvent(e);