Skip to content
Merged
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
32 changes: 25 additions & 7 deletions src/components/ReadmeBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,28 @@ const LANGUAGE_COLORS: Record<string, string> = {
Svelte: "#ff3e00",
};

const README_BANNER_FILENAME = "banner.png";

const getReadmeBannerMarkdown = (username: string) =>
`![${username}'s GitHub Banner](https://raw.githubusercontent.com/${username}/${username}/main/${README_BANNER_FILENAME})`;

const getReadmeBannerInstructions = (
username: string,
markdown: string,
copied: boolean
) =>
`${copied ? "✓ Markdown copied to clipboard!" : "Markdown could not be copied automatically."}

Next steps:
1. Download the banner and save it as ${README_BANNER_FILENAME}.
2. Upload ${README_BANNER_FILENAME} to the root of your ${username}/${username} profile repository.
3. ${copied ? "Paste the copied Markdown in README.md." : "Copy the Markdown below into README.md."}

The Markdown points to /${README_BANNER_FILENAME} on the main branch:
${markdown}

If you use a different filename or folder, update the Markdown path to match.`;

const ReadmeBanner = forwardRef<ReadmeBannerRef, ReadmeBannerProps>(
(
{ user, artType, availableForHire, showWebsite, showJoinDate, showBio },
Expand Down Expand Up @@ -520,19 +542,15 @@ const ReadmeBanner = forwardRef<ReadmeBannerRef, ReadmeBannerProps>(
* Copy markdown code to clipboard
*/
const copyMarkdown = () => {
// You would typically host the image on GitHub or a CDN
// For now, we'll provide a template markdown that users can update
const markdown = `![${user.login}'s GitHub Banner](https://raw.githubusercontent.com/${user.login}/${user.login}/main/banner.png)`;
const markdown = getReadmeBannerMarkdown(user.login);

navigator.clipboard
.writeText(markdown)
.then(() => {
alert(
"✓ Markdown copied to clipboard!\n\nUpload your banner.png to your profile repository and use this code in your README."
);
alert(getReadmeBannerInstructions(user.login, markdown, true));
})
.catch(() => {
alert("Markdown code:\n\n" + markdown);
alert(getReadmeBannerInstructions(user.login, markdown, false));
});
};

Expand Down
98 changes: 97 additions & 1 deletion tests/health.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,63 @@
import { test, expect } from '@playwright/test';
import { test, expect, type Page } from '@playwright/test';

async function mockOctocatProfile(page: Page) {
await page.route('https://api.github.com/users/octocat', async (route) => {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
login: 'octocat',
avatar_url: 'https://avatars.githubusercontent.com/u/583231?v=4',
name: 'The Octocat',
followers: 1234,
public_repos: 8,
bio: 'GitHub mascot',
created_at: '2011-01-25T18:44:36Z',
company: '@github',
location: 'San Francisco',
blog: 'github.blog',
}),
});
});

await page.route('https://github.com/octocat.contribs', async (route) => {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
total_contributions: 42,
weeks: [],
}),
});
});

await page.route('https://api.github.com/users/octocat/repos?*', async (route) => {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify([
{ stargazers_count: 10, forks_count: 2, language: 'TypeScript' },
]),
});
});
}

async function openOctocatReadmeBanner(page: Page) {
await page.goto('/');
const usernameInput = page.locator('#github-handle');
await usernameInput.click();
await usernameInput.pressSequentially('octocat');
await expect(usernameInput).toHaveValue('octocat');
await page.getByRole('button', { name: 'Generate' }).click();
await page.getByRole('tab', { name: 'README Banner' }).click();
}

async function copyMarkdownDialog(page: Page) {
const dialogPromise = page.waitForEvent('dialog');
const copyMarkdownPromise = page.getByRole('button', { name: 'Copy Markdown' }).evaluate((button: HTMLButtonElement) => {
button.click();
});
const dialog = await dialogPromise;

return { dialog, copyMarkdownPromise };
}

test('octocanvas homepage responds with expected title text', async ({ request }) => {
const res = await request.get('/');
Expand All @@ -7,3 +66,40 @@ test('octocanvas homepage responds with expected title text', async ({ request }
expect(body).toContain('OCTOCANVAS');
expect(body).toContain('Collectibles');
});

test('README banner markdown popup explains where to place the image', async ({ page }) => {
await mockOctocatProfile(page);
await openOctocatReadmeBanner(page);
const { dialog, copyMarkdownPromise } = await copyMarkdownDialog(page);

expect(dialog.message()).toContain('Download the banner and save it as banner.png.');
expect(dialog.message()).toContain('Upload banner.png to the root of your octocat/octocat profile repository.');
expect(dialog.message()).toContain('The Markdown points to /banner.png on the main branch:');
expect(dialog.message()).toContain('https://raw.githubusercontent.com/octocat/octocat/main/banner.png');

await dialog.dismiss();
await copyMarkdownPromise;
});

test('README banner markdown popup exposes markdown when clipboard copy fails', async ({ page }) => {
await page.addInitScript(() => {
Object.defineProperty(Navigator.prototype, 'clipboard', {
value: {
writeText: () => Promise.reject(new Error('Clipboard denied')),
},
configurable: true,
});
});

await mockOctocatProfile(page);
await openOctocatReadmeBanner(page);
const { dialog, copyMarkdownPromise } = await copyMarkdownDialog(page);

expect(dialog.message()).toContain('Markdown could not be copied automatically.');
expect(dialog.message()).toContain('Copy the Markdown below into README.md.');
expect(dialog.message()).toContain('https://raw.githubusercontent.com/octocat/octocat/main/banner.png');
expect(dialog.message()).not.toContain('Markdown copied to clipboard');

await dialog.dismiss();
await copyMarkdownPromise;
});