diff --git a/src/components/ReadmeBanner.tsx b/src/components/ReadmeBanner.tsx index 53786aa..7601fac 100644 --- a/src/components/ReadmeBanner.tsx +++ b/src/components/ReadmeBanner.tsx @@ -68,6 +68,28 @@ const LANGUAGE_COLORS: Record = { 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( ( { user, artType, availableForHire, showWebsite, showJoinDate, showBio }, @@ -520,19 +542,15 @@ const ReadmeBanner = forwardRef( * 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)); }); }; diff --git a/tests/health.spec.ts b/tests/health.spec.ts index 132da82..28e8f5f 100644 --- a/tests/health.spec.ts +++ b/tests/health.spec.ts @@ -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('/'); @@ -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; +});