diff --git a/apps/solid-start/.gitignore b/apps/solid-start/.gitignore index d16c893df..419106e3a 100644 --- a/apps/solid-start/.gitignore +++ b/apps/solid-start/.gitignore @@ -27,3 +27,9 @@ gitignore # System Files .DS_Store Thumbs.db + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/apps/solid-start/package.json b/apps/solid-start/package.json index 5b239d31d..cefc5ae90 100644 --- a/apps/solid-start/package.json +++ b/apps/solid-start/package.json @@ -9,9 +9,12 @@ "dev": "vinxi dev", "build": "vinxi build", "start": "vinxi start", - "version": "vinxi version" + "version": "vinxi version", + "test": "playwright test", + "test:ui": "playwright test --ui" }, "dependencies": { + "@playwright/test": "1.49.1", "@responsive-image/cdn": "workspace:*", "@responsive-image/core": "workspace:*", "@responsive-image/solid": "workspace:*", diff --git a/apps/solid-start/playwright.config.ts b/apps/solid-start/playwright.config.ts new file mode 100644 index 000000000..2e87b8c41 --- /dev/null +++ b/apps/solid-start/playwright.config.ts @@ -0,0 +1,75 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + snapshotPathTemplate: + '{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + testIdAttribute: '', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + // serve the prod build, as the dev build uses vite middleware URLs for images that we don't want in tests + command: 'pnpm build && pnpm start', + url: 'http://localhost:3000', + }, +}); diff --git a/apps/solid-start/tests/__screenshots__/chromium/responsive-image.spec.ts/fixed-layout-1.png b/apps/solid-start/tests/__screenshots__/chromium/responsive-image.spec.ts/fixed-layout-1.png new file mode 100644 index 000000000..b121cb7e8 Binary files /dev/null and b/apps/solid-start/tests/__screenshots__/chromium/responsive-image.spec.ts/fixed-layout-1.png differ diff --git a/apps/solid-start/tests/__screenshots__/chromium/responsive-image.spec.ts/fixed-layout-w-aspect-1.png b/apps/solid-start/tests/__screenshots__/chromium/responsive-image.spec.ts/fixed-layout-w-aspect-1.png new file mode 100644 index 000000000..f28e940f2 Binary files /dev/null and b/apps/solid-start/tests/__screenshots__/chromium/responsive-image.spec.ts/fixed-layout-w-aspect-1.png differ diff --git a/apps/solid-start/tests/__screenshots__/chromium/responsive-image.spec.ts/responsive-layout-1.png b/apps/solid-start/tests/__screenshots__/chromium/responsive-image.spec.ts/responsive-layout-1.png new file mode 100644 index 000000000..2e1d36129 Binary files /dev/null and b/apps/solid-start/tests/__screenshots__/chromium/responsive-image.spec.ts/responsive-layout-1.png differ diff --git a/apps/solid-start/tests/responsive-image.spec.ts b/apps/solid-start/tests/responsive-image.spec.ts new file mode 100644 index 000000000..1c1a723d3 --- /dev/null +++ b/apps/solid-start/tests/responsive-image.spec.ts @@ -0,0 +1,206 @@ +import { test, expect } from '@playwright/test'; + +const sizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840]; +const imageTypes = [ + ['jpeg', 'jpg'], + ['webp', 'webp'], +]; + +test('responsive layout', async ({ page }) => { + await page.goto('/'); + + const img = page.locator('[data-test-local-image="responsive"]'); + const picture = page.locator('picture').filter({ has: img }); + + await expect(img).toHaveClass(/ri-responsive/); + await expect(img).toHaveAttribute( + 'src', + new RegExp(`/assets/aurora-[0-9]+w(-[a-zA-Z0-9-_]+)?.jpg`), + ); + await expect(img).toHaveScreenshot(); + + for (const [type, ext] of imageTypes) { + for (const size of sizes) { + await expect( + picture.locator(`source[type="image/${type}"]`), + `has ${type} with a width of ${size}`, + ).toHaveAttribute( + 'srcset', + new RegExp(`/assets/aurora-${size}w(-[a-zA-Z0-9-_]+)?.${ext} ${size}w`), + ); + } + } +}); + +test('fixed layout', async ({ page }) => { + await page.goto('/'); + + const img = page.locator('[data-test-local-image="fixed"]'); + const picture = page.locator('picture').filter({ has: img }); + + await expect(img).toHaveClass(/ri-fixed/); + await expect(img).toHaveAttribute( + 'src', + new RegExp(`/assets/aurora-[0-9]+w(-[a-zA-Z0-9-_]+)?.jpg`), + ); + await expect(img).toHaveAttribute('width', '320'); + await expect(img).toHaveAttribute('height', '213'); + await expect(img).toHaveScreenshot(); + + for (const [type, ext] of imageTypes) { + for (const size of sizes) { + await expect( + picture.locator(`source[type="image/${type}"]`), + `has ${type} with a width of 1x`, + ).toHaveAttribute( + 'srcset', + new RegExp(`/assets/aurora-640w(-[a-zA-Z0-9-_]+)?.${ext} 1x`), + ); + + await expect( + picture.locator(`source[type="image/${type}"]`), + `has ${type} with a width of 2x`, + ).toHaveAttribute( + 'srcset', + new RegExp(`/assets/aurora-640w(-[a-zA-Z0-9-_]+)?.${ext} 2x`), + ); + } + } +}); + +test('fixed layout w/ aspect', async ({ page }) => { + await page.goto('/'); + + const img = page.locator('[data-test-local-image="fixed,aspect"]'); + const picture = page.locator('picture').filter({ has: img }); + + await expect(img).toHaveClass(/ri-fixed/); + await expect(img).toHaveAttribute( + 'src', + new RegExp(`/assets/aurora-[0-9]+w(-[a-zA-Z0-9-_]+)?.jpg`), + ); + await expect(img).toHaveAttribute('width', '320'); + await expect(img).toHaveAttribute('height', '480'); + await expect(img).toHaveScreenshot(); + + for (const [type, ext] of imageTypes) { + for (const size of sizes) { + await expect( + picture.locator(`source[type="image/${type}"]`), + `has ${type} with a width of 1x`, + ).toHaveAttribute( + 'srcset', + new RegExp(`/assets/aurora-640w(-[a-zA-Z0-9-_]+)?.${ext} 1x`), + ); + + await expect( + picture.locator(`source[type="image/${type}"]`), + `has ${type} with a width of 2x`, + ).toHaveAttribute( + 'srcset', + new RegExp(`/assets/aurora-640w(-[a-zA-Z0-9-_]+)?.${ext} 2x`), + ); + } + } +}); + +test.describe('LQIP', () => { + test('color', async ({ page }) => { + await page.goto('/'); + + const img = page.locator('[data-test-local-image="fixed,lqip-color"]'); + const picture = page.locator('picture').filter({ has: img }); + + await expect(img).toHaveClass(/ri-fixed/); + await expect(img).toHaveAttribute( + 'src', + new RegExp(`/assets/aurora-[0-9]+w(-[a-zA-Z0-9-_]+)?.jpg`), + ); + + for (const [type, ext] of imageTypes) { + for (const size of sizes) { + await expect( + picture.locator(`source[type="image/${type}"]`), + `has ${type} with a width of 1x`, + ).toHaveAttribute( + 'srcset', + new RegExp(`/assets/aurora-640w(-[a-zA-Z0-9-_]+)?.${ext} 1x`), + ); + + await expect( + picture.locator(`source[type="image/${type}"]`), + `has ${type} with a width of 2x`, + ).toHaveAttribute( + 'srcset', + new RegExp(`/assets/aurora-640w(-[a-zA-Z0-9-_]+)?.${ext} 2x`), + ); + } + } + }); + + test('inline', async ({ page }) => { + await page.goto('/'); + + const img = page.locator('[data-test-local-image="fixed,lqip-inline"]'); + const picture = page.locator('picture').filter({ has: img }); + + await expect(img).toHaveClass(/ri-fixed/); + await expect(img).toHaveAttribute( + 'src', + new RegExp(`/assets/aurora-[0-9]+w(-[a-zA-Z0-9-_]+)?.jpg`), + ); + + for (const [type, ext] of imageTypes) { + for (const size of sizes) { + await expect( + picture.locator(`source[type="image/${type}"]`), + `has ${type} with a width of 1x`, + ).toHaveAttribute( + 'srcset', + new RegExp(`/assets/aurora-640w(-[a-zA-Z0-9-_]+)?.${ext} 1x`), + ); + + await expect( + picture.locator(`source[type="image/${type}"]`), + `has ${type} with a width of 2x`, + ).toHaveAttribute( + 'srcset', + new RegExp(`/assets/aurora-640w(-[a-zA-Z0-9-_]+)?.${ext} 2x`), + ); + } + } + }); + + test('blurhash', async ({ page }) => { + await page.goto('/'); + + const img = page.locator('[data-test-local-image="fixed,lqip-blurhash"]'); + const picture = page.locator('picture').filter({ has: img }); + + await expect(img).toHaveClass(/ri-fixed/); + await expect(img).toHaveAttribute( + 'src', + new RegExp(`/assets/aurora-[0-9]+w(-[a-zA-Z0-9-_]+)?.jpg`), + ); + + for (const [type, ext] of imageTypes) { + for (const size of sizes) { + await expect( + picture.locator(`source[type="image/${type}"]`), + `has ${type} with a width of 1x`, + ).toHaveAttribute( + 'srcset', + new RegExp(`/assets/aurora-640w(-[a-zA-Z0-9-_]+)?.${ext} 1x`), + ); + + await expect( + picture.locator(`source[type="image/${type}"]`), + `has ${type} with a width of 2x`, + ).toHaveAttribute( + 'srcset', + new RegExp(`/assets/aurora-640w(-[a-zA-Z0-9-_]+)?.${ext} 2x`), + ); + } + } + }); +}); diff --git a/packages/solid/src/responsive-image.tsx b/packages/solid/src/responsive-image.tsx index 8308f0d2b..ae990ac25 100644 --- a/packages/solid/src/responsive-image.tsx +++ b/packages/solid/src/responsive-image.tsx @@ -94,7 +94,7 @@ export const ResponsiveImage: Component = (props) => { const ar = args.src.aspectRatio; if (ar !== undefined && ar !== 0 && width !== undefined) { - height = width / ar; + height = Math.round(width / ar); } // We *must not* set the src attribute before the is actually rendered, and a child of diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 784ec5003..e74beb511 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -665,6 +665,9 @@ importers: apps/solid-start: dependencies: + '@playwright/test': + specifier: 1.49.1 + version: 1.49.1 '@responsive-image/cdn': specifier: workspace:* version: link:../../packages/cdn