-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
429 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
// Use IntelliSense to learn about possible attributes. | ||
// Hover to view descriptions of existing attributes. | ||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | ||
"version": "0.2.0", | ||
"configurations": [ | ||
{ | ||
"type": "node", | ||
"request": "launch", | ||
"name": "Jest current file", | ||
"skipFiles": ["<node_internals>/**"], | ||
"runtimeExecutable": "sh", | ||
"program": "${workspaceFolder}/node_modules/.bin/jest", | ||
"args": ["${relativeFile}", "--coverage=false"], | ||
"console": "integratedTerminal", | ||
"internalConsoleOptions": "openOnFirstSessionStart" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
191 changes: 191 additions & 0 deletions
191
packages/graph-explorer/src/modules/GraphViewer/renderNode.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
/** | ||
* @jest-environment jsdom | ||
*/ | ||
|
||
import { describe, it, expect, jest } from "@jest/globals"; | ||
import { ICONS_CACHE, VertexIconConfig, renderNode } from "./renderNode"; | ||
import { | ||
createRandomColor, | ||
createRandomName, | ||
} from "../../utils/testing/randomData"; | ||
|
||
global.fetch = | ||
jest.fn< | ||
( | ||
input: RequestInfo | URL, | ||
init?: RequestInit | undefined | ||
) => Promise<Response> | ||
>(); | ||
|
||
describe("renderNode", () => { | ||
beforeEach(() => { | ||
ICONS_CACHE.clear(); | ||
jest.resetAllMocks(); | ||
}); | ||
|
||
it("should return undefined given no icon url", async () => { | ||
const mockedFetch = jest.mocked(global.fetch); | ||
const node: VertexIconConfig = { | ||
type: createRandomName("vertex"), | ||
color: createRandomColor(), | ||
iconUrl: undefined, | ||
iconImageType: "image/svg+xml", | ||
}; | ||
|
||
const result = await renderNode(node); | ||
|
||
expect(result).toBeUndefined(); | ||
expect(mockedFetch).not.toBeCalled(); | ||
expect(ICONS_CACHE.size).toEqual(0); | ||
}); | ||
|
||
it("should return undefined when error occurs in fetch", async () => { | ||
const mockedFetch = jest | ||
.mocked(global.fetch) | ||
.mockRejectedValue(new Error("Failed")); | ||
const node: VertexIconConfig = { | ||
type: createRandomName("vertex"), | ||
color: createRandomColor(), | ||
iconUrl: createRandomName("iconUrl"), | ||
iconImageType: "image/svg+xml", | ||
}; | ||
|
||
const result = await renderNode(node); | ||
|
||
expect(mockedFetch).toBeCalledWith(node.iconUrl); | ||
expect(result).toBeUndefined(); | ||
expect(ICONS_CACHE.size).toEqual(0); | ||
}); | ||
|
||
it("should return icon url given image type is not an SVG", async () => { | ||
const mockedFetch = jest.mocked(global.fetch); | ||
const node: VertexIconConfig = { | ||
type: createRandomName("vertex"), | ||
color: createRandomColor(), | ||
iconUrl: createRandomName("iconUrl"), | ||
iconImageType: "image/png", | ||
}; | ||
|
||
const result = await renderNode(node); | ||
|
||
expect(result).toBe(node.iconUrl); | ||
expect(mockedFetch).not.toBeCalled(); | ||
expect(ICONS_CACHE.size).toEqual(0); | ||
}); | ||
|
||
it("should return processed SVG string keeping original color", async () => { | ||
const originalColor = createRandomColor(); | ||
const svgContent = `<svg fill="${originalColor}" xmlns="http://www.w3.org/2000/svg"/>`; | ||
const mockedFetch = jest | ||
.mocked(global.fetch) | ||
.mockResolvedValue(new Response(new Blob([svgContent]))); | ||
const node: VertexIconConfig = { | ||
type: createRandomName("vertex"), | ||
color: createRandomColor(), | ||
iconUrl: createRandomName("iconUrl"), | ||
iconImageType: "image/svg+xml", | ||
}; | ||
|
||
const result = await renderNode(node); | ||
|
||
expect(mockedFetch).toBeCalledWith(node.iconUrl); | ||
expect(result).toBeDefined(); | ||
expect(result?.slice(0, 24)).toEqual("data:image/svg+xml;utf8,"); | ||
const decodedResult = decodeSvg(result); | ||
expect(decodedResult).toEqual( | ||
wrapExpectedSvg( | ||
`<svg fill="${originalColor}" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%"/>` | ||
) | ||
); | ||
}); | ||
|
||
it("should return processed SVG string replacing currentColor with default color when custom color not provided", async () => { | ||
const svgContent = `<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg"/>`; | ||
const mockedFetch = jest | ||
.mocked(global.fetch) | ||
.mockResolvedValue(new Response(new Blob([svgContent]))); | ||
const iconUrl = createRandomName("iconUrl"); | ||
const node: VertexIconConfig = { | ||
type: createRandomName("vertex"), | ||
color: undefined, | ||
iconUrl, | ||
iconImageType: "image/svg+xml", | ||
}; | ||
|
||
const result = await renderNode(node); | ||
|
||
expect(mockedFetch).toBeCalledWith(iconUrl); | ||
expect(result).toBeDefined(); | ||
expect(result?.slice(0, 24)).toEqual("data:image/svg+xml;utf8,"); | ||
const decodedResult = decodeSvg(result); | ||
expect(decodedResult).toEqual( | ||
wrapExpectedSvg( | ||
`<svg fill="#128EE5" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%"/>` | ||
) | ||
); | ||
}); | ||
|
||
it("should return processed SVG string replacing currentColor with provided custom color", async () => { | ||
const svgContent = `<svg fill="currentColor" stroke="currentColor" xmlns="http://www.w3.org/2000/svg"/>`; | ||
const mockedFetch = jest | ||
.mocked(global.fetch) | ||
.mockResolvedValue(new Response(new Blob([svgContent]))); | ||
const node: VertexIconConfig = { | ||
type: createRandomName("vertex"), | ||
color: createRandomColor(), | ||
iconUrl: createRandomName("iconUrl"), | ||
iconImageType: "image/svg+xml", | ||
}; | ||
|
||
const result = await renderNode(node); | ||
|
||
expect(mockedFetch).toBeCalledWith(node.iconUrl); | ||
expect(result).toBeDefined(); | ||
expect(result?.slice(0, 24)).toEqual("data:image/svg+xml;utf8,"); | ||
const decodedResult = decodeSvg(result); | ||
expect(decodedResult).toEqual( | ||
wrapExpectedSvg( | ||
`<svg fill="${node.color}" stroke="${node.color}" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%"/>` | ||
) | ||
); | ||
}); | ||
|
||
it("should return processed SVG string modifying the width and height", async () => { | ||
const originalColor = createRandomColor(); | ||
const svgContent = `<svg fill="${originalColor}" viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg"/>`; | ||
const mockedFetch = jest | ||
.mocked(global.fetch) | ||
.mockResolvedValue(new Response(new Blob([svgContent]))); | ||
const node: VertexIconConfig = { | ||
type: createRandomName("vertex"), | ||
color: createRandomColor(), | ||
iconUrl: createRandomName("iconUrl"), | ||
iconImageType: "image/svg+xml", | ||
}; | ||
|
||
const result = await renderNode(node); | ||
|
||
expect(mockedFetch).toBeCalledWith(node.iconUrl); | ||
expect(result).toBeDefined(); | ||
expect(result?.slice(0, 24)).toEqual("data:image/svg+xml;utf8,"); | ||
const decodedResult = decodeSvg(result); | ||
expect(decodedResult).toEqual( | ||
wrapExpectedSvg( | ||
`<svg fill="${originalColor}" viewBox="0 0 24 24" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg"/>` | ||
) | ||
); | ||
}); | ||
}); | ||
|
||
/** Wraps SVG string in another SVG element matching what is expected. */ | ||
function wrapExpectedSvg(svgContent: string): string { | ||
return `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg> | ||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"> | ||
${svgContent} | ||
</svg>`; | ||
} | ||
|
||
/** Decodes the string and removes the data type URL prefix, returning only the SVG portion. */ | ||
function decodeSvg(result: string | undefined) { | ||
return decodeURIComponent(result!).replace("data:image/svg+xml;utf8,", ""); | ||
} |
83 changes: 83 additions & 0 deletions
83
packages/graph-explorer/src/modules/GraphViewer/renderNode.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { VertexTypeConfig } from "../../core"; | ||
|
||
export type VertexIconConfig = Pick< | ||
VertexTypeConfig, | ||
"type" | "iconUrl" | "iconImageType" | "color" | ||
>; | ||
|
||
export const ICONS_CACHE: Map<string, string> = new Map(); | ||
|
||
export async function renderNode( | ||
vtConfig: VertexIconConfig | ||
): Promise<string | undefined> { | ||
if (!vtConfig.iconUrl) { | ||
return; | ||
} | ||
|
||
if (vtConfig.iconImageType !== "image/svg+xml") { | ||
return vtConfig.iconUrl; | ||
} | ||
|
||
// To avoid multiple requests, cache icons under the same URL | ||
if (ICONS_CACHE.get(vtConfig.iconUrl)) { | ||
return ICONS_CACHE.get(vtConfig.iconUrl); | ||
} | ||
|
||
try { | ||
const response = await fetch(vtConfig.iconUrl); | ||
let iconText = await response.text(); | ||
|
||
iconText = updateSize(iconText); | ||
iconText = embedSvgInsideCytoscapeSvgWrapper(iconText); | ||
iconText = applyCurrentColor(iconText, vtConfig.color || "#128EE5"); | ||
iconText = encodeSvg(iconText); | ||
|
||
// Save to the cache | ||
ICONS_CACHE.set(vtConfig.iconUrl, iconText); | ||
return iconText; | ||
} catch (error) { | ||
// Ignore the error and move on | ||
console.error(`Failed to fetch the icon data for vertex ${vtConfig.type}`); | ||
return; | ||
} | ||
} | ||
|
||
/** | ||
* Embeds the given SVG content inside an SVG wrapper that is designed to work well with Cytoscape rendering. | ||
* @param svgContent The SVG content to embed | ||
* @returns SVG string | ||
*/ | ||
function embedSvgInsideCytoscapeSvgWrapper(svgContent: string): string { | ||
return `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg> | ||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"> | ||
${svgContent} | ||
</svg>`; | ||
} | ||
|
||
/** | ||
* Replaces `currentColor` with the given color to make sure the SVG applies the right color in Cytoscape. | ||
* @param svgContent | ||
* @param color | ||
* @returns | ||
*/ | ||
function applyCurrentColor(svgContent: string, color: string) { | ||
return svgContent.replace(/currentColor/gm, color); | ||
} | ||
|
||
function updateSize(svgContent: string): string { | ||
const parser = new DOMParser(); | ||
const serializer = new XMLSerializer(); | ||
|
||
const doc = parser.parseFromString(svgContent, "application/xml"); | ||
|
||
doc.documentElement.setAttribute("width", "100%"); | ||
doc.documentElement.setAttribute("height", "100%"); | ||
|
||
const result = serializer.serializeToString(doc.documentElement); | ||
|
||
return result; | ||
} | ||
|
||
function encodeSvg(svgContent: string): string { | ||
return "data:image/svg+xml;utf8," + encodeURIComponent(svgContent); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,3 @@ | ||
// jest-dom adds custom jest matchers for asserting on DOM nodes. | ||
// allows you to do things like: | ||
// expect(element).toHaveTextContent(/react/i) | ||
// learn more: https://github.com/testing-library/jest-dom | ||
import "@testing-library/jest-dom"; | ||
// Sets up `fetch` for JSDom environment. | ||
// https://github.com/jsdom/jsdom/issues/1724#issuecomment-720727999 | ||
import "whatwg-fetch"; |
Oops, something went wrong.