Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
74 changes: 74 additions & 0 deletions packages/react-grab/e2e/element-context.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,5 +139,79 @@ test.describe("Element Context Fallback", () => {
expect(clipboard).toContain("long-dom-element");
expect(clipboard.length).toBeLessThanOrEqual(510);
});

test("should include descendant text for syntax highlighted code blocks", async ({
reactGrab,
}) => {
await reactGrab.page.evaluate(() => {
const wrapper = document.createElement("div");
Object.assign(wrapper.style, {
position: "fixed",
top: "200px",
left: "200px",
width: "600px",
height: "160px",
zIndex: "999",
});

const codeBlock = document.createElement("pre");
codeBlock.className = "shiki shiki-themes github-light github-dark";
codeBlock.tabIndex = 0;
codeBlock.innerHTML = `
<code>
<span class="line"><span>git</span><span> add</span><span> .github/workflows/react-doctor.yml</span></span>
<span class="line"><span>git</span><span> commit</span><span> -m</span><span> "Add React Doctor to CI"</span></span>
<span class="line"><span>git</span><span> push</span></span>
</code>
`;

wrapper.appendChild(codeBlock);
document.body.appendChild(wrapper);
});

const didCopy = await reactGrab.copyElementViaApi("pre.shiki");
expect(didCopy).toBe(true);

const clipboard = await reactGrab.getClipboardContent();
expect(clipboard).toContain("<pre");
expect(clipboard).toContain("git add .github/workflows/react-doctor.yml");
expect(clipboard).toContain('git commit -m "Add React Doctor to CI"');
expect(clipboard).toContain("</pre>");
});

test("should include descendant text for nested link labels", async ({ reactGrab }) => {
await reactGrab.page.evaluate(() => {
const wrapper = document.createElement("div");
Object.assign(wrapper.style, {
position: "fixed",
top: "200px",
left: "200px",
width: "320px",
height: "80px",
zIndex: "999",
});

const link = document.createElement("a");
link.href = "/docs/ci-and-prs/github-actions-setup";
link.className = "flex h-8 w-full items-center gap-2 rounded-md px-2";
link.innerHTML = `
<span aria-hidden="true">#</span>
<span><span>GitHub Actions setup</span></span>
`;

wrapper.appendChild(link);
document.body.appendChild(wrapper);
});

const didCopy = await reactGrab.copyElementViaApi(
"a[href='/docs/ci-and-prs/github-actions-setup']",
);
expect(didCopy).toBe(true);

const clipboard = await reactGrab.getClipboardContent();
expect(clipboard).toContain('<a href="/docs/ci-and-prs/github-actions-setup"');
expect(clipboard).toContain("GitHub Actions setup");
expect(clipboard).toContain("</a>");
});
});
});
59 changes: 49 additions & 10 deletions packages/react-grab/src/core/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,8 +552,8 @@ const getFallbackContext = (element: Element): string => {

const tagName = getTagName(element);
const attrsText = formatAttrsForPreview(element);
const directText = getDirectTextContent(element);
const truncatedText = truncateString(directText, PREVIEW_TEXT_MAX_LENGTH);
const previewText = getPreviewTextContent(element, tagName);
const truncatedText = truncateString(previewText, PREVIEW_TEXT_MAX_LENGTH);

if (truncatedText.length > 0) {
return `<${tagName}${attrsText}>\n ${truncatedText}\n</${tagName}>`;
Expand Down Expand Up @@ -591,6 +591,37 @@ const formatPriorityAttrs = (
const isClassOrStyleAttr = (name: string): boolean =>
name === "class" || name === "className" || name === "style";

const TEXT_CONTENT_PREVIEW_TAGS = new Set([
"a",
"abbr",
"b",
"button",
"caption",
"cite",
"code",
"dd",
"dt",
"em",
"figcaption",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"label",
"legend",
"li",
"p",
"pre",
"small",
"span",
"strong",
"summary",
"td",
"th",
]);

const formatAttrsForPreview = (element: Element): string => {
const identifyingParts: string[] = [];
const remainingParts: string[] = [];
Expand All @@ -614,11 +645,13 @@ const formatAttrsForPreview = (element: Element): string => {
return identifyingParts.join("") + remainingParts.join("") + classAttr;
};

const collapseTextContent = (text: string): string => text.replace(/\s+/g, " ").trim();

const getDirectTextContent = (element: Element): string => {
let directText = "";
for (const node of element.childNodes) {
if (node.nodeType === Node.TEXT_NODE) {
const trimmed = node.textContent?.trim() ?? "";
const trimmed = collapseTextContent(node.textContent ?? "");
if (trimmed) {
directText += (directText ? " " : "") + trimmed;
}
Expand All @@ -627,6 +660,12 @@ const getDirectTextContent = (element: Element): string => {
return directText;
};

const getPreviewTextContent = (element: Element, tagName: string): string => {
const directText = getDirectTextContent(element);
if (directText || !TEXT_CONTENT_PREVIEW_TAGS.has(tagName)) return directText;
return collapseTextContent(element.textContent ?? "");
};

const formatChildElements = (elements: Array<Element>): string => {
if (elements.length === 0) return "";
if (elements.length <= 2) {
Expand All @@ -647,8 +686,8 @@ export const getInlineHTMLPreview = (element: Element): string => {
}

const attrsText = formatAttrsForPreview(element);
const directText = getDirectTextContent(element);
const truncatedText = truncateString(directText, PREVIEW_TEXT_MAX_LENGTH);
const previewText = getPreviewTextContent(element, tagName);
const truncatedText = truncateString(previewText, PREVIEW_TEXT_MAX_LENGTH);

if (truncatedText) {
return `<${tagName}${attrsText}>${truncatedText}</${tagName}>`;
Expand All @@ -659,7 +698,7 @@ export const getInlineHTMLPreview = (element: Element): string => {
export const getHTMLPreview = (element: Element): string => {
const tagName = getTagName(element);
const attrsText = formatAttrsForPreview(element);
const directText = getDirectTextContent(element);
const previewText = getPreviewTextContent(element, tagName);

const topElements: Array<Element> = [];
const bottomElements: Array<Element> = [];
Expand All @@ -682,12 +721,12 @@ export const getHTMLPreview = (element: Element): string => {

let content = "";
const topElementsStr = formatChildElements(topElements);
if (topElementsStr) content += `\n ${topElementsStr}`;
if (directText.length > 0) {
content += `\n ${truncateString(directText, PREVIEW_TEXT_MAX_LENGTH)}`;
if (topElementsStr && !previewText) content += `\n ${topElementsStr}`;
if (previewText.length > 0) {
content += `\n ${truncateString(previewText, PREVIEW_TEXT_MAX_LENGTH)}`;
}
const bottomElementsStr = formatChildElements(bottomElements);
if (bottomElementsStr) content += `\n ${bottomElementsStr}`;
if (bottomElementsStr && !previewText) content += `\n ${bottomElementsStr}`;
Comment thread
cursor[bot] marked this conversation as resolved.

if (content.length > 0) {
return `<${tagName}${attrsText}>${content}\n</${tagName}>`;
Expand Down
Loading