Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions e2e/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions e2e/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export {
setupPullrequestsDC,
setupPRComments,
setupPRCommentPost,
getLastCommentPostBody,
} from './setup-mock';
40 changes: 40 additions & 0 deletions e2e/helpers/setup-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
42 changes: 40 additions & 2 deletions e2e/page-objects/fragments/IssueComments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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<boolean> {
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<string> {
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 '';
}
}
36 changes: 32 additions & 4 deletions e2e/scenarios/jira/addComment.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,46 @@
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();

const issueFrame = await getIssueFrame(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);
}
Original file line number Diff line number Diff line change
@@ -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" }
}
}
19 changes: 16 additions & 3 deletions src/commands/jira/postComment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, unknown>): string {
return typeof comment === 'string'
? comment
: convertAdfToWikimarkup(comment as unknown as Parameters<typeof convertAdfToWikimarkup>[0]);
}

export async function postComment(
issue: IssueKeyAndSite<DetailedSiteInfo>,
comment: string,
comment: string | Record<string, unknown>,
commentId?: string,
restriction?: CommentVisibility,
): Promise<Comment> {
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);
Expand Down
Loading