diff options
Diffstat (limited to 'packages/astro/test/units/dev')
-rw-r--r-- | packages/astro/test/units/dev/base.test.js | 112 | ||||
-rw-r--r-- | packages/astro/test/units/dev/collections-mixed-content-errors.test.js | 147 | ||||
-rw-r--r-- | packages/astro/test/units/dev/collections-renderentry.test.js | 298 | ||||
-rw-r--r-- | packages/astro/test/units/dev/dev.test.js | 196 | ||||
-rw-r--r-- | packages/astro/test/units/dev/head-injection.test.js | 189 | ||||
-rw-r--r-- | packages/astro/test/units/dev/hydration.test.js | 50 | ||||
-rw-r--r-- | packages/astro/test/units/dev/restart.test.js | 233 | ||||
-rw-r--r-- | packages/astro/test/units/dev/styles.test.js | 81 |
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); + }); +}); |