Skip to content
Open
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
47 changes: 46 additions & 1 deletion src/lib/__tests__/cardElements.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from "react";
import { describe, it, expect } from "vitest";
import { estimateHeight, levelColor } from "../cardElements";
import { estimateHeight, levelColor, createBlock, MAX_BIO_LENGTH } from "../cardElements";
import type { CardData } from "../cardDataFetcher";
import type { CardRenderOptions } from "../cardOptions";

describe("cardElements utility functions", () => {
Expand Down Expand Up @@ -107,4 +109,47 @@ describe("cardElements utility functions", () => {
expect(levelColor(15, 10, mockTheme)).toBe("#15803d"); // > 1
});
});

describe("createBlock - bio", () => {
const mockTheme = {
bg: "#fff",
panel: "#f8f9fa",
text: "#000",
subtext: "#666",
border: "#ccc",
success: "#0f0",
accent: "#3b82f6",
};

const mockData = {
profile: {
avatarUrl: "https://example.com/avatar.png",
name: "Test User",
login: "testuser",
bio: "",
}
} as unknown as CardData;

it("truncates bio if it exceeds MAX_BIO_LENGTH", () => {
const longBio = "A".repeat(MAX_BIO_LENGTH + 10);
mockData.profile.bio = longBio;

const element = createBlock("bio", mockData, mockTheme, new Set()) as React.ReactElement;
// @ts-expect-error - inline bypass for complex React tree props
const bioDiv = (element as React.ReactElement).props.children[1].props.children[2];
const renderedText = bioDiv.props.children;
expect(renderedText).toBe("A".repeat(MAX_BIO_LENGTH) + "...");
});

it("does not truncate bio if it is within MAX_BIO_LENGTH", () => {
const shortBio = "A".repeat(MAX_BIO_LENGTH);
mockData.profile.bio = shortBio;

const element = createBlock("bio", mockData, mockTheme, new Set()) as React.ReactElement;
// @ts-expect-error - inline bypass for complex React tree props
const bioDiv = (element as React.ReactElement).props.children[1].props.children[2];
const renderedText = bioDiv.props.children;
expect(renderedText).toBe(shortBio);
});
});
});
6 changes: 4 additions & 2 deletions src/lib/cardElements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type {
} from "./cardOptions";
import { resolveBlockLayout } from "./cardOptions";

export const MAX_BIO_LENGTH = 110;

export type ThemePalette = {
bg: string;
panel: string;
Expand Down Expand Up @@ -98,8 +100,8 @@ function createBioBlock(data: CardData, theme: ThemePalette): ReactElement {
</div>
{data.profile.bio ? (
<div style={{ color: theme.subtext, fontSize: 13, maxWidth: 470 }}>
{data.profile.bio.length > 110
? `${data.profile.bio.slice(0, 110)}...`
{data.profile.bio.length > MAX_BIO_LENGTH
? `${data.profile.bio.slice(0, MAX_BIO_LENGTH)}...`
: data.profile.bio}
Comment on lines +103 to 105

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Truncating the bio string using standard String.prototype.slice can split surrogate pairs (such as emojis or certain multi-byte characters) if the truncation point falls in the middle of one. This can result in rendering malformed characters in the UI.

Using Array.from() or the spread operator ([...]) ensures that the string is sliced by Unicode code points rather than UTF-16 code units, preserving emojis and other multi-byte characters correctly.

Suggested change
{data.profile.bio.length > MAX_BIO_LENGTH
? `${data.profile.bio.slice(0, MAX_BIO_LENGTH)}...`
: data.profile.bio}
{data.profile.bio.length > MAX_BIO_LENGTH
? Array.from(data.profile.bio).slice(0, MAX_BIO_LENGTH).join("") + "..."
: data.profile.bio}

</div>
) : null}
Expand Down
Loading