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
91 changes: 91 additions & 0 deletions packages/webamp/js/playlistHtml.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { createPlaylistURL, getAsDataURI } from "./playlistHtml";

function base64ToUtf8(str: string): string {
return decodeURIComponent(
Array.prototype.map
.call(
atob(str),
(c: string) => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`
)
.join("")
);
}

describe("playlistHtml", () => {
describe("createPlaylistURL", () => {
it("handles track names with characters outside Latin-1 range", () => {
const props = {
averageTrackLength: "3:45",
numberOfTracks: 3,
playlistLengthSeconds: 15,
playlistLengthMinutes: 11,
tracks: [
"Song with emoji 🎵🎶",
"中文歌曲名称.mp3",
"Песня на русском.mp3",
],
};

const result = createPlaylistURL(props);

// Should be a valid data URI
expect(result).toMatch(/^data:text\/html;base64,/);

// Decode the base64 to check the content
const base64Content = result.replace("data:text/html;base64,", "");
const decodedHTML = base64ToUtf8(base64Content);

// Check that all track names are present in the decoded HTML
expect(decodedHTML).toContain("Song with emoji 🎵🎶");
expect(decodedHTML).toContain("中文歌曲名称.mp3");
expect(decodedHTML).toContain("Песня на русском.mp3");

// Verify playlist metadata is included
expect(decodedHTML).toContain("3");
expect(decodedHTML).toContain("3:45");
expect(decodedHTML).toContain("11");
expect(decodedHTML).toContain("15");
});

it("creates valid HTML with basic track names", () => {
const props = {
averageTrackLength: "4:20",
numberOfTracks: 1,
playlistLengthSeconds: 20,
playlistLengthMinutes: 4,
tracks: ["test-track.mp3"],
};

const result = createPlaylistURL(props);

expect(result).toMatch(/^data:text\/html;base64,/);

const base64Content = result.replace("data:text/html;base64,", "");
const decodedHTML = atob(base64Content);

expect(decodedHTML).toContain("<html>");
expect(decodedHTML).toContain("test-track.mp3");
expect(decodedHTML).toContain("Winamp Generated PlayList");
});
});

describe("getAsDataURI", () => {
it("converts text to base64 data URI", () => {
const text = "Hello, World!";
const result = getAsDataURI(text);

expect(result).toBe("data:text/html;base64,SGVsbG8sIFdvcmxkIQ==");
});

it("handles text with HTML tags", () => {
const text = "<html>Test</html>";
const result = getAsDataURI(text);

expect(result).toMatch(/^data:text\/html;base64,/);

const base64Content = result.replace("data:text/html;base64,", "");
const decoded = atob(base64Content);
expect(decoded).toBe(text);
});
});
});
12 changes: 10 additions & 2 deletions packages/webamp/js/playlistHtml.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { flushSync } from "react-dom";

Expand All @@ -9,8 +10,15 @@ interface Props {
tracks: string[];
}

export const getAsDataURI = (text: string): string =>
`data:text/html;base64,${window.btoa(text)}`;
export const getAsDataURI = (text: string): string => {
// Properly encode UTF-8 to base64
// btoa() only handles Latin-1 (ISO-8859-1), so we need to encode UTF-8 first
const utf8Bytes = encodeURIComponent(text).replace(
/%([0-9A-F]{2})/g,
(_, p1) => String.fromCharCode(parseInt(p1, 16))
);
return `data:text/html;base64,${window.btoa(utf8Bytes)}`;
};

// Replaces deprecated "noshade" attribute
const noshadeStyle = {
Expand Down