diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df4a3ac..b637d06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,3 +23,22 @@ jobs: steps: - uses: wyvox/action@v1 - run: pnpm lint + + e2e: + name: "E2E" + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: wyvox/action@v1 + - name: Install Playwright browser + run: pnpm --filter docs-for-ember-astro exec playwright install --with-deps chromium + - name: Run e2e tests + run: pnpm --filter docs-for-ember-astro test:e2e + - name: Upload report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: docs/playwright-report/ + retention-days: 7 diff --git a/.gitignore b/.gitignore index 807dfea..32cab47 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ .env +.claude/ .env.production .astro/ .log/ .DS_Store node_modules/ dist/ +test-results/ +playwright-report/ +playwright/.cache/ diff --git a/docs/e2e/client-rendering.spec.ts b/docs/e2e/client-rendering.spec.ts new file mode 100644 index 0000000..46db975 --- /dev/null +++ b/docs/e2e/client-rendering.spec.ts @@ -0,0 +1,55 @@ +import { test, expect } from "@playwright/test"; + +/** + * These tests guard the core promise of ember-astro: that an Ember component + * placed in an Astro page with `client:only="ember"` actually mounts and + * renders in the browser. Each assertion targets output that ONLY the Ember + * component produces (not the surrounding static Astro markup), so if client + * rendering breaks, the island stays empty and the test fails. + */ + +test("props: component renders with passed-in args", async ({ page }) => { + await page.goto("/examples/props"); + + // renders `{{@props.greeting}}, {{@props.who}}` + await expect(page.getByText("Hello, World")).toBeVisible(); +}); + +test("default block content: default slot renders as raw HTML", async ({ page }) => { + await page.goto("/examples/default-block-content"); + + // Demo renders `{{{@slots.default}}}` — the block content incl. a . + const island = page.locator("astro-island[component-export='Demo']"); + await expect(island).toContainText("block content here"); + await expect(island.locator("strong", { hasText: "here" })).toBeVisible(); +}); + +test("named slots: each named slot renders into its fieldset", async ({ page }) => { + await page.goto("/examples/named-slots"); + + // The .container element only exists once the Ember component has rendered. + const container = page.locator(".container"); + await expect(container).toBeVisible(); + + // Legends come from the .gjs template; slot bodies come from Astro slots + // (Astro strips the `slot="…"` attribute, so we match the rendered text). + for (const legend of ["@slots.one", "@slots.two", "@slots.three"]) { + await expect(container.locator("legend", { hasText: legend })).toBeVisible(); + } + for (const body of ["one", "two", "three"]) { + await expect(container.getByText(body, { exact: true })).toBeVisible(); + } +}); + +test("nested components: Outer renders and contains Inner", async ({ page }) => { + await page.goto("/examples/nested-components"); + + // Each component template renders its own fieldset+legend. Scope to the + // fieldset whose *direct* legend is "Outer" so we don't also match the + // static Astro wrapper
Demo. + const outer = page.locator('fieldset:has(> legend:text-is("Outer"))'); + await expect(outer).toBeVisible(); + + // The Inner island must have rendered nested inside Outer. + await expect(outer.locator('legend:text-is("Inner")')).toBeVisible(); +}); diff --git a/docs/package.json b/docs/package.json index 49b43c7..8d5a830 100644 --- a/docs/package.json +++ b/docs/package.json @@ -12,7 +12,8 @@ "start": "astro dev", "build": "astro build", "preview": "astro preview", - "astro": "astro" + "astro": "astro", + "test:e2e": "playwright test" }, "dependencies": { "astro": "^7.0.3", @@ -20,5 +21,8 @@ "ember-source": "~7.1.0", "sharp": "^0.34.2", "vite": "^8.1.0" + }, + "devDependencies": { + "@playwright/test": "^1.61.1" } } diff --git a/docs/playwright.config.ts b/docs/playwright.config.ts new file mode 100644 index 0000000..c0a8c11 --- /dev/null +++ b/docs/playwright.config.ts @@ -0,0 +1,32 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * E2E tests run against a production build served by `astro preview`. + * This is the closest representation of what ships, and it exercises the + * ember-astro integration's build pipeline (babel + @embroider/vite) — so a + * regression that breaks client rendering of Ember components fails here. + */ +const PORT = 4321; + +export default defineConfig({ + testDir: "./e2e", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI + ? [["github"], ["html", { open: "never" }]] + : "list", + use: { + baseURL: `http://localhost:${PORT}`, + trace: "on-first-retry", + }, + projects: [ + { name: "chromium", use: { ...devices["Desktop Chrome"] } }, + ], + webServer: { + command: `pnpm build && pnpm preview --port ${PORT}`, + url: `http://localhost:${PORT}`, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 786cd44..6093238 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,6 +79,10 @@ importers: vite: specifier: ^8.1.0 version: 8.1.0(7451461716f88690ce6466d03b9ae456) + devDependencies: + '@playwright/test': + specifier: ^1.61.1 + version: 1.61.1 packages: @@ -1326,6 +1330,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} + engines: {node: '>=18'} + hasBin: true + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -2423,6 +2432,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3380,6 +3394,16 @@ packages: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} engines: {node: '>=8'} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + postcss@8.5.15: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} @@ -5756,6 +5780,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.61.1': + dependencies: + playwright: 1.61.1 + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -7168,6 +7196,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -8132,6 +8163,14 @@ snapshots: dependencies: find-up: 3.0.0 + playwright-core@1.61.1: {} + + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + postcss@8.5.15: dependencies: nanoid: 3.3.15