Fixed the chinese character enoding issue (#25294)

* Fixed the chinese character enoding issue

* Added playwright test

* Fixed the chinese encoding issue

* fixed unit test

* fix code smell
This commit is contained in:
Rohit Jain 2026-01-15 18:31:26 +05:30 committed by GitHub
parent ffabae1908
commit b5b07093ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 266 additions and 17 deletions

View file

@ -10,14 +10,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, Page, test as base } from '@playwright/test';
import { test as base, expect, Page } from '@playwright/test';
import { ApiEndpointClass } from '../../support/entity/ApiEndpointClass';
import { DatabaseClass } from '../../support/entity/DatabaseClass';
import { EntityDataClass } from '../../support/entity/EntityDataClass';
import { TableClass } from '../../support/entity/TableClass';
import { PersonaClass } from '../../support/persona/PersonaClass';
import { UserClass } from '../../support/user/UserClass';
import { REACTION_EMOJIS, reactOnFeed } from '../../utils/activityFeed';
import { performAdminLogin } from '../../utils/admin';
import { redirectToHomePage } from '../../utils/common';
import { redirectToHomePage, uuid } from '../../utils/common';
import {
navigateToCustomizeLandingPage,
setUserDefaultPersona,
@ -593,3 +595,200 @@ test.describe('Mention notifications in Notification Box', () => {
);
});
});
test.describe('Mentions: Chinese character encoding in activity feed', () => {
const database = new DatabaseClass();
const endpointName = `测试Endpoint-${uuid()}`;
const apiEndpoint = new ApiEndpointClass(undefined, endpointName);
let schemaFqn: string;
const userName = `测试-${uuid()}`;
test.beforeAll('Create database, schema, and user with Chinese name', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await database.create(apiContext);
await apiEndpoint.create(apiContext);
await adminUser.create(apiContext);
schemaFqn = database.schemaResponseData.fullyQualifiedName;
const user = new UserClass({
firstName: userName,
lastName: '',
email: `${userName}@example.com`,
password: 'User@OMD123',
});
await user.create(apiContext);
await afterAction();
});
test.beforeEach(async ({ page }) => {
await adminUser.login(page);
await redirectToHomePage(page);
});
test('Should allow mentioning a user with Chinese characters in the activity feed', async ({
page,
}) => {
test.slow();
const feedPromise = page.waitForResponse((response) => {
const url = response.url();
return (
url.includes('/api/v1/feed') &&
url.includes('entityLink=') &&
url.includes('type=Conversation') &&
response.request().method() === 'GET'
);
});
await page.goto(`/databaseSchema/${schemaFqn}/activity_feed/all`);
const feedResponse = await feedPromise;
expect(feedResponse.status()).toBe(200);
await waitForAllLoadersToDisappear(page);
await page.getByTestId('comments-input-field').click();
const editorLocator = page.locator(
'[data-testid="editor-wrapper"] [contenteditable="true"].ql-editor'
);
await editorLocator.fill('Hey ');
await editorLocator.click();
await page.keyboard.press('@');
const userSuggestionsResponse = page.waitForResponse((response) => {
const url = response.url();
return (
url.includes('/api/v1/search/query') &&
url.includes(encodeURIComponent(userName))
);
});
await editorLocator.pressSequentially(userName);
await userSuggestionsResponse;
await page.locator(`[data-value="@${userName}"]`).first().click();
await expect(page.locator('[data-testid="send-button"]')).toBeVisible();
await expect(
page.locator('[data-testid="send-button"]')
).not.toBeDisabled();
const postMentionResponse = page.waitForResponse('/api/v1/feed/*/posts');
await page.locator('[data-testid="send-button"]').click();
await postMentionResponse;
const replyCard = page
.getByTestId('feed-reply-card')
.filter({ hasText: `Hey @${userName}` });
await expect(replyCard).toBeVisible();
await expect(replyCard.getByTestId('viewer-container')).toHaveText(
`Hey @${userName}`
);
const userMentionLink = replyCard.getByRole('link', {
name: `@${userName}`,
});
await expect(userMentionLink).toBeVisible();
await expect(userMentionLink).toHaveAttribute(
'href',
new RegExp(`/users/${userName}$`)
);
const [newPage] = await Promise.all([
page.context().waitForEvent('page'),
userMentionLink.click(),
]);
await newPage.waitForResponse((response) =>
response.url().includes(`/api/v1/users/name/${encodeURIComponent(userName)}`)
);
await waitForAllLoadersToDisappear(newPage);
expect(newPage.getByTestId('user-display-name')).toHaveText(userName);
});
test('Should encode the chinese character while mentioning api endpoint', async ({ page }) => {
test.slow();
const feedPromise = page.waitForResponse((response) => {
const url = response.url();
return (
url.includes('/api/v1/feed') &&
url.includes('entityLink=') &&
url.includes('type=Conversation') &&
response.request().method() === 'GET'
);
});
await page.goto(`/databaseSchema/${schemaFqn}/activity_feed/all`);
const feedResponse = await feedPromise;
expect(feedResponse.status()).toBe(200);
await waitForAllLoadersToDisappear(page);
await page.getByTestId('comments-input-field').click();
const editorLocator = page.locator(
'[data-testid="editor-wrapper"] [contenteditable="true"].ql-editor'
);
await editorLocator.fill('Check ');
await editorLocator.click();
await page.keyboard.press('#');
const endpointSuggestionsResponse = page.waitForResponse((response) => {
const url = response.url();
return (
url.includes('/api/v1/search/query') &&
url.includes(encodeURIComponent(endpointName))
);
});
await editorLocator.pressSequentially(endpointName);
await endpointSuggestionsResponse;
await page.locator(`[data-value="#apiEndpoint/${endpointName}"]`).first().click();
await expect(page.locator('[data-testid="send-button"]')).toBeVisible();
await expect(
page.locator('[data-testid="send-button"]')
).not.toBeDisabled();
const postMentionResponse = page.waitForResponse('/api/v1/feed/*/posts');
await page.locator('[data-testid="send-button"]').click();
await postMentionResponse;
const endpointFqn = apiEndpoint.entityResponseData.fullyQualifiedName;
const replyCard = page
.getByTestId('feed-reply-card')
.filter({ hasText: `Check #${endpointFqn}` });
await expect(replyCard).toBeVisible();
await expect(replyCard.getByTestId('viewer-container')).toHaveText(
`Check #${endpointFqn}`
);
const endpointMentionLink = replyCard.getByRole('link', {
name: endpointFqn,
});
await expect(endpointMentionLink).toBeVisible();
await expect(endpointMentionLink).toHaveAttribute(
'href',
new RegExp(`/apiEndpoint/${endpointFqn}$`)
);
const [newPage] = await Promise.all([
page.context().waitForEvent('page'),
endpointMentionLink.click(),
]);
await newPage.waitForResponse((response) =>
response.url().includes('/api/v1/apiEndpoints/name/') &&
response.request().method() === 'GET'
);
await waitForAllLoadersToDisappear(newPage);
await expect(newPage.getByTestId('entity-header-display-name')).toHaveText(
endpointName
);
});
});

View file

@ -90,7 +90,7 @@ export class ApiEndpointClass extends EntityClass {
entityResponseData: ResponseDataWithServiceType =
{} as ResponseDataWithServiceType;
constructor(name?: string) {
constructor(name?: string, apiEndpointName?: string) {
super(EntityTypeEndpoint.API_ENDPOINT);
this.serviceName = name ?? `pw-api-service-${uuid()}`;
@ -115,7 +115,7 @@ export class ApiEndpointClass extends EntityClass {
service: this.service.name,
};
this.apiEndpointName = `pw-api-endpoint-${uuid()}`;
this.apiEndpointName = apiEndpointName ?? `pw-api-endpoint-${uuid()}`;
this.fqn = `${this.service.name}.${this.apiCollection.name}.${this.apiEndpointName}.requestSchema`;
this.children = [

View file

@ -41,7 +41,7 @@ export const confirmStateInitialValue = {
isThread: false,
};
export const MENTION_ALLOWED_CHARS = /^[A-Za-z0-9_.-]*$/;
export const MENTION_ALLOWED_CHARS = /^[^\s]*$/;
export const MENTION_DENOTATION_CHARS = ['@', '#'];
export const TOOLBAR_ITEMS = [

View file

@ -22,6 +22,7 @@ import {
getEntityType,
getFeedHeaderTextFromCardStyle,
getFieldOperationIcon,
getFrontEndFormat,
suggestions,
} from './FeedUtils';
@ -45,8 +46,8 @@ jest.mock('../rest/searchAPI', () => ({
}));
jest.mock('./StringsUtils', () => ({
getEncodedFqn: jest.fn().mockImplementation((fqn) => fqn),
getDecodedFqn: jest.fn().mockImplementation((fqn) => fqn),
getEncodedFqn: jest.fn().mockImplementation((fqn) => encodeURIComponent(fqn)),
getDecodedFqn: jest.fn().mockImplementation((fqn) => decodeURIComponent(fqn)),
}));
jest.mock('./FeedUtils', () => ({
@ -108,8 +109,7 @@ describe('Feed Utils', () => {
const message = `<#E::user::"admin.test"|[@admin.test](http://localhost:3000/users/%22admin.test%22)> test`;
const result = getBackendFormat(message);
// eslint-disable-next-line no-useless-escape
const expectedResult = `<#E::user::\"admin.test\"|<#E::user::%22admin.test%22|[@admin.test](http://localhost:3000/users/%22admin.test%22)>> test`;
const expectedResult = `<#E::user::"admin.test"|<#E::user::"admin.test"|[@admin.test](http://localhost:3000/users/%22admin.test%22)>> test`;
expect(result).toStrictEqual(expectedResult);
});
@ -262,3 +262,43 @@ describe('getFieldOperationIcon', () => {
expect(stringResult).toContain(FieldOperation.Deleted);
});
});
describe('getFrontEndFormat', () => {
it('should return correct frontend format for user mention', () => {
const message =
'<#E::user::admin|[@admin](http://localhost:3000/users/admin)>';
const result = getFrontEndFormat(message);
expect(result).toBe('[@admin](http://localhost:3000/users/admin)');
});
it('should return correct frontend format for api endpoint with chinese characters', () => {
const encodedFqn = 'pw-api-service.collection.%E6%B5%8B%E8%AF%95';
const decodedFqn = 'pw-api-service.collection.测试';
const message = `<#E::apiEndpoint::${encodedFqn}|[#apiEndpoint/${decodedFqn}](http://localhost:3000/apiEndpoint/${encodedFqn})>`;
const result = getFrontEndFormat(message);
expect(result).toBe(
`[#apiEndpoint/${decodedFqn}](http://localhost:3000/apiEndpoint/${decodedFqn})`
);
});
it('should return correct frontend format for user mention with encoded characters in URL', () => {
const userName = '测试';
const encodedName = encodeURIComponent(userName);
const message = `<#E::user::${userName}|[@${userName}](http://localhost:3000/users/${encodedName})>`;
const result = getFrontEndFormat(message);
expect(result).toBe(
`[@${userName}](http://localhost:3000/users/${userName})`
);
});
it('should handle message without mentions', () => {
const message = 'Hello world';
expect(getFrontEndFormat(message)).toBe('Hello world');
});
});

View file

@ -271,7 +271,7 @@ export const userMentionItemWithAvatar = (
/>
) : (
<div
className="flex-center flex-shrink align-middle mention-avatar"
className="flex-center shrink align-middle mention-avatar"
data-testid="avatar"
style={{ backgroundColor: color }}>
<span>{character}</span>
@ -344,7 +344,18 @@ export const getFrontEndFormat = (message: string) => {
(m) => getEntityLinkDetail(m) ?? []
);
entityLinkList.forEach((m, i) => {
const markdownLink = entityLinkDetails[i][3];
let markdownLink = entityLinkDetails[i]?.[3];
const entityType = entityLinkDetails[i]?.[1];
const entityFqn = entityLinkDetails[i]?.[2];
const linkText = entityLinkDetails[i]?.[4];
const entityUrl = entityLinkDetails[i]?.[5];
if (entityType && entityFqn && entityUrl) {
const decodedUrl = getDecodedFqn(entityUrl);
markdownLink = `[${linkText}](${decodedUrl})`;
}
updatedMessage = updatedMessage.replaceAll(m, markdownLink);
});
@ -386,8 +397,7 @@ export const deletePost = async (
if (isThread) {
try {
const data = await deleteThread(threadId);
callback &&
callback((prev) => prev.filter((thread) => thread.id !== data.id));
callback?.((prev) => prev.filter((thread) => thread.id !== data.id));
} catch (error) {
showErrorToast(error as AxiosError);
}
@ -515,12 +525,12 @@ export const prepareFeedLink = (
const entityLink = entityUtilClassBase.getEntityLink(entityType, entityFQN);
if (!withoutFeedEntities.includes(entityType as EntityType)) {
if (withoutFeedEntities.includes(entityType as EntityType)) {
return entityLink;
} else {
const activityFeedLink = `${entityLink}/${TabSpecificField.ACTIVITY_FEED}`;
return subTab ? `${activityFeedLink}/${subTab}` : activityFeedLink;
} else {
return entityLink;
}
};
@ -580,7 +590,7 @@ export const entityDisplayName = (entityType: string, entityFQN: string) => {
// Remove quotes if the name is wrapped in quotes
if (displayName) {
displayName = displayName.replace(/(?:^"+)|(?:"+$)/g, '');
displayName = displayName.replaceAll(/(?:^"+)|(?:"+$)/g, '');
}
return displayName;