diff --git a/package.json b/package.json
index 9508c7b0b..b143a6f0a 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"test:smoke": "node scripts/smoke/index.js",
"test:vite-ci": "turbo run test --no-deps --scope=astro --concurrency=1",
"test:e2e": "cd packages/astro && pnpm run test:e2e",
+ "test:e2e:match": "cd packages/astro && pnpm run test:e2e:match",
"benchmark": "turbo run benchmark --scope=astro",
"lint": "eslint \"packages/**/*.ts\"",
"format": "prettier -w .",
diff --git a/packages/astro/e2e/fixtures/react/astro.config.mjs b/packages/astro/e2e/fixtures/react/astro.config.mjs
new file mode 100644
index 000000000..8a6f1951c
--- /dev/null
+++ b/packages/astro/e2e/fixtures/react/astro.config.mjs
@@ -0,0 +1,7 @@
+import { defineConfig } from 'astro/config';
+import react from '@astrojs/react';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [react()],
+});
diff --git a/packages/astro/e2e/fixtures/react/package.json b/packages/astro/e2e/fixtures/react/package.json
new file mode 100644
index 000000000..f76cc192d
--- /dev/null
+++ b/packages/astro/e2e/fixtures/react/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@e2e/react",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/react": "workspace:*",
+ "astro": "workspace:*",
+ "react": "^18.1.0",
+ "react-dom": "^18.1.0"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/react/src/components/Counter.css b/packages/astro/e2e/fixtures/react/src/components/Counter.css
new file mode 100644
index 000000000..fb21044d7
--- /dev/null
+++ b/packages/astro/e2e/fixtures/react/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/react/src/components/Counter.jsx b/packages/astro/e2e/fixtures/react/src/components/Counter.jsx
new file mode 100644
index 000000000..769e0cccf
--- /dev/null
+++ b/packages/astro/e2e/fixtures/react/src/components/Counter.jsx
@@ -0,0 +1,19 @@
+import React, { useState } from 'react';
+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/react/src/components/JSXComponent.jsx b/packages/astro/e2e/fixtures/react/src/components/JSXComponent.jsx
new file mode 100644
index 000000000..90a4d7c42
--- /dev/null
+++ b/packages/astro/e2e/fixtures/react/src/components/JSXComponent.jsx
@@ -0,0 +1,5 @@
+import React from 'react';
+
+export default function({ id }) {
+ return React client:only component
+}
diff --git a/packages/astro/e2e/fixtures/react/src/pages/index.astro b/packages/astro/e2e/fixtures/react/src/pages/index.astro
new file mode 100644
index 000000000..163618b13
--- /dev/null
+++ b/packages/astro/e2e/fixtures/react/src/pages/index.astro
@@ -0,0 +1,29 @@
+---
+import Counter from '../components/Counter.jsx';
+import ReactComponent from '../components/JSXComponent.jsx';
+
+const someProps = {
+ count: 0,
+};
+---
+
+
+
+
+
+
+
+ Hello, client:idle!
+
+
+
+ Hello, client:load!
+
+
+
+ Hello, client:visible!
+
+
+
+
+
diff --git a/packages/astro/e2e/react.test.js b/packages/astro/e2e/react.test.js
new file mode 100644
index 000000000..0d6bc225b
--- /dev/null
+++ b/packages/astro/e2e/react.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/react/' });
+ 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('React', 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('React 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('React client:only component', 'Updated react client:only component')
+ );
+
+ await afterHMR;
+
+ const label = page.locator('#client-only');
+ await expect(label).toBeVisible();
+
+ await expect(label).toHaveText('Updated react 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/packages/astro/package.json b/packages/astro/package.json
index c4d266a5d..bffad5663 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -73,7 +73,8 @@
"benchmark": "node test/benchmark/dev.bench.js && node test/benchmark/build.bench.js",
"test": "mocha --exit --timeout 20000 --ignore **/lit-element.test.js && mocha --timeout 20000 **/lit-element.test.js",
"test:match": "mocha --timeout 20000 -g",
- "test:e2e": "playwright test e2e"
+ "test:e2e": "playwright test e2e",
+ "test:e2e:match": "playwright test e2e -g"
},
"dependencies": {
"@astrojs/compiler": "^0.14.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 497caf730..7a1d78e6d 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/react:
+ specifiers:
+ '@astrojs/react': workspace:*
+ astro: workspace:*
+ react: ^18.1.0
+ react-dom: ^18.1.0
+ dependencies:
+ '@astrojs/react': link:../../../../integrations/react
+ astro: link:../../..
+ react: 18.1.0
+ react-dom: 18.1.0_react@18.1.0
+
packages/astro/e2e/fixtures/tailwindcss:
specifiers:
'@astrojs/tailwind': workspace:*