summaryrefslogtreecommitdiff
path: root/packages/astro
diff options
context:
space:
mode:
Diffstat (limited to 'packages/astro')
-rw-r--r--packages/astro/e2e/astro-component.test.js42
-rw-r--r--packages/astro/e2e/fixtures/astro-component/astro.config.mjs4
-rw-r--r--packages/astro/e2e/fixtures/astro-component/package.json8
-rw-r--r--packages/astro/e2e/fixtures/astro-component/src/components/Counter.css11
-rw-r--r--packages/astro/e2e/fixtures/astro-component/src/components/Counter.jsx20
-rw-r--r--packages/astro/e2e/fixtures/astro-component/src/components/Hero.astro25
-rw-r--r--packages/astro/e2e/fixtures/astro-component/src/components/JSXComponent.jsx5
-rw-r--r--packages/astro/e2e/fixtures/astro-component/src/pages/index.astro16
-rw-r--r--packages/astro/e2e/fixtures/lit-component/astro.config.mjs7
-rw-r--r--packages/astro/e2e/fixtures/lit-component/package.json11
-rw-r--r--packages/astro/e2e/fixtures/lit-component/src/components/Counter.js36
-rw-r--r--packages/astro/e2e/fixtures/lit-component/src/pages/index.astro26
-rw-r--r--packages/astro/e2e/fixtures/lit-component/src/pages/media.astro18
-rw-r--r--packages/astro/e2e/fixtures/multiple-frameworks/astro.config.mjs12
-rw-r--r--packages/astro/e2e/fixtures/multiple-frameworks/package.json24
-rw-r--r--packages/astro/e2e/fixtures/multiple-frameworks/src/components/A.astro7
-rw-r--r--packages/astro/e2e/fixtures/multiple-frameworks/src/components/B.astro7
-rw-r--r--packages/astro/e2e/fixtures/multiple-frameworks/src/components/LitCounter.js33
-rw-r--r--packages/astro/e2e/fixtures/multiple-frameworks/src/components/PreactCounter.tsx19
-rw-r--r--packages/astro/e2e/fixtures/multiple-frameworks/src/components/ReactCounter.jsx19
-rw-r--r--packages/astro/e2e/fixtures/multiple-frameworks/src/components/SolidCounter.tsx19
-rw-r--r--packages/astro/e2e/fixtures/multiple-frameworks/src/components/SvelteCounter.svelte29
-rw-r--r--packages/astro/e2e/fixtures/multiple-frameworks/src/components/VueCounter.vue34
-rw-r--r--packages/astro/e2e/fixtures/multiple-frameworks/src/components/index.ts2
-rw-r--r--packages/astro/e2e/fixtures/multiple-frameworks/src/pages/index.astro50
-rw-r--r--packages/astro/e2e/fixtures/multiple-frameworks/src/styles/global.css21
-rw-r--r--packages/astro/e2e/fixtures/preact-component/astro.config.mjs7
-rw-r--r--packages/astro/e2e/fixtures/preact-component/package.json10
-rw-r--r--packages/astro/e2e/fixtures/preact-component/src/components/Counter.css11
-rw-r--r--packages/astro/e2e/fixtures/preact-component/src/components/Counter.jsx20
-rw-r--r--packages/astro/e2e/fixtures/preact-component/src/components/JSXComponent.jsx5
-rw-r--r--packages/astro/e2e/fixtures/preact-component/src/pages/index.astro37
-rw-r--r--packages/astro/e2e/fixtures/react-component/astro.config.mjs7
-rw-r--r--packages/astro/e2e/fixtures/react-component/package.json11
-rw-r--r--packages/astro/e2e/fixtures/react-component/src/components/Counter.css11
-rw-r--r--packages/astro/e2e/fixtures/react-component/src/components/Counter.jsx19
-rw-r--r--packages/astro/e2e/fixtures/react-component/src/components/JSXComponent.jsx5
-rw-r--r--packages/astro/e2e/fixtures/react-component/src/pages/index.astro37
-rw-r--r--packages/astro/e2e/fixtures/solid-component/astro.config.mjs7
-rw-r--r--packages/astro/e2e/fixtures/solid-component/package.json12
-rw-r--r--packages/astro/e2e/fixtures/solid-component/src/components/Counter.css11
-rw-r--r--packages/astro/e2e/fixtures/solid-component/src/components/Counter.jsx19
-rw-r--r--packages/astro/e2e/fixtures/solid-component/src/pages/index.astro34
-rw-r--r--packages/astro/e2e/fixtures/svelte-component/astro.config.mjs7
-rw-r--r--packages/astro/e2e/fixtures/svelte-component/package.json10
-rw-r--r--packages/astro/e2e/fixtures/svelte-component/src/components/Counter.svelte35
-rw-r--r--packages/astro/e2e/fixtures/svelte-component/src/pages/index.astro34
-rw-r--r--packages/astro/e2e/fixtures/tailwindcss/package.json2
-rw-r--r--packages/astro/e2e/fixtures/vue-component/astro.config.mjs13
-rw-r--r--packages/astro/e2e/fixtures/vue-component/package.json9
-rw-r--r--packages/astro/e2e/fixtures/vue-component/src/components/Counter.vue47
-rw-r--r--packages/astro/e2e/fixtures/vue-component/src/components/Result.vue16
-rw-r--r--packages/astro/e2e/fixtures/vue-component/src/pages/index.astro33
-rw-r--r--packages/astro/e2e/lit-component.test.js103
-rw-r--r--packages/astro/e2e/multiple-frameworks.test.js141
-rw-r--r--packages/astro/e2e/nested-styles.test.js10
-rw-r--r--packages/astro/e2e/preact-component.test.js143
-rw-r--r--packages/astro/e2e/react-component.test.js143
-rw-r--r--packages/astro/e2e/solid-component.test.js123
-rw-r--r--packages/astro/e2e/svelte-component.test.js114
-rw-r--r--packages/astro/e2e/tailwindcss.test.js31
-rw-r--r--packages/astro/e2e/vue-component.test.js144
-rw-r--r--packages/astro/package.json3
-rw-r--r--packages/astro/playwright.config.js42
-rw-r--r--packages/astro/src/runtime/client/hmr.ts2
-rw-r--r--packages/astro/test/errors.test.js2
-rw-r--r--packages/astro/test/test-utils.js28
67 files changed, 1979 insertions, 24 deletions
diff --git a/packages/astro/e2e/astro-component.test.js b/packages/astro/e2e/astro-component.test.js
new file mode 100644
index 000000000..65499499f
--- /dev/null
+++ b/packages/astro/e2e/astro-component.test.js
@@ -0,0 +1,42 @@
+import { test as base, expect } from '@playwright/test';
+import { loadFixture } from './test-utils.js';
+
+const test = base.extend({
+ astro: async ({}, use) => {
+ const fixture = await loadFixture({ root: './fixtures/astro-component/' });
+ await use(fixture);
+ },
+});
+
+let devServer;
+
+test.beforeEach(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterEach(async () => {
+ await devServer.stop();
+});
+
+test.describe('Astro components', () => {
+ test('HMR', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const hero = page.locator('section');
+ await expect(hero, 'hero has background: white').toHaveCSS(
+ 'background-color',
+ 'rgb(255, 255, 255)'
+ );
+ await expect(hero, 'hero has color: black').toHaveCSS('color', 'rgb(0, 0, 0)');
+
+ // Edit the Hero component with a new background color
+ await astro.editFile('./src/components/Hero.astro', (content) =>
+ content.replace('background: white', 'background: rgb(230, 230, 230)')
+ );
+
+ await expect(hero, 'background color updated').toHaveCSS(
+ 'background-color',
+ 'rgb(230, 230, 230)'
+ );
+ });
+});
diff --git a/packages/astro/e2e/fixtures/astro-component/astro.config.mjs b/packages/astro/e2e/fixtures/astro-component/astro.config.mjs
new file mode 100644
index 000000000..882e6515a
--- /dev/null
+++ b/packages/astro/e2e/fixtures/astro-component/astro.config.mjs
@@ -0,0 +1,4 @@
+import { defineConfig } from 'astro/config';
+
+// https://astro.build/config
+export default defineConfig({});
diff --git a/packages/astro/e2e/fixtures/astro-component/package.json b/packages/astro/e2e/fixtures/astro-component/package.json
new file mode 100644
index 000000000..33a377834
--- /dev/null
+++ b/packages/astro/e2e/fixtures/astro-component/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@e2e/astro-component",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/astro-component/src/components/Counter.css b/packages/astro/e2e/fixtures/astro-component/src/components/Counter.css
new file mode 100644
index 000000000..fb21044d7
--- /dev/null
+++ b/packages/astro/e2e/fixtures/astro-component/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/astro-component/src/components/Counter.jsx b/packages/astro/e2e/fixtures/astro-component/src/components/Counter.jsx
new file mode 100644
index 000000000..526f26963
--- /dev/null
+++ b/packages/astro/e2e/fixtures/astro-component/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 (
+ <>
+ <div id={id} className="counter">
+ <button className="decrement" onClick={subtract}>-</button>
+ <pre>{count}</pre>
+ <button className="increment" onClick={add}>+</button>
+ </div>
+ <div className="counter-message">{children}</div>
+ </>
+ );
+}
diff --git a/packages/astro/e2e/fixtures/astro-component/src/components/Hero.astro b/packages/astro/e2e/fixtures/astro-component/src/components/Hero.astro
new file mode 100644
index 000000000..680a5f639
--- /dev/null
+++ b/packages/astro/e2e/fixtures/astro-component/src/components/Hero.astro
@@ -0,0 +1,25 @@
+---
+export interface Props {
+ title: string;
+}
+
+const { title } = Astro.props as Props;
+---
+
+<section>
+ <h1>{title}</h1>
+ <slot />
+</section>
+
+<style>
+section {
+ width: 100%;
+ height: 80vh;
+ background: white;
+ color: black;
+}
+
+h1 {
+ text-align: center;
+}
+</style>
diff --git a/packages/astro/e2e/fixtures/astro-component/src/components/JSXComponent.jsx b/packages/astro/e2e/fixtures/astro-component/src/components/JSXComponent.jsx
new file mode 100644
index 000000000..6cc7b7858
--- /dev/null
+++ b/packages/astro/e2e/fixtures/astro-component/src/components/JSXComponent.jsx
@@ -0,0 +1,5 @@
+import { h } from 'preact';
+
+export default function({ id }) {
+ return <div id={id}>Preact client:only component</div>
+}
diff --git a/packages/astro/e2e/fixtures/astro-component/src/pages/index.astro b/packages/astro/e2e/fixtures/astro-component/src/pages/index.astro
new file mode 100644
index 000000000..a52ee713f
--- /dev/null
+++ b/packages/astro/e2e/fixtures/astro-component/src/pages/index.astro
@@ -0,0 +1,16 @@
+---
+import Hero from '../components/Hero.astro';
+---
+
+<html>
+ <head>
+ <!-- Head Stuff -->
+ </head>
+ <body>
+ <main>
+ <Hero title="Astro Components">
+ Lorem ipsum, dolor sit amet consectetur adipisicing elit.
+ </Hero>
+ </main>
+ </body>
+</html>
diff --git a/packages/astro/e2e/fixtures/lit-component/astro.config.mjs b/packages/astro/e2e/fixtures/lit-component/astro.config.mjs
new file mode 100644
index 000000000..1eab8f9ab
--- /dev/null
+++ b/packages/astro/e2e/fixtures/lit-component/astro.config.mjs
@@ -0,0 +1,7 @@
+import { defineConfig } from 'astro/config';
+import lit from '@astrojs/lit';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [lit()],
+});
diff --git a/packages/astro/e2e/fixtures/lit-component/package.json b/packages/astro/e2e/fixtures/lit-component/package.json
new file mode 100644
index 000000000..ea73f5982
--- /dev/null
+++ b/packages/astro/e2e/fixtures/lit-component/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@e2e/lit-component",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/lit": "workspace:*",
+ "astro": "workspace:*",
+ "@webcomponents/template-shadowroot": "^0.1.0",
+ "lit": "^2.2.3"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/lit-component/src/components/Counter.js b/packages/astro/e2e/fixtures/lit-component/src/components/Counter.js
new file mode 100644
index 000000000..3316a7342
--- /dev/null
+++ b/packages/astro/e2e/fixtures/lit-component/src/components/Counter.js
@@ -0,0 +1,36 @@
+import { LitElement, html } from 'lit';
+
+export const tagName = 'my-counter';
+
+class Counter extends LitElement {
+ static get properties() {
+ return {
+ count: {
+ type: Number,
+ },
+ };
+ }
+
+ constructor() {
+ super();
+ this.count = 0;
+ }
+
+ increment() {
+ this.count++;
+ }
+
+ render() {
+ return html`
+ <div>
+ <p>Count: ${this.count}</p>
+
+ <button type="button" @click=${this.increment}>Increment</button>
+
+ <slot />
+ </div>
+ `;
+ }
+}
+
+customElements.define(tagName, Counter);
diff --git a/packages/astro/e2e/fixtures/lit-component/src/pages/index.astro b/packages/astro/e2e/fixtures/lit-component/src/pages/index.astro
new file mode 100644
index 000000000..48eb7d2f9
--- /dev/null
+++ b/packages/astro/e2e/fixtures/lit-component/src/pages/index.astro
@@ -0,0 +1,26 @@
+---
+import '../components/Counter.js';
+
+const someProps = {
+ count: 0,
+};
+---
+
+<html>
+ <head>
+ <!-- Head Stuff -->
+ </head>
+ <body>
+ <my-counter id="client-idle" {...someProps} client:idle>
+ <h1>Hello, client:idle!</h1>
+ </my-counter>
+
+ <my-counter id="client-load" {...someProps} client:load>
+ <h1>Hello, client:load!</h1>
+ </my-counter>
+
+ <my-counter id="client-visible" {...someProps} client:visible>
+ <h1>Hello, client:visible!</h1>
+ </my-counter>
+ </body>
+</html>
diff --git a/packages/astro/e2e/fixtures/lit-component/src/pages/media.astro b/packages/astro/e2e/fixtures/lit-component/src/pages/media.astro
new file mode 100644
index 000000000..e54cec071
--- /dev/null
+++ b/packages/astro/e2e/fixtures/lit-component/src/pages/media.astro
@@ -0,0 +1,18 @@
+---
+import '../components/Counter.js';
+
+const someProps = {
+ count: 0,
+};
+---
+
+<html>
+ <head>
+ <!-- Head Stuff -->
+ </head>
+ <body>
+ <my-counter id="client-media" {...someProps} client:media="(max-width: 50em)">
+ <h1>Hello, client:media!</h1>
+ </my-counter>
+ </body>
+</html>
diff --git a/packages/astro/e2e/fixtures/multiple-frameworks/astro.config.mjs b/packages/astro/e2e/fixtures/multiple-frameworks/astro.config.mjs
new file mode 100644
index 000000000..4b50887cd
--- /dev/null
+++ b/packages/astro/e2e/fixtures/multiple-frameworks/astro.config.mjs
@@ -0,0 +1,12 @@
+import { defineConfig } from 'astro/config';
+import preact from '@astrojs/preact';
+import react from '@astrojs/react';
+import svelte from '@astrojs/svelte';
+import vue from '@astrojs/vue';
+import solid from '@astrojs/solid-js';
+
+// https://astro.build/config
+export default defineConfig({
+ // Enable many frameworks to support all different kinds of components.
+ integrations: [preact(), react(), svelte(), vue(), solid()],
+});
diff --git a/packages/astro/e2e/fixtures/multiple-frameworks/package.json b/packages/astro/e2e/fixtures/multiple-frameworks/package.json
new file mode 100644
index 000000000..ab0dd06bc
--- /dev/null
+++ b/packages/astro/e2e/fixtures/multiple-frameworks/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@e2e/multiple-frameworks",
+ "version": "0.0.0",
+ "private": true,
+ "devDependencies": {
+ "@astrojs/lit": "^0.1.3",
+ "@astrojs/preact": "^0.1.2",
+ "@astrojs/react": "^0.1.2",
+ "@astrojs/solid-js": "^0.1.2",
+ "@astrojs/svelte": "^0.1.3",
+ "@astrojs/vue": "^0.1.4",
+ "astro": "^1.0.0-beta.31"
+ },
+ "dependencies": {
+ "@webcomponents/template-shadowroot": "^0.1.0",
+ "lit": "^2.2.4",
+ "preact": "^10.7.2",
+ "react": "^18.1.0",
+ "react-dom": "^18.1.0",
+ "solid-js": "^1.4.2",
+ "svelte": "^3.48.0",
+ "vue": "^3.2.34"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/multiple-frameworks/src/components/A.astro b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/A.astro
new file mode 100644
index 000000000..b2c223a29
--- /dev/null
+++ b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/A.astro
@@ -0,0 +1,7 @@
+---
+const { id } = Astro.props
+---
+
+<div id={id} class="children">
+ <h1>Hello Astro (A)</h1>
+</div>
diff --git a/packages/astro/e2e/fixtures/multiple-frameworks/src/components/B.astro b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/B.astro
new file mode 100644
index 000000000..339f63735
--- /dev/null
+++ b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/B.astro
@@ -0,0 +1,7 @@
+---
+const { id } = Astro.props
+---
+
+<div id={id} class="children">
+ <h1>Hello Astro (B)</h1>
+</div>
diff --git a/packages/astro/e2e/fixtures/multiple-frameworks/src/components/LitCounter.js b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/LitCounter.js
new file mode 100644
index 000000000..883a7581d
--- /dev/null
+++ b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/LitCounter.js
@@ -0,0 +1,33 @@
+import { LitElement, html } from 'lit';
+
+export const tagName = 'my-counter';
+
+class Counter extends LitElement {
+ static get properties() {
+ return {
+ count: {
+ type: Number,
+ },
+ };
+ }
+
+ constructor() {
+ super();
+ this.count = 0;
+ }
+
+ increment() {
+ this.count++;
+ }
+
+ render() {
+ return html`
+ <div>
+ <p>Count: ${this.count}</p>
+ <button type="button" @click=${this.increment}>Increment</button>
+ </div>
+ `;
+ }
+}
+
+customElements.define(tagName, Counter);
diff --git a/packages/astro/e2e/fixtures/multiple-frameworks/src/components/PreactCounter.tsx b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/PreactCounter.tsx
new file mode 100644
index 000000000..af2258fdf
--- /dev/null
+++ b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/PreactCounter.tsx
@@ -0,0 +1,19 @@
+import { useState } from 'preact/hooks';
+
+/** a counter written in Preact */
+export function PreactCounter({ children, id }) {
+ const [count, setCount] = useState(0);
+ const add = () => setCount((i) => i + 1);
+ const subtract = () => setCount((i) => i - 1);
+
+ return (
+ <>
+ <div id={id} class="counter">
+ <button class="decrement" onClick={subtract}>-</button>
+ <pre>{count}</pre>
+ <button class="increment" onClick={add}>+</button>
+ </div>
+ <div class="counter-message">{children}</div>
+ </>
+ );
+}
diff --git a/packages/astro/e2e/fixtures/multiple-frameworks/src/components/ReactCounter.jsx b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/ReactCounter.jsx
new file mode 100644
index 000000000..02eb19539
--- /dev/null
+++ b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/ReactCounter.jsx
@@ -0,0 +1,19 @@
+import { useState } from 'react';
+
+/** a counter written in React */
+export function Counter({ children, id }) {
+ const [count, setCount] = useState(0);
+ const add = () => setCount((i) => i + 1);
+ const subtract = () => setCount((i) => i - 1);
+
+ return (
+ <>
+ <div id={id} className="counter">
+ <button className="decrement" onClick={subtract}>-</button>
+ <pre>{count}</pre>
+ <button className="increment" onClick={add}>+</button>
+ </div>
+ <div className="counter-message">{children}</div>
+ </>
+ );
+}
diff --git a/packages/astro/e2e/fixtures/multiple-frameworks/src/components/SolidCounter.tsx b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/SolidCounter.tsx
new file mode 100644
index 000000000..689c5222c
--- /dev/null
+++ b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/SolidCounter.tsx
@@ -0,0 +1,19 @@
+import { createSignal } from 'solid-js';
+
+/** a counter written with Solid */
+export default function SolidCounter({ children, id }) {
+ const [count, setCount] = createSignal(0);
+ const add = () => setCount(count() + 1);
+ const subtract = () => setCount(count() - 1);
+
+ return (
+ <>
+ <div id={id} class="counter">
+ <button class="decrement" onClick={subtract}>-</button>
+ <pre>{count()}</pre>
+ <button class="increment" onClick={add}>+</button>
+ </div>
+ <div class="counter-message">{children}</div>
+ </>
+ );
+}
diff --git a/packages/astro/e2e/fixtures/multiple-frameworks/src/components/SvelteCounter.svelte b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/SvelteCounter.svelte
new file mode 100644
index 000000000..ab13b9c71
--- /dev/null
+++ b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/SvelteCounter.svelte
@@ -0,0 +1,29 @@
+
+<script>
+ export let id;
+ let children;
+ let count = 0;
+
+ function add() {
+ count += 1;
+ }
+
+ function subtract() {
+ count -= 1;
+ }
+</script>
+
+<div {id} class="counter">
+ <button class="decrement" on:click={subtract}>-</button>
+ <pre>{ count }</pre>
+ <button class="increment" on:click={add}>+</button>
+</div>
+<div class="counter-message">
+ <slot />
+</div>
+
+<style>
+ .counter {
+ background: white;
+ }
+</style>
diff --git a/packages/astro/e2e/fixtures/multiple-frameworks/src/components/VueCounter.vue b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/VueCounter.vue
new file mode 100644
index 000000000..4861511c8
--- /dev/null
+++ b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/VueCounter.vue
@@ -0,0 +1,34 @@
+<template>
+ <div :id="id" class="counter">
+ <button class="decrement" @click="subtract()">-</button>
+ <pre>{{ count }}</pre>
+ <button class="increment" @click="add()">+</button>
+ </div>
+ <div class="counter-message">
+ <slot />
+ </div>
+</template>
+
+<script>
+import { ref } from 'vue';
+export default {
+ props: {
+ id: {
+ type: String,
+ required: true
+ }
+ },
+ setup(props) {
+ const count = ref(0);
+ const add = () => (count.value = count.value + 1);
+ const subtract = () => (count.value = count.value - 1);
+
+ return {
+ id: props.id,
+ count,
+ add,
+ subtract,
+ };
+ },
+};
+</script>
diff --git a/packages/astro/e2e/fixtures/multiple-frameworks/src/components/index.ts b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/index.ts
new file mode 100644
index 000000000..4077dcacd
--- /dev/null
+++ b/packages/astro/e2e/fixtures/multiple-frameworks/src/components/index.ts
@@ -0,0 +1,2 @@
+export { default as A } from './A.astro';
+export { default as B } from './B.astro';
diff --git a/packages/astro/e2e/fixtures/multiple-frameworks/src/pages/index.astro b/packages/astro/e2e/fixtures/multiple-frameworks/src/pages/index.astro
new file mode 100644
index 000000000..a30688bca
--- /dev/null
+++ b/packages/astro/e2e/fixtures/multiple-frameworks/src/pages/index.astro
@@ -0,0 +1,50 @@
+---
+// Style Imports
+import '../styles/global.css';
+// Component Imports
+import { A, B as Renamed } from '../components';
+import * as react from '../components/ReactCounter.jsx';
+import { PreactCounter } from '../components/PreactCounter.tsx';
+import SolidCounter from '../components/SolidCounter.tsx';
+import VueCounter from '../components/VueCounter.vue';
+import SvelteCounter from '../components/SvelteCounter.svelte';
+
+// Full Astro Component Syntax:
+// https://docs.astro.build/core-concepts/astro-components/
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ <link rel="icon" type="image/x-icon" href="/favicon.ico" />
+ </head>
+ <body>
+ <main>
+ <react.Counter id="react-counter" client:idle>
+ <h1>Hello React!</h1>
+ <p>What's up?</p>
+ </react.Counter>
+
+ <PreactCounter id="preact-counter" client:idle>
+ <h1>Hello Preact!</h1>
+ </PreactCounter>
+
+ <SolidCounter id="solid-counter" client:idle>
+ <h1>Hello Solid!</h1>
+ </SolidCounter>
+
+ <VueCounter id="vue-counter" client:idle>
+ <h1>Hello Vue!</h1>
+ </VueCounter>
+
+ <SvelteCounter id="svelte-counter" client:idle>
+ <h1>Hello Svelte!</h1>
+ </SvelteCounter>
+
+ <A id="astro-a" />
+
+ <Renamed id="astro-b" />
+ </main>
+ </body>
+</html>
diff --git a/packages/astro/e2e/fixtures/multiple-frameworks/src/styles/global.css b/packages/astro/e2e/fixtures/multiple-frameworks/src/styles/global.css
new file mode 100644
index 000000000..4912b4c39
--- /dev/null
+++ b/packages/astro/e2e/fixtures/multiple-frameworks/src/styles/global.css
@@ -0,0 +1,21 @@
+html,
+body {
+ font-family: system-ui;
+ margin: 0;
+}
+
+body {
+ padding: 2rem;
+}
+
+.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-component/astro.config.mjs b/packages/astro/e2e/fixtures/preact-component/astro.config.mjs
new file mode 100644
index 000000000..08916b1fe
--- /dev/null
+++ b/packages/astro/e2e/fixtures/preact-component/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-component/package.json b/packages/astro/e2e/fixtures/preact-component/package.json
new file mode 100644
index 000000000..8ee5c3a0a
--- /dev/null
+++ b/packages/astro/e2e/fixtures/preact-component/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@e2e/preact-component",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/preact": "workspace:*",
+ "astro": "workspace:*",
+ "preact": "^10.7.2"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/preact-component/src/components/Counter.css b/packages/astro/e2e/fixtures/preact-component/src/components/Counter.css
new file mode 100644
index 000000000..fb21044d7
--- /dev/null
+++ b/packages/astro/e2e/fixtures/preact-component/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-component/src/components/Counter.jsx b/packages/astro/e2e/fixtures/preact-component/src/components/Counter.jsx
new file mode 100644
index 000000000..526f26963
--- /dev/null
+++ b/packages/astro/e2e/fixtures/preact-component/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 (
+ <>
+ <div id={id} className="counter">
+ <button className="decrement" onClick={subtract}>-</button>
+ <pre>{count}</pre>
+ <button className="increment" onClick={add}>+</button>
+ </div>
+ <div className="counter-message">{children}</div>
+ </>
+ );
+}
diff --git a/packages/astro/e2e/fixtures/preact-component/src/components/JSXComponent.jsx b/packages/astro/e2e/fixtures/preact-component/src/components/JSXComponent.jsx
new file mode 100644
index 000000000..6cc7b7858
--- /dev/null
+++ b/packages/astro/e2e/fixtures/preact-component/src/components/JSXComponent.jsx
@@ -0,0 +1,5 @@
+import { h } from 'preact';
+
+export default function({ id }) {
+ return <div id={id}>Preact client:only component</div>
+}
diff --git a/packages/astro/e2e/fixtures/preact-component/src/pages/index.astro b/packages/astro/e2e/fixtures/preact-component/src/pages/index.astro
new file mode 100644
index 000000000..946b90be0
--- /dev/null
+++ b/packages/astro/e2e/fixtures/preact-component/src/pages/index.astro
@@ -0,0 +1,37 @@
+---
+import Counter from '../components/Counter.jsx';
+import PreactComponent from '../components/JSXComponent.jsx';
+
+const someProps = {
+ count: 0,
+};
+---
+
+<html>
+ <head>
+ <!-- Head Stuff -->
+ </head>
+ <body>
+ <Counter id="server-only" {...someProps}>
+ <h1>Hello, server!</h1>
+ </Counter>
+
+ <Counter id="client-idle" {...someProps} client:idle>
+ <h1>Hello, client:idle!</h1>
+ </Counter>
+
+ <Counter id="client-load" {...someProps} client:load>
+ <h1>Hello, client:load!</h1>
+ </Counter>
+
+ <Counter id="client-visible" {...someProps} client:visible>
+ <h1>Hello, client:visible!</h1>
+ </Counter>
+
+ <Counter id="client-media" {...someProps} client:media="(max-width: 50em)">
+ <h1>Hello, client:media!</h1>
+ </Counter>
+
+ <PreactComponent id="client-only" client:only="preact" />
+ </body>
+</html>
diff --git a/packages/astro/e2e/fixtures/react-component/astro.config.mjs b/packages/astro/e2e/fixtures/react-component/astro.config.mjs
new file mode 100644
index 000000000..8a6f1951c
--- /dev/null
+++ b/packages/astro/e2e/fixtures/react-component/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-component/package.json b/packages/astro/e2e/fixtures/react-component/package.json
new file mode 100644
index 000000000..9236558f0
--- /dev/null
+++ b/packages/astro/e2e/fixtures/react-component/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@e2e/react-component",
+ "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-component/src/components/Counter.css b/packages/astro/e2e/fixtures/react-component/src/components/Counter.css
new file mode 100644
index 000000000..fb21044d7
--- /dev/null
+++ b/packages/astro/e2e/fixtures/react-component/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-component/src/components/Counter.jsx b/packages/astro/e2e/fixtures/react-component/src/components/Counter.jsx
new file mode 100644
index 000000000..769e0cccf
--- /dev/null
+++ b/packages/astro/e2e/fixtures/react-component/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 (
+ <>
+ <div id={id} className="counter">
+ <button className="decrement" onClick={subtract}>-</button>
+ <pre>{count}</pre>
+ <button className="increment" onClick={add}>+</button>
+ </div>
+ <div className="counter-message">{children}</div>
+ </>
+ );
+}
diff --git a/packages/astro/e2e/fixtures/react-component/src/components/JSXComponent.jsx b/packages/astro/e2e/fixtures/react-component/src/components/JSXComponent.jsx
new file mode 100644
index 000000000..90a4d7c42
--- /dev/null
+++ b/packages/astro/e2e/fixtures/react-component/src/components/JSXComponent.jsx
@@ -0,0 +1,5 @@
+import React from 'react';
+
+export default function({ id }) {
+ return <div id={id}>React client:only component</div>
+}
diff --git a/packages/astro/e2e/fixtures/react-component/src/pages/index.astro b/packages/astro/e2e/fixtures/react-component/src/pages/index.astro
new file mode 100644
index 000000000..388fc1d98
--- /dev/null
+++ b/packages/astro/e2e/fixtures/react-component/src/pages/index.astro
@@ -0,0 +1,37 @@
+---
+import Counter from '../components/Counter.jsx';
+import ReactComponent from '../components/JSXComponent.jsx';
+
+const someProps = {
+ count: 0,
+};
+---
+
+<html>
+ <head>
+ <!-- Head Stuff -->
+ </head>
+ <body>
+ <Counter id="server-only" {...someProps}>
+ <h1>Hello, server!</h1>
+ </Counter>
+
+ <Counter id="client-idle" {...someProps} client:idle>
+ <h1>Hello, client:idle!</h1>
+ </Counter>
+
+ <Counter id="client-load" {...someProps} client:load>
+ <h1>Hello, client:load!</h1>
+ </Counter>
+
+ <Counter id="client-visible" {...someProps} client:visible>
+ <h1>Hello, client:visible!</h1>
+ </Counter>
+
+ <Counter id="client-media" {...someProps} client:media="(max-width: 50em)">
+ <h1>Hello, client:media!</h1>
+ </Counter>
+
+ <ReactComponent id="client-only" client:only="react" />
+ </body>
+</html>
diff --git a/packages/astro/e2e/fixtures/solid-component/astro.config.mjs b/packages/astro/e2e/fixtures/solid-component/astro.config.mjs
new file mode 100644
index 000000000..a6c39b853
--- /dev/null
+++ b/packages/astro/e2e/fixtures/solid-component/astro.config.mjs
@@ -0,0 +1,7 @@
+import { defineConfig } from 'astro/config';
+import solid from '@astrojs/solid-js';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [solid()],
+});
diff --git a/packages/astro/e2e/fixtures/solid-component/package.json b/packages/astro/e2e/fixtures/solid-component/package.json
new file mode 100644
index 000000000..5973f12fa
--- /dev/null
+++ b/packages/astro/e2e/fixtures/solid-component/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@e2e/solid-component",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/solid-js": "workspace:*",
+ "astro": "workspace:*"
+ },
+ "devDependencies": {
+ "solid-js": "^1.4.1"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/solid-component/src/components/Counter.css b/packages/astro/e2e/fixtures/solid-component/src/components/Counter.css
new file mode 100644
index 000000000..cffdbda7b
--- /dev/null
+++ b/packages/astro/e2e/fixtures/solid-component/src/components/Counter.css
@@ -0,0 +1,11 @@
+.counter {
+ display: grid;
+ font-size: 2em;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ margin-top: 3em;
+ place-items: center;
+}
+
+.counter-message {
+ text-align: center;
+}
diff --git a/packages/astro/e2e/fixtures/solid-component/src/components/Counter.jsx b/packages/astro/e2e/fixtures/solid-component/src/components/Counter.jsx
new file mode 100644
index 000000000..e315033d3
--- /dev/null
+++ b/packages/astro/e2e/fixtures/solid-component/src/components/Counter.jsx
@@ -0,0 +1,19 @@
+import { createSignal } from 'solid-js';
+import './Counter.css';
+
+export default function Counter({ children, id, count: initialCount = 0 }) {
+ const [count, setCount] = createSignal(initialCount);
+ const add = () => setCount(count() + 1);
+ const subtract = () => setCount(count() - 1);
+
+ return (
+ <>
+ <div id={id} class="counter">
+ <button class="decrement" onClick={subtract}>-</button>
+ <pre>{count()}</pre>
+ <button class="increment" onClick={add}>+</button>
+ </div>
+ <div class="counter-message">{children}</div>
+ </>
+ );
+}
diff --git a/packages/astro/e2e/fixtures/solid-component/src/pages/index.astro b/packages/astro/e2e/fixtures/solid-component/src/pages/index.astro
new file mode 100644
index 000000000..91013ad0e
--- /dev/null
+++ b/packages/astro/e2e/fixtures/solid-component/src/pages/index.astro
@@ -0,0 +1,34 @@
+---
+import Counter from '../components/Counter.jsx';
+
+const someProps = {
+ count: 0,
+};
+---
+
+<html>
+ <head>
+ <!-- Head Stuff -->
+ </head>
+ <body>
+ <Counter id="server-only" {...someProps}>
+ <h1>Hello, server!</h1>
+ </Counter>
+
+ <Counter id="client-idle" {...someProps} client:idle>
+ <h1>Hello, client:idle!</h1>
+ </Counter>
+
+ <Counter id="client-load" {...someProps} client:load>
+ <h1>Hello, client:load!</h1>
+ </Counter>
+
+ <Counter id="client-visible" {...someProps} client:visible>
+ <h1>Hello, client:visible!</h1>
+ </Counter>
+
+ <Counter id="client-media" {...someProps} client:media="(max-width: 50em)">
+ <h1>Hello, client:media!</h1>
+ </Counter>
+ </body>
+</html>
diff --git a/packages/astro/e2e/fixtures/svelte-component/astro.config.mjs b/packages/astro/e2e/fixtures/svelte-component/astro.config.mjs
new file mode 100644
index 000000000..77fdcd1b9
--- /dev/null
+++ b/packages/astro/e2e/fixtures/svelte-component/astro.config.mjs
@@ -0,0 +1,7 @@
+import { defineConfig } from 'astro/config';
+import svelte from '@astrojs/svelte';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [svelte()],
+});
diff --git a/packages/astro/e2e/fixtures/svelte-component/package.json b/packages/astro/e2e/fixtures/svelte-component/package.json
new file mode 100644
index 000000000..2777c8ecd
--- /dev/null
+++ b/packages/astro/e2e/fixtures/svelte-component/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@e2e/svelte-component",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/svelte": "workspace:*",
+ "astro": "workspace:*",
+ "svelte": "^3.48.0"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/svelte-component/src/components/Counter.svelte b/packages/astro/e2e/fixtures/svelte-component/src/components/Counter.svelte
new file mode 100644
index 000000000..264ec9dde
--- /dev/null
+++ b/packages/astro/e2e/fixtures/svelte-component/src/components/Counter.svelte
@@ -0,0 +1,35 @@
+<script lang="ts">
+ export let id: string;
+
+ let count = 0;
+
+ function add() {
+ count += 1;
+ }
+
+ function subtract() {
+ count -= 1;
+ }
+</script>
+
+<div {id} class="counter">
+ <button class="decrement" on:click={subtract}>-</button>
+ <pre>{ count }</pre>
+ <button class="increment" on:click={add}>+</button>
+</div>
+<div id={`${id}-message`} class="message">
+ <slot />
+</div>
+
+<style>
+ .counter{
+ display: grid;
+ font-size: 2em;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ margin-top: 2em;
+ place-items: center;
+ }
+ .message {
+ text-align: center;
+ }
+</style>
diff --git a/packages/astro/e2e/fixtures/svelte-component/src/pages/index.astro b/packages/astro/e2e/fixtures/svelte-component/src/pages/index.astro
new file mode 100644
index 000000000..3c8117124
--- /dev/null
+++ b/packages/astro/e2e/fixtures/svelte-component/src/pages/index.astro
@@ -0,0 +1,34 @@
+---
+import Counter from '../components/Counter.svelte';
+
+const someProps = {
+ count: 0,
+};
+---
+
+<html>
+ <head>
+ <!-- Head Stuff -->
+ </head>
+ <body>
+ <Counter id="server-only">
+ <h1>Hello, server!</h1>
+ </Counter>
+
+ <Counter id="client-idle" client:idle>
+ <h1>Hello, client:idle!</h1>
+ </Counter>
+
+ <Counter id="client-load" client:load>
+ <h1>Hello, client:load!</h1>
+ </Counter>
+
+ <Counter id="client-visible" client:visible>
+ <h1>Hello, client:visible!</h1>
+ </Counter>
+
+ <Counter id="client-media" client:media="(max-width: 50rem)">
+ <h1>Hello, client:media!</h1>
+ </Counter>
+ </body>
+</html>
diff --git a/packages/astro/e2e/fixtures/tailwindcss/package.json b/packages/astro/e2e/fixtures/tailwindcss/package.json
index 4bcc56872..f06e3ea91 100644
--- a/packages/astro/e2e/fixtures/tailwindcss/package.json
+++ b/packages/astro/e2e/fixtures/tailwindcss/package.json
@@ -1,5 +1,5 @@
{
- "name": "@test/e2e-tailwindcss",
+ "name": "@e2e/e2e-tailwindcss",
"version": "0.0.0",
"private": true,
"dependencies": {
diff --git a/packages/astro/e2e/fixtures/vue-component/astro.config.mjs b/packages/astro/e2e/fixtures/vue-component/astro.config.mjs
new file mode 100644
index 000000000..94bdad87f
--- /dev/null
+++ b/packages/astro/e2e/fixtures/vue-component/astro.config.mjs
@@ -0,0 +1,13 @@
+import { defineConfig } from 'astro/config';
+import vue from '@astrojs/vue';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [vue({
+ template: {
+ compilerOptions: {
+ isCustomElement: tag => tag.includes('my-button')
+ }
+ }
+ })],
+});
diff --git a/packages/astro/e2e/fixtures/vue-component/package.json b/packages/astro/e2e/fixtures/vue-component/package.json
new file mode 100644
index 000000000..2322b5d2d
--- /dev/null
+++ b/packages/astro/e2e/fixtures/vue-component/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@test/vue-component",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/vue": "workspace:*",
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/vue-component/src/components/Counter.vue b/packages/astro/e2e/fixtures/vue-component/src/components/Counter.vue
new file mode 100644
index 000000000..6516d1ee5
--- /dev/null
+++ b/packages/astro/e2e/fixtures/vue-component/src/components/Counter.vue
@@ -0,0 +1,47 @@
+<template>
+ <div :id="id" class="counter">
+ <h1><slot /></h1>
+ <button class="decrement" @click="subtract()">-</button>
+ <Result :value="count" />
+ <button class="increment" @click="add()">+</button>
+ </div>
+</template>
+
+<script>
+import { ref } from 'vue';
+
+import Result from './Result.vue';
+
+export default {
+ components: {
+ Result
+ },
+ props: {
+ id: {
+ type: String,
+ required: true
+ },
+ start: {
+ type: String,
+ required: true
+ },
+ stepSize: {
+ type: String,
+ default: "1"
+ }
+ },
+ setup(props) {
+ const id = props.id;
+ const count = ref(parseInt(props.start))
+ const stepSize = ref(parseInt(props.stepSize))
+ const add = () => (count.value = count.value + stepSize.value);
+ const subtract = () => (count.value = count.value - stepSize.value);
+ return {
+ count,
+ id,
+ add,
+ subtract,
+ };
+ },
+};
+</script>
diff --git a/packages/astro/e2e/fixtures/vue-component/src/components/Result.vue b/packages/astro/e2e/fixtures/vue-component/src/components/Result.vue
new file mode 100644
index 000000000..90bf218b2
--- /dev/null
+++ b/packages/astro/e2e/fixtures/vue-component/src/components/Result.vue
@@ -0,0 +1,16 @@
+<template>
+ <pre>{{ value }}</pre>
+ <my-button>Click Me</my-button>
+</template>
+
+<script>
+
+export default {
+ props: {
+ value: {
+ type: Number,
+ required: true
+ }
+ }
+}
+</script>
diff --git a/packages/astro/e2e/fixtures/vue-component/src/pages/index.astro b/packages/astro/e2e/fixtures/vue-component/src/pages/index.astro
new file mode 100644
index 000000000..40619841f
--- /dev/null
+++ b/packages/astro/e2e/fixtures/vue-component/src/pages/index.astro
@@ -0,0 +1,33 @@
+---
+import Counter from '../components/Counter.vue'
+---
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta
+ name="viewport"
+ content="width=device-width"
+ />
+ <title>Vue component</title>
+ <style>
+ :global(:root) {
+ font-family: system-ui;
+ padding: 1em;
+ }
+ </style>
+ </head>
+ <body>
+ <main>
+ <Counter id="server-only" start="0">No Client</Counter>
+ <Counter id="client-load" start="0" client:load>Hello, client:load!</Counter>
+ <!-- Test island deduplication, i.e. same UID as the component above. -->
+ <Counter id="client-load-dup" start="0" client:load>Hello, client:load!</Counter>
+ <!-- Test island deduplication account for non-render affecting props. -->
+ <Counter id="client-load-step" start="0" step-size="2" client:load>Hello, client:load!</Counter>
+ <Counter id="client-idle" start="0" client:idle>Hello, client:idle!</Counter>
+ <!-- Test that two client:visibles have unique uids -->
+ <Counter id="client-visible" start="0" client:visible>Hello, client:visible!</Counter>
+ <Counter id="client-media" start="0" client:media="(max-width: 50rem)">Hello, client:media!</Counter>
+ </main>
+ </body>
+</html>
diff --git a/packages/astro/e2e/lit-component.test.js b/packages/astro/e2e/lit-component.test.js
new file mode 100644
index 000000000..b3f48a3af
--- /dev/null
+++ b/packages/astro/e2e/lit-component.test.js
@@ -0,0 +1,103 @@
+import { test as base, expect } from '@playwright/test';
+import { loadFixture } from './test-utils.js';
+
+const test = base.extend({
+ astro: async ({}, use) => {
+ const fixture = await loadFixture({ root: './fixtures/lit-component/' });
+ await use(fixture);
+ },
+});
+
+let devServer;
+
+test.beforeEach(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterEach(async () => {
+ await devServer.stop();
+});
+
+// TODO: configure playwright to handle web component APIs
+// https://github.com/microsoft/playwright/issues/14241
+test.describe.skip('Lit components', () => {
+ test('client:idle', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#client-idle');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('p');
+ await expect(count, 'initial count is 0').toHaveText('Count: 0');
+
+ const inc = counter.locator('button');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('Count: 1');
+ });
+
+ test('client:load', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#client-load');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('p');
+ await expect(count, 'initial count is 0').toHaveText('Count: 0');
+
+ const inc = counter.locator('button');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('Count: 1');
+ });
+
+ test('client:visible', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ // Make sure the component is on screen to trigger hydration
+ const counter = page.locator('#client-visible');
+ await counter.scrollIntoViewIfNeeded();
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('p');
+ await expect(count, 'initial count is 0').toHaveText('Count: 0');
+
+ const inc = counter.locator('button');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('Count: 1');
+ });
+
+ test('client:media', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/media'));
+
+ const counter = page.locator('#client-media');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('p');
+ await expect(count, 'initial count is 0').toHaveText('Count: 0');
+
+ const inc = counter.locator('button');
+ await inc.click();
+
+ await expect(count, 'component not hydrated yet').toHaveText('Count: 0');
+
+ // Reset the viewport to hydrate the component (max-width: 50rem)
+ await page.setViewportSize({ width: 414, height: 1124 });
+
+ await inc.click();
+ await expect(count, 'count incremented by 1').toHaveText('Count: 1');
+ });
+
+ test('HMR', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const label = page.locator('#client-idle h1');
+
+ await astro.editFile('./src/pages/index.astro', (original) =>
+ original.replace('Hello, client:idle!', 'Hello, updated client:idle!')
+ );
+
+ await expect(label, 'slot text updated').toHaveText('Hello, updated client:idle!');
+ });
+});
diff --git a/packages/astro/e2e/multiple-frameworks.test.js b/packages/astro/e2e/multiple-frameworks.test.js
new file mode 100644
index 000000000..6f3906400
--- /dev/null
+++ b/packages/astro/e2e/multiple-frameworks.test.js
@@ -0,0 +1,141 @@
+import { test as base, expect } from '@playwright/test';
+import { loadFixture } from './test-utils.js';
+
+const test = base.extend({
+ astro: async ({}, use) => {
+ const fixture = await loadFixture({ root: './fixtures/multiple-frameworks/' });
+ await use(fixture);
+ },
+});
+
+let devServer;
+
+test.beforeEach(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterEach(async () => {
+ await devServer.stop();
+});
+
+test.describe('Multiple frameworks', () => {
+ test('React counter', async ({ astro, page }) => {
+ await page.goto('/');
+
+ const counter = await page.locator('#react-counter');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = await counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('.increment');
+ await increment.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('Preact counter', async ({ astro, page }) => {
+ await page.goto('/');
+
+ const counter = await page.locator('#preact-counter');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = await counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('.increment');
+ await increment.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('Solid counter', async ({ astro, page }) => {
+ await page.goto('/');
+
+ const counter = await page.locator('#solid-counter');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = await counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('.increment');
+ await increment.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('Vue counter', async ({ astro, page }) => {
+ await page.goto('/');
+
+ const counter = await page.locator('#vue-counter');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = await counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('.increment');
+ await increment.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('Svelte counter', async ({ astro, page }) => {
+ await page.goto('/');
+
+ const counter = await page.locator('#svelte-counter');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = await counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('.increment');
+ await increment.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('Astro components', async ({ astro, page }) => {
+ await page.goto('/');
+
+ const aComponent = await page.locator('#astro-a');
+ await expect(aComponent, 'component is visible').toBeVisible();
+ await expect(aComponent, 'component text is visible').toHaveText('Hello Astro (A)');
+
+
+ const bComponent = await page.locator('#astro-b');
+ await expect(bComponent, 'component is visible').toBeVisible();
+ await expect(bComponent, 'component text is visible').toHaveText('Hello Astro (B)');
+ });
+
+ test('HMR', async ({ astro, page }) => {
+ await page.goto('/');
+
+ // 1: updating the page template
+ const preactSlot = page.locator('#preact-counter + .counter-message');
+ await expect(preactSlot, 'initial slot content').toHaveText('Hello Preact!');
+
+ await astro.editFile('./src/pages/index.astro', (content) =>
+ content.replace('Hello Preact!', 'Hello Preact, updated!')
+ );
+
+ await expect(preactSlot, 'slot content updated').toHaveText('Hello Preact, updated!');
+
+ // Edit the react component
+ await astro.editFile('./src/components/ReactCounter.jsx', (content) =>
+ content.replace('useState(0)', 'useState(5)')
+ );
+
+ const reactCount = await page.locator('#react-counter pre');
+ await expect(reactCount, 'initial count updated to 5').toHaveText('5');
+
+ // Edit the svelte component's style
+ const svelteCounter = page.locator('#svelte-counter');
+ await expect(svelteCounter, 'initial background is white').toHaveCSS('background-color', 'rgb(255, 255, 255)');
+
+ await astro.editFile('./src/components/SvelteCounter.svelte', (content) =>
+ content.replace('background: white', 'background: rgb(230, 230, 230)')
+ );
+
+ await expect(svelteCounter, 'background color updated').toHaveCSS('background-color', 'rgb(230, 230, 230)');
+ });
+});
diff --git a/packages/astro/e2e/nested-styles.test.js b/packages/astro/e2e/nested-styles.test.js
index 7c233d4cb..ff7dbac30 100644
--- a/packages/astro/e2e/nested-styles.test.js
+++ b/packages/astro/e2e/nested-styles.test.js
@@ -10,18 +10,18 @@ const test = base.extend({
let devServer;
-test.beforeAll(async ({ astro }) => {
+test.beforeEach(async ({ astro }) => {
devServer = await astro.startDevServer();
});
-test.afterAll(async ({ astro }) => {
+test.afterEach(async () => {
await devServer.stop();
});
-test('Loading styles that are nested', async ({ page, astro }) => {
- await page.goto(astro.resolveUrl('/'));
+test.describe('Loading styles that are nested', () => {
+ test('header', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
- await test.step('header', async () => {
const header = page.locator('header');
await expect(header, 'should have background color').toHaveCSS(
diff --git a/packages/astro/e2e/preact-component.test.js b/packages/astro/e2e/preact-component.test.js
new file mode 100644
index 000000000..7eca69044
--- /dev/null
+++ b/packages/astro/e2e/preact-component.test.js
@@ -0,0 +1,143 @@
+import { test as base, expect } from '@playwright/test';
+import { loadFixture } from './test-utils.js';
+
+const test = base.extend({
+ astro: async ({}, use) => {
+ const fixture = await loadFixture({ root: './fixtures/preact-component/' });
+ await use(fixture);
+ },
+});
+
+let devServer;
+
+test.beforeEach(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterEach(async () => {
+ await devServer.stop();
+});
+
+test.describe('Preact components', () => {
+ test('server only', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#server-only');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('0');
+ });
+
+ test('client:idle', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#client-idle');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('client:load', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#client-load');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('client:visible', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ // Make sure the component is on screen to trigger hydration
+ const counter = page.locator('#client-visible');
+ await counter.scrollIntoViewIfNeeded();
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('client:media', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#client-media');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+ await expect(count, 'component not hydrated yet').toHaveText('0');
+
+ // Reset the viewport to hydrate the component (max-width: 50rem)
+ await page.setViewportSize({ width: 414, height: 1124 });
+ await inc.click();
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('client:only', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const label = page.locator('#client-only');
+ await expect(label, 'component is visible').toBeVisible();
+
+ await expect(label, 'slot text is visible').toHaveText('Preact client:only component');
+ });
+
+ test('HMR', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const count = page.locator('#client-idle pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ // Edit the component's initial count prop
+ await astro.editFile('./src/pages/index.astro', (original) =>
+ original.replace('id="client-idle" {...someProps}', 'id="client-idle" count={5}')
+ );
+
+ await expect(count, 'count prop updated').toHaveText('5');
+
+ // Edit the component's slot text
+ await astro.editFile('./src/components/JSXComponent.jsx', (original) =>
+ original.replace('Preact client:only component', 'Updated preact client:only component')
+ );
+
+ const label = page.locator('#client-only');
+ await expect(label, 'client:only component is visible').toBeVisible();
+ await expect(label, 'client:only slot text is visible').toHaveText(
+ 'Updated preact client:only component'
+ );
+
+ // Edit the imported CSS file
+ await astro.editFile('./src/components/Counter.css', (original) =>
+ original.replace('font-size: 2em;', 'font-size: 24px;')
+ );
+
+ await expect(count, 'imported styles updated').toHaveCSS('font-size', '24px');
+ });
+});
diff --git a/packages/astro/e2e/react-component.test.js b/packages/astro/e2e/react-component.test.js
new file mode 100644
index 000000000..2dded1e6d
--- /dev/null
+++ b/packages/astro/e2e/react-component.test.js
@@ -0,0 +1,143 @@
+import { test as base, expect } from '@playwright/test';
+import { loadFixture } from './test-utils.js';
+
+const test = base.extend({
+ astro: async ({}, use) => {
+ const fixture = await loadFixture({ root: './fixtures/react-component/' });
+ await use(fixture);
+ },
+});
+
+let devServer;
+
+test.beforeEach(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterEach(async () => {
+ await devServer.stop();
+});
+
+test.describe('React components', () => {
+ test('server only', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#server-only');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'component not hydrated').toHaveText('0');
+ });
+
+ test('client:idle', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#client-idle');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('client:load', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#client-load');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('client:visible', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ // Make sure the component is on screen to trigger hydration
+ const counter = page.locator('#client-visible');
+ await counter.scrollIntoViewIfNeeded();
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('client:media', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#client-media');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+ await expect(count, 'component not hydrated yet').toHaveText('0');
+
+ // Reset the viewport to hydrate the component (max-width: 50rem)
+ await page.setViewportSize({ width: 414, height: 1124 });
+ await inc.click();
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('client:only', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const label = page.locator('#client-only');
+ await expect(label, 'component is visible').toBeVisible();
+
+ await expect(label, 'slot text is visible').toHaveText('React client:only component');
+ });
+
+ test('HMR', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const count = page.locator('#client-idle pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ // Edit the component's initial count prop
+ await astro.editFile('./src/pages/index.astro', (original) =>
+ original.replace('id="client-idle" {...someProps}', 'id="client-idle" count={5}')
+ );
+
+ await expect(count, 'count prop updated').toHaveText('5');
+
+ // Edit the component's slot text
+ await astro.editFile('./src/components/JSXComponent.jsx', (original) =>
+ original.replace('React client:only component', 'Updated react client:only component')
+ );
+
+ const label = page.locator('#client-only');
+ await expect(label, 'client:only component is visible').toBeVisible();
+ await expect(label, 'client:only slot text is visible').toHaveText(
+ 'Updated react client:only component'
+ );
+
+ // Edit the imported CSS file
+ await astro.editFile('./src/components/Counter.css', (original) =>
+ original.replace('font-size: 2em;', 'font-size: 24px;')
+ );
+
+ await expect(count, 'imported CSS updated').toHaveCSS('font-size', '24px');
+ });
+});
diff --git a/packages/astro/e2e/solid-component.test.js b/packages/astro/e2e/solid-component.test.js
new file mode 100644
index 000000000..f02b76f65
--- /dev/null
+++ b/packages/astro/e2e/solid-component.test.js
@@ -0,0 +1,123 @@
+import { test as base, expect } from '@playwright/test';
+import { loadFixture } from './test-utils.js';
+
+const test = base.extend({
+ astro: async ({}, use) => {
+ const fixture = await loadFixture({ root: './fixtures/solid-component/' });
+ await use(fixture);
+ },
+});
+
+let devServer;
+
+test.beforeEach(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterEach(async () => {
+ await devServer.stop();
+});
+
+test.describe('Solid components', () => {
+ test('server only', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#server-only');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'component not hydrated').toHaveText('0');
+ });
+
+ test('client:idle', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#client-idle');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('client:load', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#client-load');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('client:visible', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ // Make sure the component is on screen to trigger hydration
+ const counter = page.locator('#client-visible');
+ await counter.scrollIntoViewIfNeeded();
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('client:media', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#client-media');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+ await expect(count, 'component not hydrated yet').toHaveText('0');
+
+ // Reset the viewport to hydrate the component (max-width: 50rem)
+ await page.setViewportSize({ width: 414, height: 1124 });
+ await inc.click();
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('HMR', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const count = page.locator('#client-idle pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ // Edit the component's initial count prop
+ await astro.editFile('./src/pages/index.astro', (original) =>
+ original.replace('id="client-idle" {...someProps}', 'id="client-idle" count={5}')
+ );
+
+ await expect(count, 'count prop updated').toHaveText('5');
+
+ // Edit the imported CSS
+ await astro.editFile('./src/components/Counter.css', (original) =>
+ original.replace('font-size: 2em;', 'font-size: 24px;')
+ );
+
+ await expect(count, 'imported CSS updated').toHaveCSS('font-size', '24px');
+ });
+});
diff --git a/packages/astro/e2e/svelte-component.test.js b/packages/astro/e2e/svelte-component.test.js
new file mode 100644
index 000000000..65d9ea68e
--- /dev/null
+++ b/packages/astro/e2e/svelte-component.test.js
@@ -0,0 +1,114 @@
+import { test as base, expect } from '@playwright/test';
+import { loadFixture } from './test-utils.js';
+
+const test = base.extend({
+ astro: async ({}, use) => {
+ const fixture = await loadFixture({ root: './fixtures/svelte-component/' });
+ await use(fixture);
+ },
+});
+
+let devServer;
+
+test.beforeEach(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterEach(async () => {
+ await devServer.stop();
+});
+
+test.describe('Svelte components', () => {
+ test('server only', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#server-only');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'component not hydrated').toHaveText('0');
+ });
+
+ test('client:idle', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#client-idle');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('client:load', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#client-load');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('client:visible', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ // Make sure the component is on screen to trigger hydration
+ const counter = page.locator('#client-visible');
+ await counter.scrollIntoViewIfNeeded();
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('client:media', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#client-media');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+ await expect(count, 'component not hydrated yet').toHaveText('0');
+
+ // Reset the viewport to hydrate the component (max-width: 50rem)
+ await page.setViewportSize({ width: 414, height: 1124 });
+ await inc.click();
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('HMR', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ // Edit the component's slot text
+ await astro.editFile('./src/pages/index.astro', (original) =>
+ original.replace('Hello, client:idle!', 'Hello, updated client:idle!')
+ );
+
+ const label = page.locator('#client-idle-message');
+ await expect(label, 'slot text updated').toHaveText('Hello, updated client:idle!');
+ });
+});
diff --git a/packages/astro/e2e/tailwindcss.test.js b/packages/astro/e2e/tailwindcss.test.js
index e156a7be7..acfa9b724 100644
--- a/packages/astro/e2e/tailwindcss.test.js
+++ b/packages/astro/e2e/tailwindcss.test.js
@@ -10,18 +10,19 @@ const test = base.extend({
let devServer;
-test.beforeAll(async ({ astro }) => {
+test.beforeEach(async ({ astro }) => {
devServer = await astro.startDevServer();
});
-test.afterAll(async ({ astro }) => {
+test.afterEach(async ({ astro }) => {
await devServer.stop();
+ astro.resetAllFiles();
});
-test('Tailwind CSS', async ({ page, astro }) => {
- await page.goto(astro.resolveUrl('/'));
+test.describe('Tailwind CSS', () => {
+ test('body', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
- await test.step('body', async () => {
const body = page.locator('body');
await expect(body, 'should have classes').toHaveClass('bg-dawn text-midnight');
@@ -32,7 +33,9 @@ test('Tailwind CSS', async ({ page, astro }) => {
await expect(body, 'should have color').toHaveCSS('color', 'rgb(49, 39, 74)');
});
- await test.step('button', async () => {
+ test('button', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
const button = page.locator('button');
await expect(button, 'should have bg-purple-600').toHaveClass(/bg-purple-600/);
@@ -48,4 +51,20 @@ test('Tailwind CSS', async ({ page, astro }) => {
await expect(button, 'should have font-[900]').toHaveClass(/font-\[900\]/);
await expect(button, 'should have font weight').toHaveCSS('font-weight', '900');
});
+
+ test('HMR', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ await astro.editFile('./src/components/Button.astro', (original) =>
+ original.replace('bg-purple-600', 'bg-purple-400')
+ );
+
+ const button = page.locator('button');
+
+ await expect(button, 'should have bg-purple-400').toHaveClass(/bg-purple-400/);
+ await expect(button, 'should have background color').toHaveCSS(
+ 'background-color',
+ 'rgb(192, 132, 252)'
+ );
+ });
});
diff --git a/packages/astro/e2e/vue-component.test.js b/packages/astro/e2e/vue-component.test.js
new file mode 100644
index 000000000..28b5e3fd0
--- /dev/null
+++ b/packages/astro/e2e/vue-component.test.js
@@ -0,0 +1,144 @@
+import { test as base, expect } from '@playwright/test';
+import { loadFixture } from './test-utils.js';
+
+const test = base.extend({
+ astro: async ({}, use) => {
+ const fixture = await loadFixture({ root: './fixtures/vue-component/' });
+ await use(fixture);
+ },
+});
+
+let devServer;
+
+test.beforeEach(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterEach(async () => {
+ await devServer.stop();
+});
+
+test.describe('Vue components', () => {
+ test('server only', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#server-only');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'component not hydrated').toHaveText('0');
+ });
+
+ test('client:idle', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#client-idle');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('client:load', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ // Multiple counters on the page to verify islands aren't sharing state
+ const counter = page.locator('#client-load');
+ const counterDup = page.locator('#client-load-dup');
+ const counterStep = page.locator('#client-load-step');
+
+ await expect(counter).toBeVisible();
+ await expect(counterDup).toBeVisible();
+ await expect(counterStep).toBeVisible();
+
+ const count = counter.locator('pre');
+ const countDup = counterDup.locator('pre');
+ const countStep = counterStep.locator('pre');
+
+ const countInc = counter.locator('.increment');
+ const countDupInc = counterDup.locator('.increment');
+ const countStepInc = counterStep.locator('.increment');
+
+ // Should only increment the first counter
+ await countInc.click();
+
+ await expect(count, 'intial count is 1').toHaveText('1');
+ await expect(countDup, 'initial count is 0').toHaveText('0');
+ await expect(countStep, 'initial count is 0').toHaveText('0');
+
+ // Should only increment the second counter
+ await countDupInc.click();
+
+ await expect(count, "count didn't change").toHaveText('1');
+ await expect(countDup, 'count incremented by 1').toHaveText('1');
+ await expect(countStep, "count didn't change").toHaveText('0');
+
+ // Should only increment the third counter
+ // Expecting an increase of 4 becasuse the component's
+ // step is set to 2
+ await countStepInc.click();
+ await countStepInc.click();
+
+ await expect(count, "count didn't change").toHaveText('1');
+ await expect(countDup, "count didn't change").toHaveText('1');
+ await expect(countStep, 'count incremented by 4').toHaveText('4');
+ });
+
+ test('client:visible', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ // Make sure the component is on screen to trigger hydration
+ const counter = page.locator('#client-visible');
+ await counter.scrollIntoViewIfNeeded();
+ await expect(counter).toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('client:media', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ const counter = page.locator('#client-media');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = counter.locator('pre');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const inc = counter.locator('.increment');
+ await inc.click();
+ await expect(count, 'component not hydrated yet').toHaveText('0');
+
+ // Reset the viewport to hydrate the component (max-width: 50rem)
+ await page.setViewportSize({ width: 414, height: 1124 });
+ await inc.click();
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('HMR', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ // Edit the component's slot text
+ await astro.editFile('./src/pages/index.astro', (original) =>
+ original.replace('Hello, client:visible!', 'Hello, updated client:visible!')
+ );
+
+ const label = page.locator('#client-visible h1');
+ await expect(label, 'slotted text updated').toHaveText('Hello, updated client:visible!');
+ });
+});
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 9a2b06ec3..709d6f21f 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 --ignore **/errors.test.js && mocha --timeout 20000 **/lit-element.test.js && mocha --timeout 20000 **/errors.test.js",
"test:match": "mocha --timeout 20000 -g",
- "test:e2e": "playwright test e2e"
+ "test:e2e": "playwright test",
+ "test:e2e:match": "playwright test -g"
},
"dependencies": {
"@astrojs/compiler": "^0.14.3",
diff --git a/packages/astro/playwright.config.js b/packages/astro/playwright.config.js
new file mode 100644
index 000000000..8c13d8ef9
--- /dev/null
+++ b/packages/astro/playwright.config.js
@@ -0,0 +1,42 @@
+import { devices } from '@playwright/test';
+
+const config = {
+ testMatch: 'e2e/*.test.js',
+ /* Maximum time one test can run for. */
+ timeout: 30 * 1000,
+ expect: {
+ /**
+ * Maximum time expect() should wait for the condition to be met.
+ * For example in `await expect(locator).toHaveText();`
+ */
+ timeout: 5000
+ },
+ /* Fail the build on CI if you accidentally left test 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,
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
+ actionTimeout: 0,
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000',
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+ projects: [
+ {
+ name: 'Chrome Stable',
+ use: {
+ browserName: 'chromium',
+ channel: 'chrome',
+ },
+ },
+ ],
+
+};
+
+export default config;
diff --git a/packages/astro/src/runtime/client/hmr.ts b/packages/astro/src/runtime/client/hmr.ts
index 7cd773256..8a0512f26 100644
--- a/packages/astro/src/runtime/client/hmr.ts
+++ b/packages/astro/src/runtime/client/hmr.ts
@@ -31,7 +31,7 @@ if (import.meta.hot) {
}
}
if (hasAstroUpdate) {
- return updatePage();
+ return await updatePage();
}
}
import.meta.hot.on('vite:beforeUpdate', async (event) => {
diff --git a/packages/astro/test/errors.test.js b/packages/astro/test/errors.test.js
index 7391d363e..2c5dbdbf3 100644
--- a/packages/astro/test/errors.test.js
+++ b/packages/astro/test/errors.test.js
@@ -52,9 +52,7 @@ describe('Error display', () => {
expect($('.statusMessage').text()).to.equal('Internal Error');
// 2. Edit the file, fixing the error
- let changeOccured = fixture.onNextChange();
await fixture.editFile('./src/components/SvelteSyntaxError.svelte', `<h1>No mismatch</h1>`);
- await changeOccured;
// 3. Verify that the file is fixed.
html = await fixture.fetch('/svelte-syntax-error').then((res) => res.text());
diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js
index bbdd86b57..8fd5393c0 100644
--- a/packages/astro/test/test-utils.js
+++ b/packages/astro/test/test-utils.js
@@ -28,6 +28,7 @@ polyfill(globalThis, {
* @property {(url: string) => string} resolveUrl
* @property {(url: string, opts: any) => Promise<Response>} fetch
* @property {(path: string) => Promise<string>} readFile
+ * @property {(path: string, updater: (content: string) => string) => Promise<void>} writeFile
* @property {(path: string) => Promise<string[]>} readdir
* @property {() => Promise<DevServer>} startDevServer
* @property {() => Promise<PreviewServer>} preview
@@ -97,7 +98,7 @@ export async function loadFixture(inlineConfig) {
const resolveUrl = (url) =>
`http://${'127.0.0.1'}:${config.server.port}${url.replace(/^\/?/, '/')}`;
-
+
// A map of files that have been editted.
let fileEdits = new Map();
@@ -108,6 +109,11 @@ export async function loadFixture(inlineConfig) {
fileEdits.clear();
};
+ const onNextChange = () =>
+ devServer
+ ? new Promise((resolve) => devServer.watcher.once('change', resolve))
+ : Promise.reject(new Error('No dev server running'))
+
// After each test, reset each of the edits to their original contents.
if (typeof afterEach === 'function') {
afterEach(resetAllFiles);
@@ -134,7 +140,9 @@ export async function loadFixture(inlineConfig) {
readFile: (filePath) =>
fs.promises.readFile(new URL(filePath.replace(/^\//, ''), config.outDir), 'utf8'),
readdir: (fp) => fs.promises.readdir(new URL(fp.replace(/^\//, ''), config.outDir)),
- clean: () => fs.promises.rm(config.outDir, { maxRetries: 10, recursive: true, force: true }),
+ clean: async () => {
+ await fs.promises.rm(config.outDir, { maxRetries: 10, recursive: true, force: true });
+ },
loadTestAdapterApp: async () => {
const url = new URL('./server/entry.mjs', config.outDir);
const { createApp, manifest } = await import(url);
@@ -142,22 +150,26 @@ export async function loadFixture(inlineConfig) {
app.manifest = manifest;
return app;
},
- editFile: async (filePath, newContents) => {
+ editFile: async (filePath, newContentsOrCallback) => {
const fileUrl = new URL(filePath.replace(/^\//, ''), config.root);
const contents = await fs.promises.readFile(fileUrl, 'utf-8');
- const reset = () => fs.writeFileSync(fileUrl, contents);
+ const reset = () => {
+ fs.writeFileSync(fileUrl, contents);
+ }
// Only save this reset if not already in the map, in case multiple edits happen
// to the same file.
if (!fileEdits.has(fileUrl.toString())) {
fileEdits.set(fileUrl.toString(), reset);
}
+ const newContents = typeof newContentsOrCallback === 'function'
+ ? newContentsOrCallback(contents)
+ : newContentsOrCallback;
+ const nextChange = onNextChange();
await fs.promises.writeFile(fileUrl, newContents);
+ await nextChange;
return reset;
},
- onNextChange: () =>
- devServer
- ? new Promise((resolve) => devServer.watcher.once('change', resolve))
- : Promise.reject(new Error('No dev server running')),
+ resetAllFiles
};
}