diff options
Diffstat (limited to 'packages/astro')
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 }; } |