diff --git a/packages/astro/e2e/fixtures/preact/astro.config.mjs b/packages/astro/e2e/fixtures/preact/astro.config.mjs
new file mode 100644
index 0000000000..08916b1fea
--- /dev/null
+++ b/packages/astro/e2e/fixtures/preact/astro.config.mjs
@@ -0,0 +1,7 @@
+import { defineConfig } from 'astro/config';
+import preact from '@astrojs/preact';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [preact()],
+});
diff --git a/packages/astro/e2e/fixtures/preact/package.json b/packages/astro/e2e/fixtures/preact/package.json
new file mode 100644
index 0000000000..07bb822997
--- /dev/null
+++ b/packages/astro/e2e/fixtures/preact/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@e2e/react",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/preact": "workspace:*",
+ "astro": "workspace:*",
+ "react": "^18.1.0",
+ "react-dom": "^18.1.0"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/preact/src/components/Counter.css b/packages/astro/e2e/fixtures/preact/src/components/Counter.css
new file mode 100644
index 0000000000..fb21044d78
--- /dev/null
+++ b/packages/astro/e2e/fixtures/preact/src/components/Counter.css
@@ -0,0 +1,11 @@
+.counter {
+ display: grid;
+ font-size: 2em;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ margin-top: 2em;
+ place-items: center;
+}
+
+.counter-message {
+ text-align: center;
+}
diff --git a/packages/astro/e2e/fixtures/preact/src/components/Counter.jsx b/packages/astro/e2e/fixtures/preact/src/components/Counter.jsx
new file mode 100644
index 0000000000..526f269639
--- /dev/null
+++ b/packages/astro/e2e/fixtures/preact/src/components/Counter.jsx
@@ -0,0 +1,20 @@
+import { h, Fragment } from 'preact';
+import { useState } from 'preact/hooks';
+import './Counter.css';
+
+export default function Counter({ children, count: initialCount, id }) {
+ const [count, setCount] = useState(initialCount);
+ const add = () => setCount((i) => i + 1);
+ const subtract = () => setCount((i) => i - 1);
+
+ return (
+ <>
+
+ {children}
+ >
+ );
+}
diff --git a/packages/astro/e2e/fixtures/preact/src/components/JSXComponent.jsx b/packages/astro/e2e/fixtures/preact/src/components/JSXComponent.jsx
new file mode 100644
index 0000000000..6cc7b7858c
--- /dev/null
+++ b/packages/astro/e2e/fixtures/preact/src/components/JSXComponent.jsx
@@ -0,0 +1,5 @@
+import { h } from 'preact';
+
+export default function({ id }) {
+ return Preact client:only component
+}
diff --git a/packages/astro/e2e/fixtures/preact/src/pages/index.astro b/packages/astro/e2e/fixtures/preact/src/pages/index.astro
new file mode 100644
index 0000000000..e9c582ba5e
--- /dev/null
+++ b/packages/astro/e2e/fixtures/preact/src/pages/index.astro
@@ -0,0 +1,29 @@
+---
+import Counter from '../components/Counter.jsx';
+import PreactComponent from '../components/JSXComponent.jsx';
+
+const someProps = {
+ count: 0,
+};
+---
+
+
+
+
+
+
+
+ Hello, client:idle!
+
+
+
+ Hello, client:load!
+
+
+
+ Hello, client:visible!
+
+
+
+
+
diff --git a/packages/astro/e2e/preact.test.js b/packages/astro/e2e/preact.test.js
new file mode 100644
index 0000000000..c64f4feefa
--- /dev/null
+++ b/packages/astro/e2e/preact.test.js
@@ -0,0 +1,109 @@
+import { test as base, expect } from '@playwright/test';
+import { loadFixture, onAfterHMR } from './test-utils.js';
+
+const test = base.extend({
+ astro: async ({}, use) => {
+ const fixture = await loadFixture({ root: './fixtures/preact/' });
+ await use(fixture);
+ },
+});
+
+let devServer;
+
+test.beforeAll(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterAll(async ({ astro }) => {
+ await devServer.stop();
+});
+
+test.afterEach(async ({ astro }) => {
+ astro.clean();
+});
+
+test.only('Preact', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ await test.step('client:idle', async () => {
+ const counter = page.locator('#counter-idle');
+ await expect(counter).toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count).toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count).toHaveText('1');
+ });
+
+ await test.step('client:load', async () => {
+ const counter = page.locator('#counter-load');
+ await expect(counter).toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count).toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count).toHaveText('1');
+ });
+
+ await test.step('client:visible', async () => {
+ const counter = page.locator('#counter-visible');
+ await expect(counter).toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count).toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count).toHaveText('1');
+ });
+
+ await test.step('client:only', async () => {
+ const label = page.locator('#client-only');
+ await expect(label).toBeVisible();
+
+ await expect(label).toHaveText('Preact client:only component');
+ });
+
+ await test.step('HMR', async () => {
+ const afterHMR = onAfterHMR(page);
+
+ // test 1: updating the page component
+ await astro.writeFile(
+ 'src/pages/index.astro',
+ (original) => original.replace('id="counter-idle" {...someProps}', 'id="counter-idle" count={5}')
+ );
+
+ await afterHMR;
+
+ const count = page.locator('#counter-idle pre');
+ await expect(count).toHaveText('5');
+
+ // test 2: updating the react component
+ await astro.writeFile(
+ 'src/components/JSXComponent.jsx',
+ (original) => original.replace('Preact client:only component', 'Updated preact client:only component')
+ );
+
+ await afterHMR;
+
+ const label = page.locator('#client-only');
+ await expect(label).toBeVisible();
+
+ await expect(label).toHaveText('Updated preact client:only component');
+
+ // test 3: updating imported CSS
+ await astro.writeFile(
+ 'src/components/Counter.css',
+ (original) => original.replace('font-size: 2em;', 'font-size: 24px;')
+ );
+
+ await expect(count).toHaveCSS('font-size', '24px');
+ });
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7a1d78e6db..8fd816659c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -661,6 +661,18 @@ importers:
chai-as-promised: 7.1.1_chai@4.3.6
mocha: 9.2.2
+ packages/astro/e2e/fixtures/preact:
+ specifiers:
+ '@astrojs/preact': workspace:*
+ astro: workspace:*
+ react: ^18.1.0
+ react-dom: ^18.1.0
+ dependencies:
+ '@astrojs/preact': link:../../../../integrations/preact
+ astro: link:../../..
+ react: 18.1.0
+ react-dom: 18.1.0_react@18.1.0
+
packages/astro/e2e/fixtures/react:
specifiers:
'@astrojs/react': workspace:*