|
| 1 | +import { fileURLToPath } from 'node:url' |
| 2 | +import { expect, test } from '@playwright/test' |
| 3 | + |
| 4 | +const fixtureUrl = `/@fs${fileURLToPath(new URL('./icons.fixture.ts', import.meta.url))}` |
| 5 | +const contextUrl = `/@fs${fileURLToPath(new URL('../../hub/src/client/context.ts', import.meta.url))}` |
| 6 | +const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M4 12l5 5L20 6" stroke="currentColor" stroke-width="3"/><circle cx="5" cy="5" r="3" opacity=".4"/></svg>' |
| 7 | +const dataUrl = `data:image/svg+xml,${encodeURIComponent(svg)}` |
| 8 | +const mask = `mask:${dataUrl}` |
| 9 | + |
| 10 | +test('mask data bypasses fetching and ordinary images keep their rendering', async ({ page }) => { |
| 11 | + await page.route('https://api.iconify.design/**', route => route.abort()) |
| 12 | + await page.goto('/') |
| 13 | + await expect(page.getByRole('button', { name: 'Settings', exact: true })).toBeVisible() |
| 14 | + const result = await page.evaluate(async ({ url, dataUrl, mask }) => { |
| 15 | + const { createApp, h, ref, nextTick, IconifyIcon } = await import(url) |
| 16 | + const originalFetch = window.fetch |
| 17 | + let calls = 0 |
| 18 | + window.fetch = async () => { |
| 19 | + calls++ |
| 20 | + throw new Error('No network expected') |
| 21 | + } |
| 22 | + const source = ref(mask) |
| 23 | + const container = document.createElement('div') |
| 24 | + document.body.append(container) |
| 25 | + container.style.color = 'rgb(10, 20, 30)' |
| 26 | + const application = createApp({ render: () => h(IconifyIcon, { icon: source.value }) }) |
| 27 | + application.mount(container) |
| 28 | + const style = getComputedStyle(container.firstElementChild!) |
| 29 | + const masked = { image: style.maskImage, color: style.backgroundColor, mode: style.maskMode } |
| 30 | + source.value = dataUrl |
| 31 | + await nextTick() |
| 32 | + const image = container.querySelector('img')?.src |
| 33 | + application.unmount() |
| 34 | + container.remove() |
| 35 | + window.fetch = originalFetch |
| 36 | + return { masked, image, calls } |
| 37 | + }, { url: fixtureUrl, dataUrl, mask }) |
| 38 | + expect(result.masked.image).toContain(dataUrl) |
| 39 | + expect(result.masked.color).toBe('rgb(10, 20, 30)') |
| 40 | + expect(result.masked.mode).toBe('alpha') |
| 41 | + expect(result.image).toBe(dataUrl) |
| 42 | + expect(result.calls).toBe(0) |
| 43 | +}) |
| 44 | + |
| 45 | +for (const mode of ['standalone', 'embedded']) { |
| 46 | + for (const colorScheme of ['light', 'dark'] as const) { |
| 47 | + test(`mask states screenshot: ${mode}, ${colorScheme}`, async ({ page }, testInfo) => { |
| 48 | + await page.emulateMedia({ colorScheme }) |
| 49 | + await page.route('https://api.iconify.design/**', route => route.abort()) |
| 50 | + await page.goto(mode === 'embedded' ? '/?embedded' : '/') |
| 51 | + await expect(page.getByRole('button', { name: 'Settings', exact: true })).toBeVisible() |
| 52 | + await page.evaluate(async ({ url, mask, dataUrl }) => { |
| 53 | + const { getDevframeClientContext } = await import(url) |
| 54 | + const context = getDevframeClientContext()! |
| 55 | + context.docks.register({ |
| 56 | + id: 'test:mask', |
| 57 | + type: 'launcher', |
| 58 | + title: 'Local mask', |
| 59 | + icon: mask, |
| 60 | + launcher: { title: 'Local mask launcher', icon: mask, description: 'Bundled SVG with inherited foreground color' }, |
| 61 | + }) |
| 62 | + context.docks.register({ |
| 63 | + id: 'test:themed', |
| 64 | + type: 'launcher', |
| 65 | + title: 'Theme pair', |
| 66 | + icon: { light: mask, dark: `mask:${dataUrl.replace('opacity%3D%22.4', 'opacity%3D%22.8')}` }, |
| 67 | + launcher: { title: 'Themed launcher' }, |
| 68 | + }) |
| 69 | + context.commands.register({ id: 'test:mask-command', title: 'Local mask command', icon: mask, handler: () => {} }) |
| 70 | + }, { url: contextUrl, mask, dataUrl }) |
| 71 | + const button = page.getByRole('button', { name: 'Local mask', exact: true }) |
| 72 | + const icon = button.locator('[style*="mask:"]') |
| 73 | + await expect(icon).toBeVisible() |
| 74 | + await button.click() |
| 75 | + await expect(page.getByText('Local mask launcher', { exact: true })).toBeVisible() |
| 76 | + await expect(button).toHaveClass(/text-primary/) |
| 77 | + await expect.poll(() => icon.evaluate(element => getComputedStyle(element).backgroundColor === getComputedStyle(element).color)).toBe(true) |
| 78 | + const themed = page.getByRole('button', { name: 'Theme pair', exact: true }) |
| 79 | + if (mode === 'embedded') |
| 80 | + await expect(themed).toHaveClass(/op50/) |
| 81 | + await expect(themed.locator('[style*="mask:"]:visible')).toHaveAttribute('style', new RegExp(colorScheme === 'dark' ? 'opacity%3D%22.8' : 'opacity%3D%22.4')) |
| 82 | + await button.hover() |
| 83 | + await page.screenshot({ animations: 'disabled', path: testInfo.outputPath(`${mode}-${colorScheme}-masks.png`) }) |
| 84 | + await page.keyboard.press('ControlOrMeta+k') |
| 85 | + await page.getByPlaceholder('Type a command...').fill('Local mask command') |
| 86 | + await expect(page.locator('[id="cmd-test:mask-command"] [style*="mask:"]')).toBeVisible() |
| 87 | + await page.screenshot({ animations: 'disabled', path: testInfo.outputPath(`${mode}-${colorScheme}-command.png`) }) |
| 88 | + }) |
| 89 | + } |
| 90 | +} |
0 commit comments