aboutsummaryrefslogtreecommitdiff
path: root/packages/astro/test/units/dev
diff options
context:
space:
mode:
Diffstat (limited to 'packages/astro/test/units/dev')
-rw-r--r--packages/astro/test/units/dev/base.test.js112
-rw-r--r--packages/astro/test/units/dev/collections-mixed-content-errors.test.js147
-rw-r--r--packages/astro/test/units/dev/collections-renderentry.test.js298
-rw-r--r--packages/astro/test/units/dev/dev.test.js196
-rw-r--r--packages/astro/test/units/dev/head-injection.test.js189
-rw-r--r--packages/astro/test/units/dev/hydration.test.js50
-rw-r--r--packages/astro/test/units/dev/restart.test.js233
-rw-r--r--packages/astro/test/units/dev/styles.test.js81
8 files changed, 1306 insertions, 0 deletions
diff --git a/packages/astro/test/units/dev/base.test.js b/packages/astro/test/units/dev/base.test.js
new file mode 100644
index 000000000..f230ad563
--- /dev/null
+++ b/packages/astro/test/units/dev/base.test.js
@@ -0,0 +1,112 @@
+import * as assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import { createFixture, createRequestAndResponse, runInContainer } from '../test-utils.js';
+
+describe('base configuration', () => {
+ describe('with trailingSlash: "never"', () => {
+ describe('index route', () => {
+ it('Requests that include a trailing slash 404', async () => {
+ const fixture = await createFixture({
+ '/src/pages/index.astro': `<h1>testing</h1>`,
+ });
+
+ await runInContainer(
+ {
+ inlineConfig: {
+ root: fixture.path,
+ base: '/docs',
+ trailingSlash: 'never',
+ },
+ },
+ async (container) => {
+ const { req, res, done } = createRequestAndResponse({
+ method: 'GET',
+ url: '/docs/',
+ });
+ container.handle(req, res);
+ await done;
+ assert.equal(res.statusCode, 404);
+ },
+ );
+ });
+
+ it('Requests that exclude a trailing slash 200', async () => {
+ const fixture = await createFixture({
+ '/src/pages/index.astro': `<h1>testing</h1>`,
+ });
+
+ await runInContainer(
+ {
+ fs,
+ inlineConfig: {
+ root: fixture.path,
+ base: '/docs',
+ trailingSlash: 'never',
+ },
+ },
+ async (container) => {
+ const { req, res, done } = createRequestAndResponse({
+ method: 'GET',
+ url: '/docs',
+ });
+ container.handle(req, res);
+ await done;
+ assert.equal(res.statusCode, 200);
+ },
+ );
+ });
+ });
+
+ describe('sub route', () => {
+ it('Requests that include a trailing slash 404', async () => {
+ const fixture = await createFixture({
+ '/src/pages/sub/index.astro': `<h1>testing</h1>`,
+ });
+
+ await runInContainer(
+ {
+ inlineConfig: {
+ root: fixture.path,
+ base: '/docs',
+ trailingSlash: 'never',
+ },
+ },
+ async (container) => {
+ const { req, res, done } = createRequestAndResponse({
+ method: 'GET',
+ url: '/docs/sub/',
+ });
+ container.handle(req, res);
+ await done;
+ assert.equal(res.statusCode, 404);
+ },
+ );
+ });
+
+ it('Requests that exclude a trailing slash 200', async () => {
+ const fixture = await createFixture({
+ '/src/pages/sub/index.astro': `<h1>testing</h1>`,
+ });
+
+ await runInContainer(
+ {
+ inlineConfig: {
+ root: fixture.path,
+ base: '/docs',
+ trailingSlash: 'never',
+ },
+ },
+ async (container) => {
+ const { req, res, done } = createRequestAndResponse({
+ method: 'GET',
+ url: '/docs/sub',
+ });
+ container.handle(req, res);
+ await done;
+ assert.equal(res.statusCode, 200);
+ },
+ );
+ });
+ });
+ });
+});
diff --git a/packages/astro/test/units/dev/collections-mixed-content-errors.test.js b/packages/astro/test/units/dev/collections-mixed-content-errors.test.js
new file mode 100644
index 000000000..295662c93
--- /dev/null
+++ b/packages/astro/test/units/dev/collections-mixed-content-errors.test.js
@@ -0,0 +1,147 @@
+import * as assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import _sync from '../../../dist/core/sync/index.js';
+import { createFixture } from '../test-utils.js';
+
+async function sync(root) {
+ try {
+ await _sync({
+ root,
+ logLevel: 'silent',
+ });
+ return 0;
+ } catch {
+ return 1;
+ }
+}
+
+const baseFileTree = {
+ '/astro.config.mjs': `export default { legacy: { collections: true }}`,
+ '/src/content/authors/placeholder.json': `{ "name": "Placeholder" }`,
+ '/src/content/blog/placeholder.md': `\
+---
+title: Placeholder post
+---
+`,
+ '/src/pages/authors.astro': `\
+---
+import { getCollection } from 'astro:content';
+try {
+ await getCollection('authors')
+} catch (e) {
+ return e
+}
+---
+
+<h1>Worked</h1>
+`,
+ '/src/pages/blog.astro': `\
+---
+import { getCollection } from 'astro:content';
+
+await getCollection('blog')
+---
+
+<h1>Worked</h1>`,
+};
+
+describe('Content Collections - mixed content errors', () => {
+ it('raises "mixed content" error when content in data collection', async () => {
+ const fixture = await createFixture({
+ ...baseFileTree,
+ '/src/content/authors/ben.md': `\
+---
+name: Ben
+---
+
+# Ben
+`,
+ '/src/content/authors/tony.json': `{ "name": "Tony" }`,
+ '/src/content.config.ts': `\
+import { z, defineCollection } from 'astro:content';
+
+const authors = defineCollection({
+ type: 'data',
+ schema: z.object({
+ name: z.string(),
+ }),
+});
+
+export const collections = { authors };
+`,
+ });
+
+ assert.equal(await sync(fixture.path), 1);
+ });
+
+ it('raises "mixed content" error when data in content collection', async () => {
+ const fixture = await createFixture({
+ ...baseFileTree,
+ '/src/content/blog/post.md': `\
+---
+title: Post
+---
+
+# Post
+`,
+ '/src/content/blog/post.yaml': `title: YAML Post`,
+ '/src/content.config.ts': `\
+import { z, defineCollection } from 'astro:content';
+
+const blog = defineCollection({
+ type: 'content',
+ schema: z.object({
+ title: z.string(),
+ }),
+});
+
+export const collections = { blog };
+`,
+ });
+
+ assert.equal(await sync(fixture.path), 1);
+ });
+
+ it('raises error when data collection configured as content collection', async () => {
+ const fixture = await createFixture({
+ ...baseFileTree,
+ '/src/content/banners/welcome.json': `{ "src": "/example", "alt": "Welcome" }`,
+ '/src/content/config.ts': `\
+import { z, defineCollection } from 'astro:content';
+
+const banners = defineCollection({
+ schema: z.object({
+ src: z.string(),
+ alt: z.string(),
+ }),
+});
+
+export const collections = { banners };
+`,
+ });
+
+ assert.equal(await sync(fixture.path), 1);
+ });
+
+ it('does not raise error for empty collection with config', async () => {
+ const fixture = await createFixture({
+ ...baseFileTree,
+ // Add placeholder to ensure directory exists
+ '/src/content/i18n/_placeholder.txt': 'Need content here',
+ '/src/content.config.ts': `\
+import { z, defineCollection } from 'astro:content';
+
+const i18n = defineCollection({
+ type: 'data',
+ schema: z.object({
+ greeting: z.string(),
+ }),
+});
+
+export const collections = { i18n };
+`,
+ });
+
+ assert.equal(await sync(fixture.path), 0);
+ });
+});
diff --git a/packages/astro/test/units/dev/collections-renderentry.test.js b/packages/astro/test/units/dev/collections-renderentry.test.js
new file mode 100644
index 000000000..298c433b8
--- /dev/null
+++ b/packages/astro/test/units/dev/collections-renderentry.test.js
@@ -0,0 +1,298 @@
+import * as assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import * as cheerio from 'cheerio';
+
+import { attachContentServerListeners } from '../../../dist/content/server-listeners.js';
+import { createFixture, createRequestAndResponse, runInContainer } from '../test-utils.js';
+
+const baseFileTree = {
+ 'astro.config.mjs': `\
+import mdx from '@astrojs/mdx';
+export default {
+ integrations: [mdx()],
+ legacy: {
+ // Enable legacy content collections as we test layout fields
+ collections: true
+ }
+};
+`,
+ '/src/content/blog/promo/_launch-week-styles.css': `\
+body {
+ font-family: 'Comic Sans MS', sans-serif;
+}
+`,
+ '/src/content/blog/promo/launch-week.mdx': `\
+---
+title: 'Launch week!'
+description: 'Join us for the exciting launch of SPACE BLOG'
+publishedDate: 'Sat May 21 2022 00:00:00 GMT-0400 (Eastern Daylight Time)'
+tags: ['announcement']
+---
+
+import './_launch-week-styles.css';
+
+Join us for the space blog launch!
+
+- THIS THURSDAY
+- Houston, TX
+- Dress code: **interstellar casual** ✨
+`,
+};
+
+/** @type {typeof runInContainer} */
+async function runInContainerWithContentListeners(params, callback) {
+ return await runInContainer(params, async (container) => {
+ await attachContentServerListeners(container);
+ await callback(container);
+ });
+}
+
+describe('Content Collections - render()', () => {
+ it('can be called in a page component', async () => {
+ const fixture = await createFixture({
+ ...baseFileTree,
+ '/src/content/config.ts': `
+ import { z, defineCollection } from 'astro:content';
+
+ const blog = defineCollection({
+ schema: z.object({
+ title: z.string(),
+ description: z.string().max(60, 'For SEO purposes, keep descriptions short!'),
+ }),
+ });
+
+ export const collections = { blog };
+ `,
+ '/src/pages/index.astro': `
+ ---
+ import { getCollection } from 'astro:content';
+ const blog = await getCollection('blog');
+ const launchWeekEntry = blog.find(post => post.id === 'promo/launch-week.mdx');
+ const { Content } = await launchWeekEntry.render();
+ ---
+ <html>
+ <head><title>Testing</title></head>
+ <body>
+ <h1>testing</h1>
+ <Content />
+ </body>
+ </html>
+ `,
+ });
+
+ await runInContainerWithContentListeners(
+ {
+ inlineConfig: {
+ root: fixture.path,
+ vite: { server: { middlewareMode: true } },
+ },
+ },
+ async (container) => {
+ const { req, res, done, text } = createRequestAndResponse({
+ method: 'GET',
+ url: '/',
+ });
+ container.handle(req, res);
+ await done;
+ const html = await text();
+
+ const $ = cheerio.load(html);
+ // Rendered the content
+ assert.equal($('ul li').length, 3);
+
+ // Rendered the styles
+ assert.equal($('style').length, 1);
+ },
+ );
+ });
+
+ it('can be used in a layout component', async () => {
+ const fixture = await createFixture({
+ ...baseFileTree,
+ '/src/components/Layout.astro': `
+ ---
+ import { getCollection } from 'astro:content';
+ const blog = await getCollection('blog');
+ const launchWeekEntry = blog.find(post => post.id === 'promo/launch-week.mdx');
+ const { Content } = await launchWeekEntry.render();
+ ---
+ <html>
+ <head></head>
+ <body>
+ <slot name="title"></slot>
+ <article>
+ <Content />
+ </article>
+ </body>
+ </html>
+
+ `,
+ '/src/pages/index.astro': `
+ ---
+ import Layout from '../components/Layout.astro';
+ ---
+ <Layout>
+ <h1 slot="title">Index page</h2>
+ </Layout>
+ `,
+ });
+
+ await runInContainerWithContentListeners(
+ {
+ inlineConfig: {
+ root: fixture.path,
+ vite: { server: { middlewareMode: true } },
+ },
+ },
+ async (container) => {
+ const { req, res, done, text } = createRequestAndResponse({
+ method: 'GET',
+ url: '/',
+ });
+ container.handle(req, res);
+ await done;
+ const html = await text();
+
+ const $ = cheerio.load(html);
+ // Rendered the content
+ assert.equal($('ul li').length, 3);
+
+ // Rendered the styles
+ assert.equal($('style').length, 1);
+ },
+ );
+ });
+
+ it('can be used in a slot', async () => {
+ const fixture = await createFixture({
+ ...baseFileTree,
+ '/src/content.config.ts': `
+ import { z, defineCollection } from 'astro:content';
+
+ const blog = defineCollection({
+ schema: z.object({
+ title: z.string(),
+ description: z.string().max(60, 'For SEO purposes, keep descriptions short!'),
+ }),
+ });
+
+ export const collections = { blog };
+ `,
+ '/src/components/Layout.astro': `
+ <html>
+ <head></head>
+ <body>
+ <slot name="title"></slot>
+ <article>
+ <slot name="main"></slot>
+ </article>
+ </body>
+ </html>
+ `,
+ '/src/pages/index.astro': `
+ ---
+ import Layout from '../components/Layout.astro';
+ import { getCollection } from 'astro:content';
+ const blog = await getCollection('blog');
+ const launchWeekEntry = blog.find(post => post.id === 'promo/launch-week.mdx');
+ const { Content } = await launchWeekEntry.render();
+ ---
+ <Layout>
+ <h1 slot="title">Index page</h2>
+ <Content slot="main" />
+ </Layout>
+ `,
+ });
+
+ await runInContainerWithContentListeners(
+ {
+ inlineConfig: {
+ root: fixture.path,
+ vite: { server: { middlewareMode: true } },
+ },
+ },
+ async (container) => {
+ const { req, res, done, text } = createRequestAndResponse({
+ method: 'GET',
+ url: '/',
+ });
+ container.handle(req, res);
+ await done;
+ const html = await text();
+
+ const $ = cheerio.load(html);
+ // Rendered the content
+ assert.equal($('ul li').length, 3);
+
+ // Rendered the styles
+ assert.equal($('style').length, 1);
+ },
+ );
+ });
+
+ it('can be called from any js/ts file', async () => {
+ const fixture = await createFixture({
+ ...baseFileTree,
+ '/src/content.config.ts': `
+ import { z, defineCollection } from 'astro:content';
+
+ const blog = defineCollection({
+ schema: z.object({
+ title: z.string(),
+ description: z.string().max(60, 'For SEO purposes, keep descriptions short!'),
+ }),
+ });
+
+ export const collections = { blog };
+ `,
+ '/src/launch-week.ts': `
+ import { getCollection } from 'astro:content';
+
+ export let Content;
+
+ const blog = await getCollection('blog');
+ const launchWeekEntry = blog.find(post => post.id === 'promo/launch-week.mdx');
+ const mod = await launchWeekEntry.render();
+
+ Content = mod.Content;
+ `,
+ '/src/pages/index.astro': `
+ ---
+ import { Content } from '../launch-week.ts';
+ ---
+ <html>
+ <head><title>Testing</title></head>
+ <body>
+ <h1>Testing</h1>
+ <Content />
+ </body>
+ </html>
+ `,
+ });
+
+ await runInContainerWithContentListeners(
+ {
+ inlineConfig: {
+ root: fixture.path,
+ vite: { server: { middlewareMode: true } },
+ },
+ },
+ async (container) => {
+ const { req, res, done, text } = createRequestAndResponse({
+ method: 'GET',
+ url: '/',
+ });
+ container.handle(req, res);
+ await done;
+ const html = await text();
+
+ const $ = cheerio.load(html);
+ // Rendered the content
+ assert.equal($('ul li').length, 3);
+
+ // Rendered the styles
+ assert.equal($('style').length, 1);
+ },
+ );
+ });
+});
diff --git a/packages/astro/test/units/dev/dev.test.js b/packages/astro/test/units/dev/dev.test.js
new file mode 100644
index 000000000..74f1d1e92
--- /dev/null
+++ b/packages/astro/test/units/dev/dev.test.js
@@ -0,0 +1,196 @@
+import * as assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import * as cheerio from 'cheerio';
+import { createFixture, createRequestAndResponse, runInContainer } from '../test-utils.js';
+
+describe('dev container', () => {
+ it('can render requests', async () => {
+ const fixture = await createFixture({
+ '/src/pages/index.astro': `
+ ---
+ const name = 'Testing';
+ ---
+ <html>
+ <head><title>{name}</title></head>
+ <body>
+ <h1>{name}</h1>
+ </body>
+ </html>
+ `,
+ });
+
+ await runInContainer({ inlineConfig: { root: fixture.path } }, async (container) => {
+ const { req, res, text } = createRequestAndResponse({
+ method: 'GET',
+ url: '/',
+ });
+ container.handle(req, res);
+ const html = await text();
+ const $ = cheerio.load(html);
+ assert.equal(res.statusCode, 200);
+ assert.equal($('h1').length, 1);
+ });
+ });
+
+ it('Allows dynamic segments in injected routes', async () => {
+ const fixture = await createFixture({
+ '/src/components/test.astro': `<h1>{Astro.params.slug}</h1>`,
+ '/src/pages/test-[slug].astro': `<h1>{Astro.params.slug}</h1>`,
+ });
+
+ await runInContainer(
+ {
+ inlineConfig: {
+ root: fixture.path,
+ output: 'server',
+ integrations: [
+ {
+ name: '@astrojs/test-integration',
+ hooks: {
+ 'astro:config:setup': ({ injectRoute }) => {
+ injectRoute({
+ pattern: '/another-[slug]',
+ entrypoint: './src/components/test.astro',
+ });
+ },
+ },
+ },
+ ],
+ },
+ },
+ async (container) => {
+ let r = createRequestAndResponse({
+ method: 'GET',
+ url: '/test-one',
+ });
+ container.handle(r.req, r.res);
+ await r.done;
+ assert.equal(r.res.statusCode, 200);
+
+ // Try with the injected route
+ r = createRequestAndResponse({
+ method: 'GET',
+ url: '/another-two',
+ });
+ container.handle(r.req, r.res);
+ await r.done;
+ assert.equal(r.res.statusCode, 200);
+ },
+ );
+ });
+
+ it('Serves injected 404 route for any 404', async () => {
+ const fixture = await createFixture({
+ '/src/components/404.astro': `<h1>Custom 404</h1>`,
+ '/src/pages/page.astro': `<h1>Regular page</h1>`,
+ });
+
+ await runInContainer(
+ {
+ inlineConfig: {
+ root: fixture.path,
+ output: 'server',
+ integrations: [
+ {
+ name: '@astrojs/test-integration',
+ hooks: {
+ 'astro:config:setup': ({ injectRoute }) => {
+ injectRoute({
+ pattern: '/404',
+ entrypoint: './src/components/404.astro',
+ });
+ },
+ },
+ },
+ ],
+ },
+ },
+ async (container) => {
+ {
+ // Regular pages are served as expected.
+ const r = createRequestAndResponse({ method: 'GET', url: '/page' });
+ container.handle(r.req, r.res);
+ await r.done;
+ const doc = await r.text();
+ assert.equal(doc.includes('Regular page'), true);
+ assert.equal(r.res.statusCode, 200);
+ }
+ {
+ // `/404` serves the custom 404 page as expected.
+ const r = createRequestAndResponse({ method: 'GET', url: '/404' });
+ container.handle(r.req, r.res);
+ await r.done;
+ const doc = await r.text();
+ assert.equal(doc.includes('Custom 404'), true);
+ assert.equal(r.res.statusCode, 404);
+ }
+ {
+ // A non-existent page also serves the custom 404 page.
+ const r = createRequestAndResponse({ method: 'GET', url: '/other-page' });
+ container.handle(r.req, r.res);
+ await r.done;
+ const doc = await r.text();
+ assert.equal(doc.includes('Custom 404'), true);
+ assert.equal(r.res.statusCode, 404);
+ }
+ },
+ );
+ });
+
+ it('items in public/ are not available from root when using a base', async () => {
+ const fixture = await createFixture({
+ '/public/test.txt': `Test`,
+ });
+
+ await runInContainer(
+ {
+ inlineConfig: {
+ root: fixture.path,
+ base: '/sub/',
+ },
+ },
+ async (container) => {
+ // First try the subpath
+ let r = createRequestAndResponse({
+ method: 'GET',
+ url: '/sub/test.txt',
+ });
+
+ container.handle(r.req, r.res);
+ await r.done;
+
+ assert.equal(r.res.statusCode, 200);
+
+ // Next try the root path
+ r = createRequestAndResponse({
+ method: 'GET',
+ url: '/test.txt',
+ });
+
+ container.handle(r.req, r.res);
+ await r.done;
+
+ assert.equal(r.res.statusCode, 404);
+ },
+ );
+ });
+
+ it('items in public/ are available from root when not using a base', async () => {
+ const fixture = await createFixture({
+ '/public/test.txt': `Test`,
+ });
+
+ await runInContainer({ inlineConfig: { root: fixture.path } }, async (container) => {
+ // Try the root path
+ let r = createRequestAndResponse({
+ method: 'GET',
+ url: '/test.txt',
+ });
+
+ container.handle(r.req, r.res);
+ await r.done;
+
+ assert.equal(r.res.statusCode, 200);
+ });
+ });
+});
diff --git a/packages/astro/test/units/dev/head-injection.test.js b/packages/astro/test/units/dev/head-injection.test.js
new file mode 100644
index 000000000..fa61cea58
--- /dev/null
+++ b/packages/astro/test/units/dev/head-injection.test.js
@@ -0,0 +1,189 @@
+import * as assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import * as cheerio from 'cheerio';
+import { createFixture, createRequestAndResponse, runInContainer } from '../test-utils.js';
+
+const root = new URL('../../fixtures/alias/', import.meta.url);
+
+describe('head injection', () => {
+ it('Dynamic injection from component created in the page frontmatter', async () => {
+ const fixture = await createFixture(
+ {
+ '/src/components/Other.astro': `
+ <style>
+ div {
+ background: grey;
+ }
+ </style>
+ <div id="other">Other</div>
+ `,
+ '/src/common/head.js': `
+ // astro-head-inject
+ import Other from '../components/Other.astro';
+ import {
+ createComponent,
+ createHeadAndContent,
+ renderComponent,
+ renderTemplate,
+ renderUniqueStylesheet,
+ unescapeHTML
+ } from 'astro/runtime/server/index.js';
+
+ export function renderEntry() {
+ return createComponent({
+ factory(result, props, slots) {
+ return createHeadAndContent(
+ unescapeHTML(renderUniqueStylesheet(result, {
+ type: 'external',
+ src: '/some/fake/styles.css'
+ })),
+ renderTemplate\`$\{renderComponent(result, 'Other', Other, props, slots)}\`
+ );
+ },
+ propagation: 'self'
+ });
+ }
+ `.trim(),
+ '/src/pages/index.astro': `
+ ---
+ import { renderEntry } from '../common/head.js';
+ const Head = renderEntry();
+ ---
+ <html>
+ <head><title>Testing</title></head>
+ <body>
+ <h1>testing</h1>
+ <Head />
+ </body>
+ </html>
+ `,
+ },
+ root,
+ );
+
+ await runInContainer(
+ {
+ inlineConfig: {
+ root: fixture.path,
+ vite: { server: { middlewareMode: true } },
+ },
+ },
+ async (container) => {
+ const { req, res, done, text } = createRequestAndResponse({
+ method: 'GET',
+ url: '/',
+ });
+ container.handle(req, res);
+ await done;
+ const html = await text();
+ const $ = cheerio.load(html);
+
+ assert.equal($('link[rel=stylesheet][href="/some/fake/styles.css"]').length, 1);
+ assert.equal($('#other').length, 1);
+ },
+ );
+ });
+
+ it('Dynamic injection from a layout component', async () => {
+ const fixture = await createFixture(
+ {
+ '/src/components/Other.astro': `
+ <style>
+ div {
+ background: grey;
+ }
+ </style>
+ <div id="other">Other</div>
+ `,
+ '/src/common/head.js': `
+ // astro-head-inject
+ import Other from '../components/Other.astro';
+ import {
+ createComponent,
+ createHeadAndContent,
+ renderComponent,
+ renderTemplate,
+ renderUniqueStylesheet,
+ unescapeHTML,
+ } from 'astro/runtime/server/index.js';
+
+ export function renderEntry() {
+ return createComponent({
+ factory(result, props, slots) {
+ return createHeadAndContent(
+ unescapeHTML(renderUniqueStylesheet(result, {
+ type: 'external',
+ src: '/some/fake/styles.css'
+ })),
+ renderTemplate\`$\{renderComponent(result, 'Other', Other, props, slots)}\`
+ );
+ },
+ propagation: 'self'
+ });
+ }
+ `.trim(),
+ '/src/components/Content.astro': `
+ ---
+ import { renderEntry } from '../common/head.js';
+ const ExtraHead = renderEntry();
+ ---
+ <ExtraHead />
+ `,
+ '/src/components/Inner.astro': `
+ ---
+ import Content from './Content.astro';
+ ---
+ <Content />
+ `,
+ '/src/components/Layout.astro': `
+ <html>
+ <head>
+ <title>Normal head stuff</title>
+ </head>
+ <body>
+ <slot name="title" />
+ <slot name="inner" />
+ </body>
+ </html>
+ `,
+ '/src/pages/index.astro': `
+ ---
+ import Layout from '../components/Layout.astro';
+ import Inner from '../components/Inner.astro';
+ ---
+ <Layout>
+ <h1 slot="title">Test page</h1>
+ <Inner slot="inner" />
+ </Layout>
+ `,
+ },
+ root,
+ );
+
+ await runInContainer(
+ {
+ inlineConfig: {
+ root: fixture.path,
+ vite: { server: { middlewareMode: true } },
+ },
+ },
+ async (container) => {
+ const { req, res, done, text } = createRequestAndResponse({
+ method: 'GET',
+ url: '/',
+ });
+ container.handle(req, res);
+ await done;
+ const html = await text();
+ const $ = cheerio.load(html);
+
+ assert.equal(
+ $('link[rel=stylesheet][href="/some/fake/styles.css"]').length,
+ 1,
+ 'found inner link',
+ );
+ assert.equal($('#other').length, 1, 'Found the #other div');
+ },
+ );
+ });
+});
diff --git a/packages/astro/test/units/dev/hydration.test.js b/packages/astro/test/units/dev/hydration.test.js
new file mode 100644
index 000000000..03962a416
--- /dev/null
+++ b/packages/astro/test/units/dev/hydration.test.js
@@ -0,0 +1,50 @@
+import * as assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import { createFixture, createRequestAndResponse, runInContainer } from '../test-utils.js';
+
+describe('hydration', () => {
+ it(
+ 'should not crash when reassigning a hydrated component',
+ { skip: true, todo: "It seems that `components/Client.svelte` isn't found" },
+ async () => {
+ const fixture = await createFixture({
+ '/src/pages/index.astro': `
+ ---
+ import Svelte from '../components/Client.svelte';
+ const Foo = Svelte;
+ const Bar = Svelte;
+ ---
+ <html>
+ <head><title>testing</title></head>
+ <body>
+ <Foo client:load />
+ <Bar client:load />
+ </body>
+ </html>
+ `,
+ });
+
+ await runInContainer(
+ {
+ inlineConfig: {
+ root: fixture.path,
+ logLevel: 'silent',
+ },
+ },
+ async (container) => {
+ const { req, res, done } = createRequestAndResponse({
+ method: 'GET',
+ url: '/',
+ });
+ container.handle(req, res);
+ await done;
+ assert.equal(
+ res.statusCode,
+ 200,
+ "We get a 200 because the error occurs in the template, but we didn't crash!",
+ );
+ },
+ );
+ },
+ );
+});
diff --git a/packages/astro/test/units/dev/restart.test.js b/packages/astro/test/units/dev/restart.test.js
new file mode 100644
index 000000000..9d5664cbf
--- /dev/null
+++ b/packages/astro/test/units/dev/restart.test.js
@@ -0,0 +1,233 @@
+import * as assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import * as cheerio from 'cheerio';
+
+import {
+ createContainerWithAutomaticRestart,
+ startContainer,
+} from '../../../dist/core/dev/index.js';
+import { createFixture, createRequestAndResponse } from '../test-utils.js';
+
+/** @type {import('astro').AstroInlineConfig} */
+const defaultInlineConfig = {
+ logLevel: 'silent',
+};
+
+function isStarted(container) {
+ return !!container.viteServer.httpServer?.listening;
+}
+
+// Checking for restarts may hang if no restarts happen, so set a 20s timeout for each test
+describe('dev container restarts', { timeout: 20000 }, () => {
+ it('Surfaces config errors on restarts', async () => {
+ const fixture = await createFixture({
+ '/src/pages/index.astro': `
+ <html>
+ <head><title>Test</title></head>
+ <body>
+ <h1>Test</h1>
+ </body>
+ </html>
+ `,
+ '/astro.config.mjs': ``,
+ });
+
+ const restart = await createContainerWithAutomaticRestart({
+ inlineConfig: {
+ ...defaultInlineConfig,
+ root: fixture.path,
+ },
+ });
+
+ try {
+ let r = createRequestAndResponse({
+ method: 'GET',
+ url: '/',
+ });
+ restart.container.handle(r.req, r.res);
+ let html = await r.text();
+ const $ = cheerio.load(html);
+ assert.equal(r.res.statusCode, 200);
+ assert.equal($('h1').length, 1);
+
+ // Create an error
+ let restartComplete = restart.restarted();
+ await fixture.writeFile('/astro.config.mjs', 'const foo = bar');
+ // TODO: fix this hack
+ restart.container.viteServer.watcher.emit(
+ 'change',
+ fixture.getPath('/astro.config.mjs').replace(/\\/g, '/'),
+ );
+
+ // Wait for the restart to finish
+ let hmrError = await restartComplete;
+ assert.ok(hmrError instanceof Error);
+
+ // Do it a second time to make sure we are still watching
+
+ restartComplete = restart.restarted();
+ await fixture.writeFile('/astro.config.mjs', 'const foo = bar2');
+ // TODO: fix this hack
+ restart.container.viteServer.watcher.emit(
+ 'change',
+ fixture.getPath('/astro.config.mjs').replace(/\\/g, '/'),
+ );
+
+ hmrError = await restartComplete;
+ assert.ok(hmrError instanceof Error);
+ } finally {
+ await restart.container.close();
+ }
+ });
+
+ it('Restarts the container if previously started', async () => {
+ const fixture = await createFixture({
+ '/src/pages/index.astro': `
+ <html>
+ <head><title>Test</title></head>
+ <body>
+ <h1>Test</h1>
+ </body>
+ </html>
+ `,
+ '/astro.config.mjs': ``,
+ });
+
+ const restart = await createContainerWithAutomaticRestart({
+ inlineConfig: {
+ ...defaultInlineConfig,
+ root: fixture.path,
+ },
+ });
+ await startContainer(restart.container);
+ assert.equal(isStarted(restart.container), true);
+
+ try {
+ // Trigger a change
+ let restartComplete = restart.restarted();
+ await fixture.writeFile('/astro.config.mjs', '');
+ // TODO: fix this hack
+ restart.container.viteServer.watcher.emit(
+ 'change',
+ fixture.getPath('/astro.config.mjs').replace(/\\/g, '/'),
+ );
+ await restartComplete;
+
+ assert.equal(isStarted(restart.container), true);
+ } finally {
+ await restart.container.close();
+ }
+ });
+
+ it('Is able to restart project using Tailwind + astro.config.ts', async () => {
+ const fixture = await createFixture({
+ '/src/pages/index.astro': ``,
+ '/astro.config.ts': ``,
+ });
+
+ const restart = await createContainerWithAutomaticRestart({
+ inlineConfig: {
+ ...defaultInlineConfig,
+ root: fixture.path,
+ },
+ });
+ await startContainer(restart.container);
+ assert.equal(isStarted(restart.container), true);
+
+ try {
+ // Trigger a change
+ let restartComplete = restart.restarted();
+ await fixture.writeFile('/astro.config.ts', '');
+ // TODO: fix this hack
+ restart.container.viteServer.watcher.emit(
+ 'change',
+ fixture.getPath('/astro.config.mjs').replace(/\\/g, '/'),
+ );
+ await restartComplete;
+
+ assert.equal(isStarted(restart.container), true);
+ } finally {
+ await restart.container.close();
+ }
+ });
+
+ it('Is able to restart project on package.json changes', async () => {
+ const fixture = await createFixture({
+ '/src/pages/index.astro': ``,
+ });
+
+ const restart = await createContainerWithAutomaticRestart({
+ inlineConfig: {
+ ...defaultInlineConfig,
+ root: fixture.path,
+ },
+ });
+ await startContainer(restart.container);
+ assert.equal(isStarted(restart.container), true);
+
+ try {
+ let restartComplete = restart.restarted();
+ await fixture.writeFile('/package.json', `{}`);
+ // TODO: fix this hack
+ restart.container.viteServer.watcher.emit(
+ 'change',
+ fixture.getPath('/package.json').replace(/\\/g, '/'),
+ );
+ await restartComplete;
+ } finally {
+ await restart.container.close();
+ }
+ });
+
+ it('Is able to restart on viteServer.restart API call', async () => {
+ const fixture = await createFixture({
+ '/src/pages/index.astro': ``,
+ });
+
+ const restart = await createContainerWithAutomaticRestart({
+ inlineConfig: {
+ ...defaultInlineConfig,
+ root: fixture.path,
+ },
+ });
+ await startContainer(restart.container);
+ assert.equal(isStarted(restart.container), true);
+
+ try {
+ let restartComplete = restart.restarted();
+ await restart.container.viteServer.restart();
+ await restartComplete;
+ } finally {
+ await restart.container.close();
+ }
+ });
+
+ it('Is able to restart project on .astro/settings.json changes', async () => {
+ const fixture = await createFixture({
+ '/src/pages/index.astro': ``,
+ '/.astro/settings.json': `{}`,
+ });
+
+ const restart = await createContainerWithAutomaticRestart({
+ inlineConfig: {
+ ...defaultInlineConfig,
+ root: fixture.path,
+ },
+ });
+ await startContainer(restart.container);
+ assert.equal(isStarted(restart.container), true);
+
+ try {
+ let restartComplete = restart.restarted();
+ await fixture.writeFile('/.astro/settings.json', `{ }`);
+ // TODO: fix this hack
+ restart.container.viteServer.watcher.emit(
+ 'change',
+ fixture.getPath('/.astro/settings.json').replace(/\\/g, '/'),
+ );
+ await restartComplete;
+ } finally {
+ await restart.container.close();
+ }
+ });
+});
diff --git a/packages/astro/test/units/dev/styles.test.js b/packages/astro/test/units/dev/styles.test.js
new file mode 100644
index 000000000..3b674108d
--- /dev/null
+++ b/packages/astro/test/units/dev/styles.test.js
@@ -0,0 +1,81 @@
+import * as assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import { viteID } from '../../../dist/core/util.js';
+import { getStylesForURL } from '../../../dist/vite-plugin-astro-server/css.js';
+
+const root = new URL('../../fixtures/alias/', import.meta.url);
+
+class TestLoader {
+ constructor(modules) {
+ this.modules = new Map(modules.map((m) => [m.id, m]));
+ }
+ getModuleById(id) {
+ return this.modules.get(id);
+ }
+ getModulesByFile(id) {
+ return this.modules.has(id) ? [this.modules.get(id)] : [];
+ }
+ import(id) {
+ // try to normalize inline CSS requests so we can map to the existing modules value
+ id = id.replace(/(\?|&)inline=?(&|$)/, (_, start, end) => (end ? start : '')).replace(/=$/, '');
+ for (const mod of this.modules.values()) {
+ for (const importedMod of mod.importedModules) {
+ if (importedMod.id === id) {
+ return importedMod.ssrModule;
+ }
+ }
+ }
+ }
+}
+
+describe('Crawling graph for CSS', () => {
+ let loader;
+ before(() => {
+ const indexId = viteID(new URL('./src/pages/index.astro', root));
+ const aboutId = viteID(new URL('./src/pages/about.astro', root));
+ loader = new TestLoader([
+ {
+ id: indexId,
+ importedModules: [
+ {
+ id: aboutId,
+ url: aboutId,
+ importers: new Set(),
+ },
+ {
+ id: indexId + '?astro&style.css',
+ url: indexId + '?astro&style.css',
+ importers: new Set([{ id: indexId }]),
+ ssrModule: { default: '.index {}' },
+ },
+ ],
+ importers: new Set(),
+ ssrTransformResult: {
+ deps: [indexId + '?astro&style.css'],
+ },
+ },
+ {
+ id: aboutId,
+ importedModules: [
+ {
+ id: aboutId + '?astro&style.css',
+ url: aboutId + '?astro&style.css',
+ importers: new Set([{ id: aboutId }]),
+ ssrModule: { default: '.about {}' },
+ },
+ ],
+ importers: new Set(),
+ ssrTransformResult: {
+ deps: [aboutId + '?astro&style.css'],
+ },
+ },
+ ]);
+ });
+
+ it("importedModules is checked against the child's importers", async () => {
+ // In dev mode, HMR modules tracked are added to importedModules. We use `importers`
+ // to verify that they are true importers.
+ const res = await getStylesForURL(new URL('./src/pages/index.astro', root), loader);
+ assert.equal(res.styles.length, 1);
+ });
+});