Update tests

This commit is contained in:
Nate Moore 2021-09-10 22:11:34 +00:00 committed by Drew Powers
parent ddca8c4dea
commit d8cd8a46ee
118 changed files with 4306 additions and 3217 deletions

View file

@ -1,6 +1,6 @@
---
// Component Imports
import Counter from '../components/Counter.jsx'
import Counter from '../components/Counter.tsx'
// Full Astro Component Syntax:

View file

@ -1,5 +1,6 @@
---
---
<html lang="en">
<head>
@ -7,6 +8,7 @@
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width" />
<title>Welcome to Astro</title>
</head>
<body>

View file

@ -44,7 +44,7 @@
},
"devDependencies": {
"@changesets/cli": "^2.16.0",
"@octokit/action": "^3.15.4",
"@types/jest": "^27.0.1",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.18.0",
"autoprefixer": "^10.2.6",
@ -56,11 +56,11 @@
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"execa": "^5.0.0",
"jest": "^27.1.0",
"lerna": "^4.0.0",
"prettier": "^2.3.2",
"tiny-glob": "^0.2.8",
"typescript": "^4.4.2",
"uvu": "^0.5.1"
"typescript": "^4.4.2"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"

View file

@ -1,12 +1,14 @@
import fetch from 'node-fetch';
---
import { renderMarkdown } from '@astrojs/markdown-support';
import { __astro_slot } from 'astro/runtime/__astro_slot.js';
if(!('fetch' in globalThis)) {
globalThis.fetch = fetch;
export interface Props {
content?: string;
}
// Internal props that should not be part of the external interface.
interface InternalProps extends Props {
$scope: string;
}
const __TopLevelAstro = {
site: new URL("http://localhost:3000"),
@ -64,11 +66,11 @@ async function __render(props, ...children) {
$scope
} = Astro.props;
let html = null;
// This flow is only triggered if a user passes `<Markdown content={content} />`
if (content) {
const {
content: htmlContent
} = await renderMarkdown(content, {
mode: "md",
const { content: htmlContent } = await renderMarkdown(content, {
mode: 'md',
$: {
scopedClassName: $scope
}
@ -76,55 +78,9 @@ if (content) {
html = htmlContent;
}
return h(Fragment, null, h(Fragment, null,(html ? html : h(Fragment, null, h(__astro_slot, { [__astroContext]: props[__astroContext] }, children)))));
}
export default { isAstroComponent: true, __render };
// `__renderPage()`: Render the contents of the Astro module as a page. This is a special flow,
// triggered by loading a component directly by URL.
export async function __renderPage({request, children, props, css}) {
const currentChild = {
isAstroComponent: true,
layout: typeof __layout === 'undefined' ? undefined : __layout,
content: typeof __content === 'undefined' ? undefined : __content,
__render,
};
const isLayout = (__astroContext in props);
if(!isLayout) {
let astroRootUIDCounter = 0;
Object.defineProperty(props, __astroContext, {
value: {
pageCSS: css,
request,
createAstroRootUID(seed) { return seed + astroRootUIDCounter++; },
},
writable: false,
enumerable: false
});
}
Object.defineProperty(props, __astroInternal, {
value: {
isPage: !isLayout
},
writable: false,
enumerable: false
});
const childBodyResult = await currentChild.__render(props, children);
// find layout, if one was given.
if (currentChild.layout) {
return currentChild.layout({
request,
props: {content: currentChild.content, [__astroContext]: props[__astroContext]},
children: [childBodyResult],
});
}
return childBodyResult;
};
/*
If we have rendered `html` for `content`, render that
Otherwise, just render the slotted content
*/
---
{html ? html : <slot />}

View file

@ -17,7 +17,8 @@
"./debug": "./components/Debug.astro",
"./components/*": "./components/*",
"./package.json": "./package.json",
"./runtime/*": "./dist/runtime/*"
"./runtime/*": "./dist/runtime/*.js",
"./internal": "./dist/internal/index.js"
},
"imports": {
"#astro/*": "./dist/*.js"
@ -35,12 +36,12 @@
"dev": "astro-scripts dev \"src/**/*.ts\"",
"postbuild": "astro-scripts copy \"src/**/*.astro\"",
"benchmark": "node test/benchmark/dev.bench.js && node test/benchmark/build.bench.js",
"test": "uvu test -i fixtures -i benchmark -i test-utils.js"
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
},
"dependencies": {
"@astrojs/compiler": "^0.1.0-canary.36",
"@astrojs/language-server": "^0.7.16",
"@astrojs/markdown-support": "0.3.1",
"@astrojs/parser": "0.20.2",
"@astrojs/prism": "0.2.2",
"@astrojs/renderer-preact": "0.2.2",
"@astrojs/renderer-react": "0.2.2",
@ -68,10 +69,12 @@
"es-module-lexer": "^0.7.1",
"esbuild": "^0.12.23",
"estree-util-value-to-estree": "^1.2.0",
"etag": "^1.8.1",
"fast-xml-parser": "^3.19.0",
"fdir": "^5.1.0",
"kleur": "^4.1.4",
"mime": "^2.5.2",
"morphdom": "^2.6.1",
"node-fetch": "^2.6.1",
"path-to-regexp": "^6.2.0",
"picomatch": "^2.3.0",
@ -85,7 +88,7 @@
"srcset-parse": "^1.1.0",
"string-width": "^5.0.0",
"supports-esm": "^1.0.0",
"vite": "^2.5.1",
"vite": "^2.5.2",
"yargs-parser": "^20.2.9",
"zod": "^3.8.1"
},
@ -93,6 +96,7 @@
"@astrojs/parser": "^0.20.2",
"@types/babel__core": "^7.1.15",
"@types/connect": "^3.4.35",
"@types/etag": "^1.8.1",
"@types/mime": "^2.0.3",
"@types/node-fetch": "^2.5.12",
"@types/send": "^0.17.1",

View file

@ -3,15 +3,16 @@ import type babel from '@babel/core';
import type vite from 'vite';
import type { z } from 'zod';
import type { AstroConfigSchema } from '../config';
import type { AstroComponentFactory } from '../internal';
export { AstroMarkdownOptions };
export interface AstroComponentMetadata {
displayName: string;
hydrate?: 'load' | 'idle' | 'visible' | 'media' | 'only';
hydrateArgs?: any;
componentUrl?: string;
componentExport?: { value: string; namespace?: boolean };
value?: undefined | string;
}
/**
@ -140,12 +141,7 @@ export interface CollectionRSS {
/** Generic interface for a component (Astro, Svelte, React, etc.) */
export interface ComponentInstance {
default: {
isAstroComponent: boolean;
__render?(props: Props, ...children: any[]): string;
__renderer?: Renderer;
};
__renderPage?: (options: RenderPageOptions) => string;
default: AstroComponentFactory;
css?: string[];
getStaticPaths?: (options: GetStaticPathsOptions) => GetStaticPathsResult;
}

45
packages/astro/src/@types/compiler.d.ts vendored Normal file
View file

@ -0,0 +1,45 @@
declare module '@astrojs/compiler' {
export type Platform = 'browser' | 'node' | 'neutral';
export type SourceMap = any;
export interface TransformOptions {}
export interface TransformResult {
code: string;
map: SourceMap;
warnings: any[];
}
export interface TransformResults {
js: TransformResult;
css?: TransformResult;
}
// This function transforms a single JavaScript file. It can be used to minify
// JavaScript, convert TypeScript/JSX to JavaScript, or convert newer JavaScript
// to older JavaScript. It returns a promise that is either resolved with a
// "TransformResult" object or rejected with a "TransformFailure" object.
//
// Works in node: yes
// Works in browser: yes
export declare function transform(input: string, options?: TransformOptions): Promise<string>;
// This configures the browser-based version of astro. It is necessary to
// call this first and wait for the returned promise to be resolved before
// making other API calls when using astro in the browser.
//
// Works in node: yes
// Works in browser: yes ("options" is required)
export declare function initialize(options: InitializeOptions): Promise<void>;
export interface InitializeOptions {
// The URL of the "astro.wasm" file. This must be provided when running
// astro in the browser.
wasmURL?: string;
// By default astro runs the WebAssembly-based browser API in a web worker
// to avoid blocking the UI thread. This can be disabled by setting "worker"
// to false.
worker?: boolean;
}
}

View file

@ -75,7 +75,7 @@ class AstroBuilder {
// static pages
if (pathname) {
allPages.push(
ssr({ filePath, logging, mode: 'production', origin, route, routeCache: this.routeCache, pathname, viteServer }).then((html) => ({
ssr({ astroConfig: this.config, filePath, logging, mode: 'production', origin, route, routeCache: this.routeCache, pathname, viteServer }).then((html) => ({
html,
name: pathname.replace(/\/?$/, '/index.html').replace(/^\//, ''),
}))
@ -97,7 +97,7 @@ class AstroBuilder {
// TODO: throw error if conflict
staticPaths.paths.forEach((staticPath) => {
allPages.push(
ssr({ filePath, logging, mode: 'production', origin, route, routeCache: this.routeCache, pathname: staticPath, viteServer }).then((html) => ({
ssr({ astroConfig: this.config, filePath, logging, mode: 'production', origin, route, routeCache: this.routeCache, pathname: staticPath, viteServer }).then((html) => ({
html,
name: staticPath.replace(/\/?$/, '/index.html').replace(/^\//, ''),
}))

View file

@ -10,7 +10,7 @@ import { z } from 'zod';
import { defaultLogDestination } from '../logger.js';
import build from '../build/index.js';
import devServer from '../dev/index.js';
import { preview } from '../preview/index.js';
import preview from '../preview/index.js';
import { reload } from './reload.js';
import { check } from './check.js';
import { formatConfigError, loadConfig } from '../config.js';

View file

@ -1,13 +1,18 @@
import type { NextFunction } from 'connect';
import type http from 'http';
import type { AstroConfig, ManifestData, RouteCache } from '../@types/astro';
import type { AstroConfig, ManifestData, RouteCache, RouteData } from '../@types/astro';
import type { LogOptions } from '../logger';
import type { HmrContext, ModuleNode } from 'vite';
import chokidar from 'chokidar';
import connect from 'connect';
import mime from 'mime';
import getEtag from 'etag';
import { performance } from 'perf_hooks';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
import path from 'path';
import { promises as fs } from 'fs';
import vite from 'vite';
import { defaultLogOptions, error, info } from '../logger.js';
import { createRouteManifest, matchRoute } from '../runtime/routing.js';
@ -16,6 +21,8 @@ import { loadViteConfig } from '../runtime/vite/config.js';
import * as msg from './messages.js';
import { errorTemplate } from './template/error.js';
const require = createRequire(import.meta.url);
export interface DevOptions {
logging: LogOptions;
}
@ -39,16 +46,18 @@ export default async function dev(config: AstroConfig, options: DevOptions = { l
hostname: server.hostname,
port: server.port,
server: server.app,
stop: server.stop,
stop: () => server.stop(),
};
}
/** Dev server */
class AstroDevServer {
export class AstroDevServer {
app = connect();
httpServer: http.Server | undefined;
hostname: string;
port: number;
private internalCache: Map<string, string>;
private config: AstroConfig;
private logging: LogOptions;
private manifest: ManifestData;
@ -56,8 +65,10 @@ class AstroDevServer {
private routeCache: RouteCache = {};
private viteServer: vite.ViteDevServer | undefined;
private watcher: chokidar.FSWatcher;
private mostRecentRoute?: RouteData;
constructor(config: AstroConfig, options: DevOptions) {
this.internalCache = new Map();
this.config = config;
this.hostname = config.devOptions.hostname || 'localhost';
this.logging = options.logging;
@ -94,7 +105,7 @@ class AstroDevServer {
host: this.hostname,
},
},
{ astroConfig: this.config, logging: this.logging }
{ astroConfig: this.config, logging: this.logging, devServer: this }
);
this.viteServer = await vite.createServer(viteConfig);
@ -104,27 +115,24 @@ class AstroDevServer {
this.app.use((req, res, next) => this.renderError(req, res, next));
// 4. listen on port
await new Promise<void>((resolve, reject) => {
this.app
.listen(this.port, this.hostname, () => {
this.httpServer = this.app.listen(this.port, this.hostname, () => {
info(this.logging, 'astro', msg.devStart({ startupTime: performance.now() - devStart }));
info(this.logging, 'astro', msg.devHost({ host: `http://${this.hostname}:${this.port}` }));
resolve();
})
.on('error', (err: NodeJS.ErrnoException) => {
});
this.httpServer.on('error', (err: NodeJS.ErrnoException) => {
if (err.code && err.code === 'EADDRINUSE') {
error(this.logging, 'astro', `Address ${this.hostname}:${this.port} already in use. Try changing devOptions.port in your config file`);
} else {
error(this.logging, 'astro', err.stack);
}
reject();
process.exit(1);
});
});
}
/** Stop dev server */
async stop() {
this.internalCache = new Map();
if (this.httpServer) this.httpServer.close(); // close HTTP server
await Promise.all([
...(this.viteServer ? [this.viteServer.close()] : []), // close Vite server
this.watcher.close(), // close chokidar
@ -138,6 +146,13 @@ class AstroDevServer {
let pathname = req.url || '/'; // original request
const reqStart = performance.now();
if (pathname.startsWith('/@astro')) {
const spec = pathname.slice(2);
const url = await this.viteServer.moduleGraph.resolveUrl(spec);
req.url = url[1];
return this.viteServer.middlewares.handle(req, res, next);
}
try {
const route = matchRoute(pathname, this.manifest);
@ -147,8 +162,11 @@ class AstroDevServer {
return;
}
this.mostRecentRoute = route;
// handle .astro and .md pages
const html = await ssr({
astroConfig: this.config,
filePath: new URL(`./${route.component}`, this.config.projectRoot),
logging: this.logging,
mode: 'development',
@ -168,7 +186,7 @@ class AstroDevServer {
} catch (e) {
const err = e as Error;
this.viteServer.ssrFixStacktrace(err);
console.error(err.stack);
console.log(err.stack);
const statusCode = 500;
const html = errorTemplate({ statusCode, title: 'Internal Error', tabTitle: '500: Error', message: err.message });
info(this.logging, 'astro', msg.req({ url: pathname, statusCode: 500, reqTime: performance.now() - reqStart }));
@ -195,6 +213,7 @@ class AstroDevServer {
const userDefined404 = this.manifest.routes.find((route) => route.component === relPages + '404.astro');
if (userDefined404) {
html = await ssr({
astroConfig: this.config,
filePath: new URL(`./${userDefined404.component}`, this.config.projectRoot),
logging: this.logging,
mode: 'development',
@ -216,4 +235,53 @@ class AstroDevServer {
res.write(html);
res.end();
}
public async handleHotUpdate({ file, modules }: HmrContext): Promise<void | ModuleNode[]> {
if (!this.viteServer) throw new Error(`AstroDevServer.start() not called`);
for (const module of modules) {
this.viteServer.moduleGraph.invalidateModule(module);
}
const route = this.mostRecentRoute;
const pathname = route?.pathname ?? '/';
if (!route) {
this.viteServer.ws.send({
type: 'full-reload',
});
return [];
}
try {
// try to update the most recent route
const html = await ssr({
astroConfig: this.config,
filePath: new URL(`./${route.component}`, this.config.projectRoot),
logging: this.logging,
mode: 'development',
origin: this.origin,
pathname,
route,
routeCache: this.routeCache,
viteServer: this.viteServer,
});
// TODO: log update
this.viteServer.ws.send({
type: 'custom',
event: 'astro:reload',
data: { html },
});
return [];
} catch (e) {
const err = e as Error;
this.viteServer.ssrFixStacktrace(err);
console.log(err.stack);
this.viteServer.ws.send({
type: 'full-reload',
});
return [];
}
}
}

View file

@ -15,6 +15,12 @@ export function req({ url, statusCode, reqTime }: { url: string; statusCode: num
return `${color(statusCode)} ${pad(url, 40)} ${dim(Math.round(reqTime) + 'ms')}`;
}
/** Display */
export function reload({ url, reqTime }: { url: string; reqTime: number }): string {
let color = yellow;
return `${pad(url, 40)} ${dim(Math.round(reqTime) + 'ms')}`;
}
/** Display dev server host and startup time */
export function devStart({ startupTime }: { startupTime: number }): string {
return `${pad(`Server started`, 44)} ${dim(`${Math.round(startupTime)}ms`)}`;

View file

@ -0,0 +1,211 @@
import type { AstroComponentMetadata } from '../@types/astro';
import { renderAstroComponent } from '../runtime/ssr.js';
import { valueToEstree, Value } from 'estree-util-value-to-estree';
import * as astring from 'astring';
import shorthash from 'shorthash';
const { generate, GENERATOR } = astring;
// A more robust version alternative to `JSON.stringify` that can handle most values
// see https://github.com/remcohaszing/estree-util-value-to-estree#readme
const customGenerator: astring.Generator = {
...GENERATOR,
Literal(node, state) {
if (node.raw != null) {
// escape closing script tags in strings so browsers wouldn't interpret them as
// closing the actual end tag in HTML
state.write(node.raw.replace('</script>', '<\\/script>'));
} else {
GENERATOR.Literal(node, state);
}
},
};
const serialize = (value: Value) =>
generate(valueToEstree(value), {
generator: customGenerator,
});
async function _render(child: any) {
// Special: If a child is a function, call it automatically.
// This lets you do {() => ...} without the extra boilerplate
// of wrapping it in a function and calling it.
if (typeof child === 'function') {
return await child();
} else if (typeof child === 'string') {
return child;
} else if (!child && child !== 0) {
// do nothing, safe to ignore falsey values.
} else {
return child;
}
}
export class AstroComponent {
private htmlParts: string[];
private expressions: TemplateStringsArray;
constructor(htmlParts: string[], expressions: TemplateStringsArray) {
this.htmlParts = htmlParts;
this.expressions = expressions;
}
*[Symbol.iterator]() {
const { htmlParts, expressions } = this;
for (let i = 0; i < htmlParts.length; i++) {
const html = htmlParts[i];
const expression = expressions[i];
yield _render(html);
yield _render(expression);
}
}
}
export function render(htmlParts: string[], ...expressions: TemplateStringsArray) {
return new AstroComponent(htmlParts, expressions);
}
export interface AstroComponentFactory {
(result: any, props: any, slots: any): ReturnType<typeof render>;
isAstroComponentFactory?: boolean;
}
export const createComponent = (cb: AstroComponentFactory) => {
// Add a flag to this callback to mark it as an Astro component
(cb as any).isAstroComponentFactory = true;
return cb;
}
function extractHydrationDirectives(inputProps: Record<string | number, any>): { hydrationDirective: [string, any] | null; props: Record<string | number, any> } {
let props: Record<string | number, any> = {};
let hydrationDirective: [string, any] | null = null;
for (const [key, value] of Object.entries(inputProps)) {
if (key.startsWith('client:')) {
hydrationDirective = [key.split(':')[1], value];
} else {
props[key] = value;
}
}
return { hydrationDirective, props };
}
interface HydrateScriptOptions {
renderer: any;
astroId: string;
props: any;
}
/** For hydrated components, generate a <script type="module"> to load the component */
async function generateHydrateScript(scriptOptions: HydrateScriptOptions, metadata: Required<AstroComponentMetadata>) {
const { renderer, astroId, props } = scriptOptions;
const { hydrate, componentUrl, componentExport } = metadata;
if (!componentExport) {
throw new Error(`Unable to resolve a componentExport for "${metadata.displayName}"! Please open an issue.`)
}
let hydrationSource = '';
if (renderer.hydrationPolyfills) {
hydrationSource += `await Promise.all([${renderer.hydrationPolyfills.map((src: string) => `\n import("${src}")`).join(', ')}]);\n`;
}
hydrationSource += renderer.source
? `const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${componentUrl}"), import("${renderer.source}")]);
return (el, children) => hydrate(el)(Component, ${serialize(props)}, children);
`
: `await import("${componentUrl}");
return () => {};
`;
const hydrationScript = `<script type="module">
import setup from 'astro/client/${hydrate}.js';
setup("${astroId}", {${metadata.hydrateArgs ? `value: ${JSON.stringify(metadata.hydrateArgs)}` : ''}}, async () => {
${hydrationSource}
});
</script>
`;
return hydrationScript;
}
export const renderComponent = async (result: any, displayName: string, Component: unknown, _props: Record<string | number, any>, children: any) => {
Component = await Component;
// children = await renderGenerator(children);
const { renderers } = result._metadata;
if (Component && (Component as any).isAstroComponentFactory) {
const output = await renderAstroComponent(await (Component as any)(result, Component, _props, children))
return output;
}
let metadata: AstroComponentMetadata = { displayName };
if (Component == null) {
throw new Error(`Unable to render ${metadata.displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`);
}
// else if (typeof Component === 'string' && !isCustomElementTag(Component)) {
// throw new Error(`Astro is unable to render ${metadata.displayName}!\nIs there a renderer to handle this type of component defined in your Astro config?`);
// }
const { hydrationDirective, props } = extractHydrationDirectives(_props);
let html = '';
if (!hydrationDirective) {
return '<pre>Not implemented</pre>';
}
metadata.hydrate = hydrationDirective[0] as AstroComponentMetadata['hydrate'];
metadata.hydrateArgs = hydrationDirective[1];
for (const [url, exported] of Object.entries(result._metadata.importedModules)) {
for (const [key, value] of Object.entries(exported as any)) {
if (Component === value) {
metadata.componentExport = { value: key };
metadata.componentUrl = url;
break;
}
}
}
let renderer = null;
for (const r of renderers) {
if (await r.ssr.check(Component, props, null)) {
renderer = r;
}
}
({ html } = await renderer.ssr.renderToStaticMarkup(Component, props, null));
const astroId = shorthash.unique(html);
result.scripts.add(await generateHydrateScript({ renderer, astroId, props }, metadata as Required<AstroComponentMetadata>));
return `<astro-root uid="${astroId}">${html}</astro-root>`;
};
export const addAttribute = (value: any, key: string) => {
if (value == null || value === false) {
return '';
}
return ` ${key}="${value}"`;
};
export const spreadAttributes = (values: Record<any, any>) => {
let output = '';
for (const [key, value] of Object.entries(values)) {
output += addAttribute(value, key);
}
return output;
};
export const defineStyleVars = (astroId: string, vars: Record<any, any>) => {
let output = '\n';
for (const [key, value] of Object.entries(vars)) {
output += ` --${key}: ${value};\n`;
}
return `.${astroId} {${output}}`;
};
export const defineScriptVars = (vars: Record<any, any>) => {
let output = '';
for (const [key, value] of Object.entries(vars)) {
output += `let ${key} = ${JSON.stringify(value)};\n`;
}
return output;
};

View file

@ -13,7 +13,7 @@ interface PreviewOptions {
}
/** The primary dev action */
export async function preview(config: AstroConfig, { logging }: PreviewOptions) {
export default async function preview(config: AstroConfig, { logging }: PreviewOptions) {
const {
dist,
devOptions: { hostname, port },

View file

@ -1,242 +0,0 @@
import type { AstroComponentMetadata, ComponentInstance, Renderer } from '../@types/astro';
import hash from 'shorthash';
import { valueToEstree, Value } from 'estree-util-value-to-estree';
import * as astring from 'astring';
import * as astroHtml from './html.js';
const { generate, GENERATOR } = astring;
// A more robust version alternative to `JSON.stringify` that can handle most values
// see https://github.com/remcohaszing/estree-util-value-to-estree#readme
const customGenerator: astring.Generator = {
...GENERATOR,
Literal(node, state) {
if (node.raw != null) {
// escape closing script tags in strings so browsers wouldn't interpret them as
// closing the actual end tag in HTML
state.write(node.raw.replace('</script>', '<\\/script>'));
} else {
GENERATOR.Literal(node, state);
}
},
};
const serialize = (value: Value) =>
generate(valueToEstree(value), {
generator: customGenerator,
});
declare let rendererInstances: Renderer[];
function isCustomElementTag(name: unknown) {
return typeof name === 'string' && /-/.test(name);
}
const rendererCache = new Map<any, Renderer>();
/** For client:only components, attempt to infer the required renderer. */
function inferClientRenderer(metadata: Partial<AstroComponentMetadata>) {
// If there's only one renderer, assume it's the required renderer
if (rendererInstances.length === 1) {
return rendererInstances[0];
} else if (metadata.value) {
// Attempt to find the renderer by matching the hydration value
const hint = metadata.value;
let match = rendererInstances.find((instance) => instance.name === hint);
if (!match) {
// Didn't find an exact match, try shorthand hints for the internal renderers
const fullHintName = `@astrojs/renderer-${hint}`;
match = rendererInstances.find((instance) => instance.name === fullHintName);
}
if (!match) {
throw new Error(
`Couldn't find a renderer for <${metadata.displayName} client:only="${metadata.value}" />. Is there a renderer that matches the "${metadata.value}" hint in your Astro config?`
);
}
return match;
} else {
// Multiple renderers included but no hint was provided
throw new Error(
`Can't determine the renderer for ${metadata.displayName}. Include a hint similar to <${metadata.displayName} client:only="react" /> when multiple renderers are included in your Astro config.`
);
}
}
/** For a given component, resolve the renderer. Results are cached if this instance is encountered again */
async function resolveRenderer(Component: any, props: any = {}, children?: string, metadata: Partial<AstroComponentMetadata> = {}): Promise<Renderer | undefined> {
// For client:only components, the component can't be imported
// during SSR. We need to infer the required renderer.
if (metadata.hydrate === 'only') {
return inferClientRenderer(metadata);
}
if (rendererCache.has(Component)) {
return rendererCache.get(Component);
}
const errors: Error[] = [];
for (const renderer of rendererInstances) {
// Yes, we do want to `await` inside of this loop!
// __renderer.check can't be run in parallel, it
// returns the first match and skips any subsequent checks
try {
const shouldUse: boolean = await renderer.ssr.check(Component, props, children);
if (shouldUse) {
rendererCache.set(Component, renderer);
return renderer;
}
} catch (err) {
errors.push(err as any);
}
}
if (errors.length) {
// For now just throw the first error we encounter.
throw errors[0];
}
}
interface HydrateScriptOptions {
renderer: Renderer;
astroId: string;
props: any;
}
const astroHtmlRendererInstance: Renderer = {
name: '@astrojs/renderer-html',
source: '',
ssr: astroHtml as any,
polyfills: [],
hydrationPolyfills: [],
};
/** For hydrated components, generate a <script type="module"> to load the component */
async function generateHydrateScript(scriptOptions: HydrateScriptOptions, metadata: Required<AstroComponentMetadata>) {
const { renderer, astroId, props } = scriptOptions;
const { hydrate, componentUrl, componentExport } = metadata;
let hydrationSource = '';
if (renderer.hydrationPolyfills) {
hydrationSource += `await Promise.all([${renderer.hydrationPolyfills.map((src) => `\n import("${src}")`).join(', ')}]);\n`;
}
hydrationSource += renderer.source
? `const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${componentUrl}"), import("${renderer.source}")]);
return (el, children) => hydrate(el)(Component, ${serialize(props)}, children);
`
: `await import("${componentUrl}");
return () => {};
`;
const hydrationScript = `<script type="module">
import setup from 'astro/client/${hydrate}.js';
setup("${astroId}", {${metadata.value ? `value: "${metadata.value}"` : ''}}, async () => {
${hydrationSource}
});
</script>
`;
return hydrationScript;
}
const getComponentName = (Component: any, componentProps: any) => {
if (componentProps.displayName) return componentProps.displayName;
switch (typeof Component) {
case 'function':
return Component.displayName ?? Component.name;
case 'string':
return Component;
default: {
return Component;
}
}
};
const prepareSlottedChildren = (children: string | Record<any, any>[]) => {
const $slots: Record<string, string> = {
default: '',
};
for (const child of children) {
if (typeof child === 'string') {
$slots.default += child;
} else if (typeof child === 'object' && child['$slot']) {
if (!$slots[child['$slot']]) $slots[child['$slot']] = '';
$slots[child['$slot']] += child.children.join('').replace(new RegExp(`slot="${child['$slot']}"\s*`, ''));
}
}
return { $slots };
};
const removeSlottedChildren = (_children: string | Record<any, any>[]) => {
let children = '';
for (const child of _children) {
if (typeof child === 'string') {
children += child;
} else if (typeof child === 'object' && child['$slot']) {
children += child.children.join('');
}
}
return children;
};
/** The main wrapper for any components in Astro files */
export function __astro_component(Component: ComponentInstance['default'], metadata: AstroComponentMetadata = {} as any) {
if (Component == null) {
throw new Error(`Unable to render ${metadata.displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`);
} else if (typeof Component === 'string' && !isCustomElementTag(Component)) {
throw new Error(`Astro is unable to render ${metadata.displayName}!\nIs there a renderer to handle this type of component defined in your Astro config?`);
}
return async function __astro_component_internal(props: any, ..._children: any[]) {
if (Component.isAstroComponent && Component.__render) {
return Component.__render(props, prepareSlottedChildren(_children));
}
const children = removeSlottedChildren(_children);
let renderer = await resolveRenderer(Component, props, children, metadata);
if (!renderer) {
if (isCustomElementTag(Component as any)) {
renderer = astroHtmlRendererInstance;
} else {
// If the user only specifies a single renderer, but the check failed
// for some reason... just default to their preferred renderer.
renderer = rendererInstances.length === 2 ? rendererInstances[1] : undefined;
}
if (!renderer) {
const name = getComponentName(Component, metadata);
throw new Error(`No renderer found for ${name}! Did you forget to add a renderer to your Astro config?`);
}
}
let html = '';
// Skip SSR for components using client:only hydration
if (metadata.hydrate !== 'only') {
const rendered = await renderer.ssr.renderToStaticMarkup(Component, props, children, metadata);
html = rendered.html;
}
if (renderer.polyfills) {
let polyfillScripts = renderer.polyfills.map((src: string) => `<script type="module" src="${src}"></script>`).join('');
html = html + polyfillScripts;
}
// If we're NOT hydrating this component, just return the HTML
if (!metadata.hydrate) {
// It's safe to remove <astro-fragment>, static content doesn't need the wrapper
return html.replace(/\<\/?astro-fragment\>/g, '');
}
// If we ARE hydrating this component, let's generate the hydration script
const uniqueId = props[Symbol.for('astro.context')].createAstroRootUID(html);
const uniqueIdHashed = hash.unique(uniqueId);
const script = await generateHydrateScript({ renderer, astroId: uniqueIdHashed, props }, metadata as Required<AstroComponentMetadata>);
const astroRoot = `<astro-root uid="${uniqueIdHashed}">${html}</astro-root>`;
return [astroRoot, script].join('\n');
};
}

View file

@ -0,0 +1,33 @@
import 'vite/client';
if (import.meta.hot) {
const parser = new DOMParser();
import.meta.hot.on('astro:reload', async ({ html }: { html: string }) => {
const { default: morphdom } = await import('morphdom');
const doc = parser.parseFromString(html, 'text/html');
morphdom(document.head, doc.head, {
onBeforeElUpdated: function (fromEl, toEl) {
if (fromEl.isEqualNode(toEl)) {
return false;
}
return true;
},
});
morphdom(document.body, doc.body, {
onBeforeElUpdated: function (fromEl, toEl) {
if (fromEl.localName === 'astro-root') {
return fromEl.getAttribute('uid') !== toEl.getAttribute('uid');
}
if (fromEl.isEqualNode(toEl)) {
return false;
}
return true;
},
});
});
}

View file

@ -1,15 +1,19 @@
import cheerio from 'cheerio';
import * as eslexer from 'es-module-lexer';
import type { ViteDevServer } from 'vite';
import type { ComponentInstance, GetStaticPathsResult, Params, Props, RouteCache, RouteData, RuntimeMode } from '../@types/astro';
import type { ComponentInstance, GetStaticPathsResult, Params, Props, RouteCache, RouteData, RuntimeMode, AstroConfig } from '../@types/astro';
import type { LogOptions } from '../logger';
import { fileURLToPath } from 'url';
import path from 'path';
import { generatePaginateFunction } from './paginate.js';
import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
import { canonicalURL, parseNpmName } from './util.js';
import { parseNpmName, canonicalURL as getCanonicalURL } from './util.js';
import type { AstroComponent, AstroComponentFactory } from '../internal';
interface SSROptions {
/** an instance of the AstroConfig */
astroConfig: AstroConfig,
/** location of file on disk */
filePath: URL;
/** logging options */
@ -32,11 +36,129 @@ interface SSROptions {
// this prevents client-side errors such as the "double React bug" (https://reactjs.org/warnings/invalid-hook-call-warning.html#mismatching-versions-of-react-and-react-dom)
let browserHash: string | undefined;
export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) {
let template = '';
for await (const value of component) {
if (value || value === 0) {
template += value;
}
}
return template;
}
export async function renderToString(result: any, componentFactory: AstroComponentFactory, props: any, children: any) {
const Component = componentFactory(result, props, children);
let template = await renderAstroComponent(Component);
return template
}
async function renderPage(result: any, Component: AstroComponentFactory, props: any, children: any) {
const template = await renderToString(result, Component, props, children);
const styles = Array.from(result.styles).map(style => `<style>${style}</style>`);
const scripts = Array.from(result.scripts);
return template.replace("</head>", styles.join('\n') + scripts.join('\n') + "</head>");
}
const cache = new Map();
// TODO: improve validation and error handling here.
async function resolveRenderers(viteServer: ViteDevServer, ids: string[]) {
const resolve = viteServer.config.createResolver();
const renderers = await Promise.all(ids.map(async renderer => {
if (cache.has(renderer)) return cache.get(renderer);
const resolvedRenderer: any = {};
// We can dynamically import the renderer by itself because it shouldn't have
// any non-standard imports, the index is just meta info.
// The other entrypoints need to be loaded through Vite.
const { default: instance } = await import(renderer);
// This resolves the renderer's entrypoints to a final URL through Vite
const getPath = async (src: string) => {
const spec = path.posix.join(instance.name, src);
const resolved = await resolve(spec);
if (!resolved) {
throw new Error(`Unable to resolve "${spec}" to a package!`)
}
return resolved;
}
resolvedRenderer.name = instance.name;
if (instance.client) {
resolvedRenderer.source = await getPath(instance.client);
}
if (Array.isArray(instance.hydrationPolyfills)) {
resolvedRenderer.hydrationPolyfills = await Promise.all(instance.hydrationPolyfills.map((src: string) => getPath(src)));
}
if (Array.isArray(instance.polyfills)) {
resolvedRenderer.polyfills = await Promise.all(instance.polyfills.map((src: string) => getPath(src)));
}
const { url } = await viteServer.moduleGraph.ensureEntryFromUrl(await getPath(instance.server));
const { default: server } = await viteServer.ssrLoadModule(url);
resolvedRenderer.ssr = server;
cache.set(renderer, resolvedRenderer);
return resolvedRenderer
}));
return renderers;
}
async function resolveImportedModules(viteServer: ViteDevServer, file: string) {
const { url } = await viteServer.moduleGraph.ensureEntryFromUrl(file);
const modulesByFile = viteServer.moduleGraph.getModulesByFile(url);
if (!modulesByFile) {
return {};
}
let importedModules: Record<string, any> = {};
const moduleNodes = Array.from(modulesByFile);
// Loop over the importedModules and grab the exports from each one.
// We'll pass these to the shared $$result so renderers can match
// components to their exported identifier and URL
// NOTE: Important that this is parallelized as much as possible!
await Promise.all(moduleNodes.map(moduleNode => {
const entries = Array.from(moduleNode.importedModules);
return Promise.all(entries.map(entry => {
// Skip our internal import that every module will have
if (entry.id?.endsWith('astro/dist/internal/index.js')) {
return;
}
return viteServer.moduleGraph.ensureEntryFromUrl(entry.url).then(mod => {
if (mod.ssrModule) {
importedModules[mod.url] = mod.ssrModule;
return;
} else {
return viteServer.ssrLoadModule(mod.url).then(result => {
importedModules[mod.url] = result.ssrModule;
return;
})
}
})
}))
}))
return importedModules
}
/** use Vite to SSR */
export async function ssr({ filePath, logging, mode, origin, pathname, route, routeCache, viteServer }: SSROptions): Promise<string> {
export async function ssr({ astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer }: SSROptions): Promise<string> {
// 1. load module
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
// 1.5. resolve renderers and imported modules.
// important that this happens _after_ ssrLoadModule, otherwise `importedModules` would be empty
const [renderers, importedModules] = await Promise.all([
resolveRenderers(viteServer, astroConfig.renderers),
resolveImportedModules(viteServer, fileURLToPath(filePath))
]);
// 2. handle dynamic routes
let params: Params = {};
let pageProps: Props = {};
@ -68,24 +190,34 @@ export async function ssr({ filePath, logging, mode, origin, pathname, route, ro
// 3. render page
if (!browserHash && (viteServer as any)._optimizeDepsMetadata?.browserHash) browserHash = (viteServer as any)._optimizeDepsMetadata.browserHash; // note: this is "private" and may change over time
const fullURL = new URL(pathname, origin);
if (!mod.__renderPage) throw new Error(`__renderPage() undefined (${route?.component})`);
let html = await mod.__renderPage({
request: {
params,
url: fullURL,
canonicalURL: canonicalURL(fullURL.pathname, fullURL.origin),
const Component = await mod.default;
if (!Component)
throw new Error(`Expected an exported Astro component but recieved typeof ${typeof Component}`);
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
let html = await renderPage({
styles: new Set(),
scripts: new Set(),
/** This function returns the `Astro` faux-global */
createAstro(props: any) {
const site = new URL(origin);
const url = new URL('.' + pathname, site);
const canonicalURL = getCanonicalURL(pathname, astroConfig.buildOptions.site || origin)
return { isPage: true, site, request: { url, canonicalURL }, props };
},
children: [],
props: pageProps,
css: mod.css || [],
});
_metadata: { importedModules, renderers },
}, Component, { }, null);
// 4. modify response
// inject Vite HMR code (dev only)
if (mode === 'development') html = injectViteClient(html);
if (mode === 'development') {
// inject Astro HMR code
html = injectAstroHMR(html);
// inject Vite HMR code
html = injectViteClient(html);
// replace client hydration scripts
if (mode === 'development') html = resolveNpmImports(html);
html = resolveNpmImports(html);
}
// 5. finish
return html;
@ -96,6 +228,11 @@ function injectViteClient(html: string): string {
return html.replace('<head>', `<head><script type="module" src="/@vite/client"></script>`);
}
/** Injects Astro HMR client code */
function injectAstroHMR(html: string): string {
return html.replace('<head>', `<head><script type="module" src="/@astro/runtime/hmr"></script>`);
}
/** Convert npm specifier into Vite URL */
function resolveViteNpmPackage(spec: string): string {
const pkg = parseNpmName(spec);

View file

@ -10,6 +10,7 @@ import { createRequire } from 'module';
import { getPackageJSON, parseNpmName } from '../util.js';
import astro from './plugin-astro.js';
import jsx from './plugin-jsx.js';
import { AstroDevServer } from '../../dev';
const require = createRequire(import.meta.url);
@ -17,7 +18,7 @@ const require = createRequire(import.meta.url);
type ViteConfigWithSSR = InlineConfig & { ssr?: { external?: string[]; noExternal?: string[] } };
/** Return a common starting point for all Vite actions */
export async function loadViteConfig(viteConfig: ViteConfigWithSSR, { astroConfig, logging }: { astroConfig: AstroConfig; logging: LogOptions }): Promise<ViteConfigWithSSR> {
export async function loadViteConfig(viteConfig: ViteConfigWithSSR, { astroConfig, logging, devServer }: { astroConfig: AstroConfig; logging: LogOptions, devServer?: AstroDevServer }): Promise<ViteConfigWithSSR> {
const optimizedDeps = new Set<string>(); // dependencies that must be bundled for the client (Vite may not detect all of these)
const dedupe = new Set<string>(); // dependencies that cant be duplicated (e.g. React & SolidJS)
const plugins: Plugin[] = []; // Vite plugins
@ -73,7 +74,7 @@ export async function loadViteConfig(viteConfig: ViteConfigWithSSR, { astroConfi
/** Always include these dependencies for optimization */
include: [...optimizedDeps],
},
plugins: [astro(astroConfig), jsx({ config: astroConfig, logging }), ...plugins],
plugins: [astro({ config: astroConfig, devServer }), jsx({ config: astroConfig, logging }), ...plugins],
publicDir: fileURLToPath(astroConfig.public),
resolve: {
dedupe: [...dedupe],
@ -82,6 +83,8 @@ export async function loadViteConfig(viteConfig: ViteConfigWithSSR, { astroConfi
server: {
/** prevent serving outside of project root (will become new default soon) */
fs: { strict: true },
/** disable HMR for test */
hmr: process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'production' ? false : undefined,
/** handle Vite URLs */
proxy: {
// add proxies here

View file

@ -1,58 +1,36 @@
import type { Plugin } from 'vite';
import type { AstroConfig, Renderer } from '../../@types/astro.js';
import type { LogOptions } from '../../logger';
import { camelCase } from 'camel-case';
import esbuild from 'esbuild';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { markdownToH } from '../markdown.js';
import { transform } from '@astrojs/compiler';
import { AstroDevServer } from '../../dev/index.js';
interface AstroPluginOptions {
config: AstroConfig;
devServer?: AstroDevServer;
}
/** Transform .astro files for Vite */
export default function astro(config: AstroConfig): Plugin {
export default function astro({ devServer }: AstroPluginOptions): Plugin {
return {
name: '@astrojs/vite-plugin-astro',
enforce: 'pre', // run transforms before other plugins can
// note: dont claim .astro files with resolveId() — it prevents Vite from transpiling the final JS (import.meta.globEager, etc.)
async load(id) {
if (id.endsWith('.astro') || id.endsWith('.md')) {
// TODO: replace with compiler
let code = await fs.promises.readFile(id, 'utf8');
return {
code: code,
map: null,
};
}
// inject renderers (TODO: improve this?)
if (id.endsWith('runtime/__astro_component.js')) {
let code = await fs.promises.readFile(id, 'utf8');
let rendererCode = '';
let source = await fs.promises.readFile(id, 'utf8');
// add imports
config.renderers.forEach((name) => {
rendererCode += `import ${jsRef(name)} from '${name}';
import ${jsRef(name, '_ssr')} from '${name}/server';
`;
});
// 1. Transform from `.astro` to valid `.ts`
const tsResult = await transform(source, { sourcefile: id });
// initialize renderers
rendererCode += `
function initRenderer(name, entry, ssr) {
const join = (...parts) => parts.map((part) => part.replace(/^\\./, '')).join('');
const renderer = {};
renderer.name = name;
renderer.ssr = ssr;
if (entry.client) renderer.source = join(name, entry.client);
if (Array.isArray(entry.hydrationPolyfills)) renderer.hydrationPolyfills = entry.hydrationPolyfills.map((src) => join(name, src));
if (Array.isArray(entry.polyfills)) renderer.polyfills = entry.polyfills.map((src) => join(name, src));
return renderer;
}
let rendererInstances = [
${config.renderers.map((name) => `initRenderer('${name}', ${jsRef(name)}, ${jsRef(name, '_ssr')})`).join(',\n')}
];
`;
// 2. Compile `.ts` to `.js`
const { code, map } = await esbuild.transform(tsResult, { loader: 'ts', sourcemap: 'inline', sourcefile: id });
return {
code: rendererCode + code,
map: null,
code,
map,
};
}
@ -64,34 +42,10 @@ let rendererInstances = [
// }
return null;
},
async handleHotUpdate({ file, modules, timestamp, server, read }) {
// invalidate module
const module = server.moduleGraph.getModuleById(file);
if (module) server.moduleGraph.invalidateModule(module);
try {
const {
default: { __render: render },
} = await server.ssrLoadModule(file);
const html = await render();
server.ws.send({
type: 'custom',
event: 'astro:reload',
data: {
html,
},
});
} catch (e) {
server.ws.send({
type: 'full-reload',
});
async handleHotUpdate(context) {
if (devServer) {
return devServer.handleHotUpdate(context);
}
return [];
},
};
}
/** Given any string (e.g. npm package name), generate a JS-friendly ref */
function jsRef(name: string, suffix = ''): string {
return `__${camelCase(name)}${suffix}`;
}

View file

@ -1,20 +1,22 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import build from '../dist/build/index.js';
import { loadFixture } from './test-utils';
const Assets = suite('Assets');
let fixture;
await build({ projectRoot: 'test/fixtures/astro-assets/' });
describe('Assets', () => {
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-assets/' });
await fixture.build();
});
Assets('build the base image', () => {});
test('built the base image', async () => {
await fixture.readFile('/images/twitter.png');
});
// let oneX = await readFile('/_astro/src/images/twitter.png');
// assert.ok(oneX, 'built the base image');
test('built the 2x image', async () => {
await fixture.readFile('/images/twitter@2x.png');
});
// let twoX = await readFile('/_astro/src/images/twitter@2x.png');
// assert.ok(twoX, 'built the 2x image');
// let threeX = await readFile('/_astro/src/images/twitter@3x.png');
// assert.ok(threeX, 'build the 3x image');
Assets.run();
test('built the 3x image', async () => {
await fixture.readFile('/images/twitter@3x.png');
});
});

View file

@ -1,59 +1,62 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const Attributes = suite('Attributes test');
let fixture;
let devServer;
setup(Attributes, './fixtures/astro-attrs');
describe('Attributes', () => {
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-attrs/' });
devServer = await fixture.dev();
});
Attributes('Passes attributes to elements as expected', async ({ runtime }) => {
const result = await runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
test('Passes attributes to elements as expected', async () => {
const html = await fixture.fetch('/').then((res) => res.html());
const $ = cheerio.load(html);
const $ = doc(result.contents);
const attrs = {
'false-str': 'false',
'true-str': 'true',
false: undefined,
true: '',
empty: '',
null: undefined,
undefined: undefined,
};
const ids = ['false-str', 'true-str', 'false', 'true', 'empty', 'null', 'undefined'];
const specs = ['false', 'true', undefined, '', '', undefined, undefined];
let i = 0;
for (const id of ids) {
const spec = specs[i];
const attr = $(`#${id}`).attr('attr');
assert.equal(attr, spec, `Passes ${id} as "${spec}"`);
i++;
for (const [k, v] of Object.entries(attrs)) {
const attr = $(`#${k}`).attr('attr');
expect(attr).toBe(v);
}
});
test('Passes boolean attributes to components as expected', async () => {
const html = await fixture.fetch('/component').then((res) => res.text());
const $ = cheerio.load(html);
expect($('#true').attr('attr')).toBe('attr-true');
expect($('#true').attr('type')).toBe('boolean');
expect($('#false').attr('attr')).toBe('attr-false');
expect($('#false').attr('type')).toBe('boolean');
});
test('Passes namespaced attributes as expected', async () => {
const html = await fixture.fetch('/namespaced').then((res) => res.text());
const $ = cheerio.load(result.contents);
expect($('div').attr('xmlns:happy')).toBe('https://example.com/schemas/happy');
expect($('img').attr('happy:smile')).toBe('sweet');
});
test('Passes namespaced attributes to components as expected', async () => {
const html = await fixture.fetch('/namespaced-component');
const $ = cheerio.load(html);
expect($('span').attr('on:click')).toEqual(Function.prototype.toString.call((event) => console.log(event)));
});
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.close();
});
});
Attributes('Passes boolean attributes to components as expected', async ({ runtime }) => {
const result = await runtime.load('/component');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal($('#true').attr('attr'), 'attr-true');
assert.equal($('#true').attr('type'), 'boolean');
assert.equal($('#false').attr('attr'), 'attr-false');
assert.equal($('#false').attr('type'), 'boolean');
});
Attributes('Passes namespaced attributes as expected', async ({ runtime }) => {
const result = await runtime.load('/namespaced');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal($('div').attr('xmlns:happy'), 'https://example.com/schemas/happy');
assert.equal($('img').attr('happy:smile'), 'sweet');
});
Attributes('Passes namespaced attributes to components as expected', async ({ runtime }) => {
const result = await runtime.load('/namespaced-component');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal(
$('span').attr('on:click'),
Function.prototype.toString.call((event) => console.log(event))
);
});
Attributes.run();

View file

@ -1,123 +1,109 @@
import { suite } from 'uvu';
import http from 'http';
import { promisify } from 'util';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup, setupBuild, setupPreview } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const Basics = suite('Basic test');
describe('Astro basics', () => {
describe('dev', () => {
let fixture; // fixture #1. Note that .dev() and .preview() share a port, so these fixtures must be kept separate.
let devServer;
setup(Basics, './fixtures/astro-basic', {
runtimeOptions: {
mode: 'development',
},
});
setupBuild(Basics, './fixtures/astro-basic');
setupPreview(Basics, './fixtures/astro-basic');
Basics('Can load page', async ({ runtime }) => {
const result = await runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal($('h1').text(), 'Hello world!');
});
Basics('Sets the HMR port when dynamic components used', async ({ runtime }) => {
const result = await runtime.load('/client');
const html = result.contents;
assert.ok(/HMR_WEBSOCKET_PORT/.test(html), 'Sets the websocket port');
});
Basics('Correctly serializes boolean attributes', async ({ runtime }) => {
const result = await runtime.load('/');
const html = result.contents;
const $ = doc(html);
assert.equal($('h1').attr('data-something'), '');
assert.equal($('h2').attr('not-data-ok'), '');
});
Basics('Selector with an empty body', async ({ runtime }) => {
const result = await runtime.load('/empty-class');
const html = result.contents;
const $ = doc(html);
assert.equal($('.author').length, 1, 'author class added');
});
Basics('Build does not include HMR client', async ({ build, readFile }) => {
await build().catch((err) => {
assert.ok(!err, 'Error during the build');
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-basic/' });
devServer = await fixture.dev();
});
const clientHTML = await readFile('/client/index.html');
const $ = doc(clientHTML);
assert.equal($('script[src="/_snowpack/hmr-client.js"]').length, 0, 'No HMR client script');
const hmrPortScript = $('script').filter((i, el) => {
return $(el)
.text()
.match(/window\.HMR_WEBSOCKET_PORT/);
test('Can load page', async () => {
const html = await fixture.fetch(`/`).then((res) => res.text());
const $ = cheerio.load(html);
expect($('h1').text()).toBe('Hello world!');
});
assert.equal(hmrPortScript.length, 0, 'No script setting the websocket port');
});
Basics('Preview server works as expected', async ({ build, previewServer }) => {
await build().catch((err) => {
assert.ok(!err, 'Error during the build');
test('Correctly serializes boolean attributes', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
expect($('h1').attr('data-something')).toBe('');
expect($('h2').attr('not-data-ok')).toBe('');
});
test('Selector with an empty body', async () => {
const html = await fixture.fetch('/empty-class').then((res) => res.text());
const $ = cheerio.load(html);
expect($('.author')).toHaveLength(1);
});
test('Allows forward-slashes in mustache tags (#407)', async () => {
const html = await fixture.fetch('/forward-slash').then((res) => res.text());
const $ = cheerio.load(html);
expect($('a[href="/post/one"]')).toHaveLength(1);
expect($('a[href="/post/two"]')).toHaveLength(1);
expect($('a[href="/post/three"]')).toHaveLength(1);
});
test('Allows spread attributes (#521)', async () => {
const html = await fixture.fetch('/spread').then((res) => res.text());
const $ = cheerio.load(html);
expect($('#spread-leading')).toHaveLength(1);
expect($('#spread-leading').attr('a')).toBe('0');
expect($('#spread-leading').attr('b')).toBe('1');
expect($('#spread-leading').attr('c')).toBe('2');
expect($('#spread-trailing')).toHaveLength(1);
expect($('#spread-trailing').attr('a')).toBe('0');
expect($('#spread-trailing').attr('b')).toBe('1');
expect($('#spread-trailing').attr('c')).toBe('2');
});
test('Allows spread attributes with TypeScript (#521)', async () => {
const html = await fixture.fetch('/spread').then((res) => res.text());
const $ = cheerio.load(html);
expect($('#spread-ts')).toHaveLength(1);
expect($('#spread-ts').attr('a')).toBe('0');
expect($('#spread-ts').attr('b')).toBe('1');
expect($('#spread-ts').attr('c')).toBe('2');
});
test('Allows using the Fragment element to be used', async () => {
const html = await fixture.fetch('/fragment').then((res) => res.text());
const $ = cheerio.load(html);
// will be 1 if element rendered correctly
expect($('#one')).toHaveLength(1);
});
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.stop();
});
});
describe('preview', () => {
let fixture; // fixture #2. Note that .dev() and .preview() share a port, so these fixtures must be kept separate.
let previewServer;
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-basic' });
await fixture.build();
previewServer = await fixture.preview();
});
test('returns 200 for valid URLs', async () => {
const result = fixture.fetch('/');
expect(result.statusCode).toBe(200);
});
test('returns 404 for invalid URLs', async () => {
const result = fixture.fetch('/bad-url');
expect(result.statusCode).toBe(404);
});
// important: close preview server (free up port and connection)
afterAll(() => {
previewServer.close();
});
});
{
const resultOrError = await promisify(http.get)(`http://localhost:${previewServer.address().port}/`).catch((err) => err);
assert.equal(resultOrError.statusCode, 200);
}
{
const resultOrError = await promisify(http.get)(`http://localhost:${previewServer.address().port}/bad-url`).catch((err) => err);
assert.equal(resultOrError.statusCode, 404);
}
});
Basics('Allows forward-slashes in mustache tags (#407)', async ({ runtime }) => {
const result = await runtime.load('/forward-slash');
const html = result.contents;
const $ = doc(html);
assert.equal($('a[href="/post/one"]').length, 1);
assert.equal($('a[href="/post/two"]').length, 1);
assert.equal($('a[href="/post/three"]').length, 1);
});
Basics('Allows spread attributes (#521)', async ({ runtime }) => {
const result = await runtime.load('/spread');
const html = result.contents;
const $ = doc(html);
assert.equal($('#spread-leading').length, 1);
assert.equal($('#spread-leading').attr('a'), '0');
assert.equal($('#spread-leading').attr('b'), '1');
assert.equal($('#spread-leading').attr('c'), '2');
assert.equal($('#spread-trailing').length, 1);
assert.equal($('#spread-trailing').attr('a'), '0');
assert.equal($('#spread-trailing').attr('b'), '1');
assert.equal($('#spread-trailing').attr('c'), '2');
});
Basics('Allows spread attributes with TypeScript (#521)', async ({ runtime }) => {
const result = await runtime.load('/spread');
const html = result.contents;
const $ = doc(html);
assert.equal($('#spread-ts').length, 1);
assert.equal($('#spread-ts').attr('a'), '0');
assert.equal($('#spread-ts').attr('b'), '1');
assert.equal($('#spread-ts').attr('c'), '2');
});
Basics('Allows using the Fragment element to be used', async ({ runtime }) => {
const result = await runtime.load('/fragment');
assert.ok(!result.error, 'No errors thrown');
const html = result.contents;
const $ = doc(html);
assert.equal($('#one').length, 1, 'Element in a fragment rendered');
});
Basics.run();

View file

@ -1,75 +1,81 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup, setupBuild } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const ComponentChildren = suite('Component children tests');
describe('Component children', () => {
let fixture;
let devServer;
setup(ComponentChildren, './fixtures/astro-children');
setupBuild(ComponentChildren, './fixtures/astro-children');
beforeAll(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-children/',
renderers: ['@astrojs/renderer-preact', '@astrojs/renderer-vue', '@astrojs/renderer-svelte'],
});
devServer = await fixture.dev();
});
ComponentChildren('Passes string children to framework components', async ({ runtime }) => {
let result = await runtime.load('/strings');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
test('Passes string children to framework components', async () => {
const html = await fixture.fetch('/strings').then((res) => res.text());
const $ = cheerio.load(html);
// test 1: Can pass text to Preact components
const $preact = $('#preact');
assert.equal($preact.text().trim(), 'Hello world', 'Can pass text to Preact components');
expect($preact.text().trim()).toBe('Hello world');
// test 2: Can pass text to Vue components
const $vue = $('#vue');
assert.equal($vue.text().trim(), 'Hello world', 'Can pass text to Vue components');
expect($vue.text().trim()).toBe('Hello world');
// test 3: Can pass text to Svelte components
const $svelte = $('#svelte');
assert.equal($svelte.text().trim(), 'Hello world', 'Can pass text to Svelte components');
});
expect($svelte.text().trim()).toBe('Hello world');
});
ComponentChildren('Passes markup children to framework components', async ({ runtime }) => {
let result = await runtime.load('/markup');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
test('Passes markup children to framework components', async () => {
const html = await fixture.fetch('/markup').then((res) => res.text());
const $ = cheerio.load(html);
// test 1: Can pass markup to Preact components
const $preact = $('#preact h1');
assert.equal($preact.text().trim(), 'Hello world', 'Can pass markup to Preact components');
expect($preact.text().trim()).toBe('Hello world');
// test 2: Can pass markup to Vue components
const $vue = $('#vue h1');
assert.equal($vue.text().trim(), 'Hello world', 'Can pass markup to Vue components');
expect($vue.text().trim()).toBe('Hello world');
// test 3: Can pass markup to Svelte components
const $svelte = $('#svelte h1');
assert.equal($svelte.text().trim(), 'Hello world', 'Can pass markup to Svelte components');
});
expect($svelte.text().trim()).toBe('Hello world');
});
ComponentChildren('Passes multiple children to framework components', async ({ runtime }) => {
let result = await runtime.load('/multiple');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
test('Passes multiple children to framework components', async () => {
const html = await fixture.fetch('/multiple').then((res) => res.text());
const $ = cheerio.load(html);
// test 1: Can pass multiple children to Preact components
const $preact = $('#preact');
assert.equal($preact.children().length, 2, 'Can pass multiple children to Preact components');
assert.equal($preact.children(':first-child').text().trim(), 'Hello world');
assert.equal($preact.children(':last-child').text().trim(), 'Goodbye world');
expect($preact.children()).toHaveLength(2);
expect($preact.children(':first-child').text().trim()).toBe('Hello world');
expect($preact.children(':last-child').text().trim()).toBe('Goodbye world');
// test 2: Can pass multiple children to Vue components
const $vue = $('#vue');
assert.equal($vue.children().length, 2, 'Can pass multiple children to Vue components');
assert.equal($vue.children(':first-child').text().trim(), 'Hello world');
assert.equal($vue.children(':last-child').text().trim(), 'Goodbye world');
expect($vue.children()).toHaveLength(2);
expect($vue.children(':first-child').text().trim()).toBe('Hello world');
expect($vue.children(':last-child').text().trim()).toBe('Goodbye world');
// test 3: Can pass multiple children to Svelte components
const $svelte = $('#svelte');
assert.equal($svelte.children().length, 2, 'Can pass multiple children to Svelte components');
assert.equal($svelte.children(':first-child').text().trim(), 'Hello world');
assert.equal($svelte.children(':last-child').text().trim(), 'Goodbye world');
});
expect($svelte.children()).toHaveLength(2);
expect($svelte.children(':first-child').text().trim()).toBe('Hello world');
expect($svelte.children(':last-child').text().trim()).toBe('Goodbye world');
});
ComponentChildren('Can be built', async ({ build }) => {
try {
await build();
assert.ok(true, 'Can build a project with component children');
} catch (err) {
console.log(err);
assert.ok(false, 'build threw');
}
});
test('Can build a project with component children', async () => {
expect(() => fixture.build()).not.toThrow();
});
ComponentChildren.run();
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.close();
});
});

View file

@ -1,22 +1,23 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { setup, setupBuild } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const ClientOnlyComponents = suite('Client only components tests');
describe('Client only components', () => {
let fixture;
let devServer;
setup(ClientOnlyComponents, './fixtures/astro-client-only');
setupBuild(ClientOnlyComponents, './fixtures/astro-client-only');
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-client-only/' });
devServer = await fixture.dev();
});
ClientOnlyComponents('Loads pages using client:only hydrator', async ({ runtime }) => {
let result = await runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
test('Loads pages using client:only hydrator', async () => {
const html = await fixture.fetch('/').then((res) => res.html());
const $ = cheerio.load(html);
let html = result.contents;
// test 1: <astro-root> is empty
expect($('astro-root').html()).toBe('');
const rootExp = /<astro-root\s[^>]*><\/astro-root>/;
assert.ok(rootExp.exec(html), 'astro-root is empty');
// Grab the svelte import
// test 2: svelte renderer is on the page
const exp = /import\("(.+?)"\)/g;
let match, svelteRenderer;
while ((match = exp.exec(result.contents))) {
@ -24,21 +25,19 @@ ClientOnlyComponents('Loads pages using client:only hydrator', async ({ runtime
svelteRenderer = match[1];
}
}
expect(svelteRenderer).toBeTruthy();
assert.ok(svelteRenderer, 'Svelte renderer is on the page');
// test 3: can load svelte renderer
result = await fixture.fetch(svelteRenderer);
expect(result.statusCode).toBe(200);
});
result = await runtime.load(svelteRenderer);
assert.equal(result.statusCode, 200, 'Can load svelte renderer');
test('Can build a project with svelte dynamic components', async () => {
expect(() => fixture.build()).not.toThrow();
});
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.close();
});
});
ClientOnlyComponents('Can be built', async ({ build }) => {
try {
await build();
assert.ok(true, 'Can build a project with svelte dynamic components');
} catch (err) {
console.log(err);
assert.ok(false, 'build threw');
}
});
ClientOnlyComponents.run();

View file

@ -1,53 +1,55 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup, setupBuild } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const Components = suite('Components tests');
describe('Components tests', () => {
let fixture;
let devServer;
setup(Components, './fixtures/astro-components');
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-components/' });
devServer = await fixture.dev();
});
Components('Astro components are able to render framework components', async ({ runtime }) => {
let result = await runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
test('Astro components are able to render framework components', async () => {
const html = await fixture.fetch('/').then((res) => res.html());
const $ = cheerio.load(html);
// test 1: Renders Astro component
const $astro = $('#astro');
assert.equal($astro.children().length, 3, 'Renders astro component');
expect($astro.children()).toHaveLength(3);
// test 2: Renders React component
const $react = $('#react');
assert.not.type($react, 'undefined', 'Renders React component');
expect($react).not.toHaveLength(0);
// test 3: Renders Vue component
const $vue = $('#vue');
assert.not.type($vue, 'undefined', 'Renders Vue component');
expect($vue).not.toHaveLength(0);
// test 4: Renders Svelte component
const $svelte = $('#svelte');
assert.not.type($svelte, 'undefined', 'Renders Svelte component');
expect($svelte).not.toHaveLength(0);
});
test('Allows Components defined in frontmatter', async () => {
const html = await fixture.fetch('/frontmatter-component').then((res) => res.text());
const $ = cheerio.load(html);
expect($('h1')).toHaveLength(1);
});
test('Still throws an error for undefined components', async () => {
const result = await fixture.fetch('/undefined-component');
expect(result.statusCode).toBe(500);
});
test('Client attrs not added', async () => {
const html = await fixture.fetch('/client').then((res) => res.text());
expect(html).not.toEqual(expect.stringMatching(/"client:load": true/));
});
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.close();
});
});
Components('Allows Components defined in frontmatter', async ({ runtime }) => {
const result = await runtime.load('/frontmatter-component');
const html = result.contents;
const $ = doc(html);
assert.equal($('h1').length, 1);
});
Components('Allows variables named props', async ({ runtime }) => {
const result = await runtime.load('/props-shadowing');
assert.equal(result.statusCode, 500);
});
Components('Still throws an error for undefined components', async ({ runtime }) => {
const result = await runtime.load('/undefined-component');
assert.equal(result.statusCode, 500);
});
Components('Svelte component', async ({ runtime }) => {
const result = await runtime.load('/client');
const html = result.contents;
assert.ok(!/"client:load": true/.test(html), 'Client attrs not added');
});
Components.run();

View file

@ -1,11 +1,5 @@
import cheerio from 'cheerio';
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { setupBuild } from './helpers.js';
const CSSBundling = suite('CSS Bundling');
setupBuild(CSSBundling, './fixtures/astro-css-bundling');
import { loadFixture } from './test-utils';
// note: the hashes should be deterministic, but updating the file contents will change hashes
// be careful not to test that the HTML simply contains CSS, because it always will! filename and quanity matter here (bundling).
@ -18,48 +12,53 @@ const EXPECTED_CSS = {
};
const UNEXPECTED_CSS = ['/_astro/components/nav.css', '../css/typography.css', '../css/colors.css', '../css/page-index.css', '../css/page-one.css', '../css/page-two.css'];
CSSBundling('Bundles CSS', async (context) => {
await context.build();
describe('CSS Bundling', () => {
let fixture;
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-css-bundling/' });
await fixture.build();
});
test('Bundles CSS', async () => {
const builtCSS = new Set();
// for all HTML files…
for (const [filepath, css] of Object.entries(EXPECTED_CSS)) {
const html = await context.readFile(filepath);
const html = await fixture.readFile(filepath);
const $ = cheerio.load(html);
// test 1: assert new bundled CSS is present
for (const href of css) {
const link = $(`link[rel="stylesheet"][href^="${href}"]`);
assert.equal(link.length, 1, 'New bundled CSS is not present');
expect(link).toHaveLength(1);
builtCSS.add(link.attr('href'));
}
// test 2: assert old CSS was removed
for (const href of UNEXPECTED_CSS) {
const link = $(`link[rel="stylesheet"][href="${href}"]`);
assert.equal(link.length, 0, 'Old CSS was not removed');
expect(link).toHaveLength(0);
}
// test 3: preload tags was not removed and attributes was preserved
if (filepath === '/preload/index.html') {
const stylesheet = $('link[rel="stylesheet"][href^="/_astro/preload/index-"]');
const preload = $('link[rel="preload"][href^="/_astro/preload/index-"]');
assert.equal(stylesheet[0].attribs.media, 'print', 'Attribute was not preserved');
assert.equal(preload.length, 1, 'Preload tag was removed');
expect(stylesheet[0].attribs.media).toBe('print');
expect(preload).toHaveLength(1); // Preload tag was removed
}
// test 4: preload tags was not removed and attributes was preserved
if (filepath === '/preload-merge/index.html') {
const preload = $('link[rel="preload"]');
assert.equal(preload.length, 1, 'Preload tag was not merged or was removed completly');
}
expect(preload).toHaveLength(1);
}
// test 5: assert all bundled CSS was built and contains CSS
for (const url of builtCSS.keys()) {
const css = await context.readFile(url);
assert.ok(css, true);
expect(css).toBeTruthy();
}
// test 6: assert ordering is preserved (typography.css before colors.css)
@ -67,21 +66,21 @@ CSSBundling('Bundles CSS', async (context) => {
const bundledContents = await context.readFile(bundledLoc);
const typographyIndex = bundledContents.indexOf('body{');
const colorsIndex = bundledContents.indexOf(':root{');
assert.ok(typographyIndex < colorsIndex);
expect(typographyIndex).toBeLessThan(colorsIndex);
// test 7: assert multiple style blocks were bundled (Nav.astro includes 2 scoped style blocks)
const scopedNavStyles = [...bundledContents.matchAll('.nav.astro-')];
assert.is(scopedNavStyles.length, 2);
expect(scopedNavStyles).toHaveLength(2);
// test 8: assert <style global> was not scoped (in Nav.astro)
const globalStyles = [...bundledContents.matchAll('html{')];
assert.is(globalStyles.length, 1);
expect(globalStyles).toHaveLength(1);
// test 9: assert keyframes are only scoped for non-global styles (from Nav.astro)
const scopedKeyframes = [...bundledContents.matchAll('nav-scoped-fade-astro')];
const globalKeyframes = [...bundledContents.matchAll('nav-global-fade{')];
assert.ok(scopedKeyframes.length > 0);
assert.ok(globalKeyframes.length > 0);
expect(scopedKeyframes.length).toBeGreaterThan(0);
expect(globalKeyframes.length).toBeGreaterThan(0);
}
});
});
CSSBundling.run();

View file

@ -1,73 +1,77 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const DType = suite('doctype');
describe('Doctype', () => {
let fixture;
let devServer;
setup(DType, './fixtures/astro-doctype');
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-doctype/' });
devServer = await fixture.dev();
});
DType('Automatically prepends the standards mode doctype', async ({ runtime }) => {
const result = await runtime.load('/prepend');
assert.ok(!result.error, `build error: ${result.error}`);
test('Automatically prepends the standards mode doctype', async () => {
const html = await fixture.fetch('/prepend').then((res) => res.text());
const html = result.contents.toString('utf-8');
assert.ok(html.startsWith('<!doctype html>'), 'Doctype always included');
});
// test that Doctype always included
expect(html).toEqual(expect.stringMatching(/^<!doctype html>/));
});
DType('No attributes added when doctype is provided by user', async ({ runtime }) => {
const result = await runtime.load('/provided');
assert.ok(!result.error, `build error: ${result.error}`);
test('No attributes added when doctype is provided by user', async () => {
const html = await fixture.fetch('/provided').then((res) => res.text());
const html = result.contents.toString('utf-8');
assert.ok(html.startsWith('<!doctype html>'), 'Doctype always included');
});
// test that Doctype always included
expect(html).toEqual(expect.stringMatching(/^<!doctype html>/));
});
DType.skip('Preserves user provided doctype', async ({ runtime }) => {
const result = await runtime.load('/preserve');
assert.ok(!result.error, `build error: ${result.error}`);
test.skip('Preserves user provided doctype', async () => {
const html = await fixture.fetch('/preserve').then((res) => res.text());
const html = result.contents.toString('utf-8');
assert.ok(html.startsWith('<!doctype HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'), 'Doctype included was preserved');
});
// test that Doctype included was preserved
expect(html).toEqual(expect.stringMatching(new RegExp('^<!doctype HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">')));
});
DType('User provided doctype is case insensitive', async ({ runtime }) => {
const result = await runtime.load('/capital');
assert.ok(!result.error, `build error: ${result.error}`);
test('User provided doctype is case insensitive', async () => {
const html = await fixture.fetch('/capital').then((res) => res.text());
const html = result.contents.toString('utf-8');
assert.ok(html.startsWith('<!DOCTYPE html>'), 'Doctype left alone');
assert.not.ok(html.includes('</!DOCTYPE>'), 'There should not be a closing tag');
});
// test 1: Doctype left alone
expect(html).toEqual(expect.stringMatching(/^<!DOCTYPE html>/));
DType('Doctype can be provided in a layout', async ({ runtime }) => {
const result = await runtime.load('/in-layout');
assert.ok(!result.error, `build error: ${result.error}`);
// test 2: no closing tag
expect(html).not.toEqual(expect.stringContaining('</!DOCTYPE>'));
});
const html = result.contents.toString('utf-8');
assert.ok(html.startsWith('<!doctype html>'), 'doctype is at the front');
test('Doctype can be provided in a layout', async () => {
const html = await fixture.fetch('/in-layout').then((res) => res.text());
// test 1: doctype is at the front
expect(html).toEqual(expect.stringMatching(/^<!doctype html>/));
// test 2: A link inside of the head
const $ = cheerio.load(html);
expect($('head link')).toHaveLength(1);
});
test('Doctype is added in a layout without one', async () => {
const html = await fixture.fetch('/in-layout-no-doctype').then((res) => res.text());
// test that doctype is at the front
expect(html).toEqual(expect.stringMatching(/^<!doctype html>/));
});
test('Doctype is added in a layout used with markdown pages', async () => {
const html = await fixture.fetch('/in-layout-article').then((res) => res.text());
// test 1: doctype is at the front
expect(html).toEqual(expect.stringMatching(/^<!doctype html>/));
// test 2: A link inside of the head
const $ = doc(html);
assert.equal($('head link').length, 1, 'A link inside of the head');
expect($('head link')).toHaveLength(1);
});
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.close();
});
});
DType('Doctype is added in a layout without one', async ({ runtime }) => {
const result = await runtime.load('/in-layout-no-doctype');
assert.ok(!result.error, `build error: ${result.error}`);
const html = result.contents.toString('utf-8');
assert.ok(html.startsWith('<!doctype html>'), 'doctype is at the front');
});
DType('Doctype is added in a layout used with markdown pages', async ({ runtime }) => {
const result = await runtime.load('/in-layout-article');
assert.ok(!result.error, `build error: ${result.error}`);
const html = result.contents.toString('utf-8');
assert.ok(html.startsWith('<!doctype html>'), 'doctype is at the front');
const $ = doc(html);
assert.equal($('head link').length, 1, 'A link inside of the head');
});
DType.run();

View file

@ -1,48 +1,51 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { setup, setupBuild } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const DynamicComponents = suite('Dynamic components tests');
describe('Dynamic components', () => {
let fixture;
let devServer;
setup(DynamicComponents, './fixtures/astro-dynamic');
setupBuild(DynamicComponents, './fixtures/astro-dynamic');
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-dynamic/' });
devServer = await fixture.dev();
});
DynamicComponents('Loads client-only packages', async ({ runtime }) => {
let result = await runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
test('Loads client-only packages', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
// Grab the react-dom import
const exp = /import\("(.+?)"\)/g;
let match, reactRenderer;
while ((match = exp.exec(result.contents))) {
while ((match = exp.exec(html))) {
if (match[1].includes('renderers/renderer-react/client.js')) {
reactRenderer = match[1];
}
}
assert.ok(reactRenderer, 'React renderer is on the page');
// test 1: React renderer is on the page
expect(reactRenderer).toBeTruthy();
result = await runtime.load(reactRenderer);
assert.equal(result.statusCode, 200, 'Can load react renderer');
});
// test 2: Can load React renderer
const result = await fixture.fetch(reactRenderer);
expect(result.statusCode).toBe(200);
});
DynamicComponents('Loads pages using client:media hydrator', async ({ runtime }) => {
let result = await runtime.load('/media');
assert.ok(!result.error, `build error: ${result.error}`);
test('Loads pages using client:media hydrator', async () => {
const html = await fixture.fetch('/media').then((res) => res.text());
let html = result.contents;
assert.ok(html.includes(`value: "(max-width: 700px)"`), 'static value rendered');
assert.ok(html.includes(`value: "(max-width: 600px)"`), 'dynamic value rendered');
});
// test 1: static value rendered
expect(html).toEqual(expect.stringContaining(`value: "(max-width: 700px)"`));
DynamicComponents('Loads pages using client:only hydrator', async ({ runtime }) => {
let result = await runtime.load('/client-only');
assert.ok(!result.error, `build error: ${result.error}`);
// test 2: dynamic value rendered
expect(html).toEqual(expect.stringContaining(`value: "(max-width: 600px)"`));
});
let html = result.contents;
test('Loads pages using client:only hydrator', async () => {
const html = await fixture.fetch('/client-only').then((res) => res.html());
const $ = cheerio.load(html);
const rootExp = /<astro-root\s[^>]*><\/astro-root>/;
assert.ok(rootExp.exec(html), 'astro-root is empty');
// test 1: <astro-root> is empty
expect($('<astro-root>').html()).toBe('');
// Grab the svelte import
const exp = /import\("(.+?)"\)/g;
@ -53,20 +56,20 @@ DynamicComponents('Loads pages using client:only hydrator', async ({ runtime })
}
}
assert.ok(svelteRenderer, 'Svelte renderer is on the page');
// test 2: Svelte renderer is on the page
expect(svelteRenderer).toBeTruthy();
result = await runtime.load(svelteRenderer);
assert.equal(result.statusCode, 200, 'Can load svelte renderer');
// test 3: Can load svelte renderer
const result = await fixture.fetch(svelteRenderer);
expect(result.statusCode).toBe(200);
});
test('Can build a project with svelte dynamic components', async () => {
expect(() => fixture.build()).not.toThrow();
});
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.stop();
});
});
DynamicComponents('Can be built', async ({ build }) => {
try {
await build();
assert.ok(true, 'Can build a project with svelte dynamic components');
} catch (err) {
console.log(err);
assert.ok(false, 'build threw');
}
});
DynamicComponents.run();

View file

@ -1,91 +1,107 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const Expressions = suite('Expressions');
describe('Expressions', () => {
let fixture;
let devServer;
setup(Expressions, './fixtures/astro-expr');
beforeAll(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-expr/',
renderers: ['@astrojs/renderer-preact'],
});
devServer = await fixture.dev();
});
Expressions('Can load page', async ({ runtime }) => {
const result = await runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
test('Can load page', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
for (let col of ['red', 'yellow', 'blue']) {
assert.equal($('#' + col).length, 1);
expect($('#' + col)).toHaveLength(1);
}
});
});
Expressions('Ignores characters inside of strings', async ({ runtime }) => {
const result = await runtime.load('/strings');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
test('Ignores characters inside of strings', async () => {
const html = await fixture.fetch('/strings').then((res) => res.text());
const $ = cheerio.load(html);
for (let col of ['red', 'yellow', 'blue']) {
assert.equal($('#' + col).length, 1);
expect($('#' + col)).toHaveLength(1);
}
});
});
Expressions('Ignores characters inside of line comments', async ({ runtime }) => {
const result = await runtime.load('/line-comments');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
test('Ignores characters inside of line comments', async () => {
const html = await fixture.fetch('/line-comments').then((res) => res.text());
const $ = cheerio.load(html);
for (let col of ['red', 'yellow', 'blue']) {
assert.equal($('#' + col).length, 1);
expect($('#' + col)).toHaveLength(1);
}
});
});
Expressions('Ignores characters inside of multiline comments', async ({ runtime }) => {
const result = await runtime.load('/multiline-comments');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
test('Ignores characters inside of multiline comments', async () => {
const html = await fixture.fetch('/multiline-comments').then((res) => res.text());
const $ = cheerio.load(html);
for (let col of ['red', 'yellow', 'blue']) {
assert.equal($('#' + col).length, 1);
expect($('#' + col)).toHaveLength(1);
}
});
});
Expressions('Allows multiple JSX children in mustache', async ({ runtime }) => {
const result = await runtime.load('/multiple-children');
assert.ok(!result.error, `build error: ${result.error}`);
test('Allows multiple JSX children in mustache', async () => {
const html = await fixture.fetch('/multiple-children').then((res) => res.text());
assert.ok(result.contents.includes('#f') && !result.contents.includes('#t'));
});
expect(html).toEqual(expect.stringContaining('#f'));
expect(html).not.toEqual(expect.stringContaining('#t'));
});
Expressions('Allows <> Fragments in expressions', async ({ runtime }) => {
const result = await runtime.load('/multiple-children');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
test('Allows <> Fragments in expressions', async () => {
const html = await fixture.fetch('/multiple-children').then((res) => res.text());
const $ = cheerio.load(html);
assert.equal($('#fragment').children().length, 3);
assert.equal($('#fragment').children('#a').length, 1);
assert.equal($('#fragment').children('#b').length, 1);
assert.equal($('#fragment').children('#c').length, 1);
});
expect($('#fragment').children()).toHaveLength(3);
expect($('#fragment').children('#a')).toHaveLength(1);
expect($('#fragment').children('#b')).toHaveLength(1);
expect($('#fragment').children('#c')).toHaveLength(1);
});
Expressions('Does not render falsy values using &&', async ({ runtime }) => {
const result = await runtime.load('/falsy');
assert.ok(!result.error, `build error: ${result.error}`);
test('Does not render falsy values using &&', async () => {
const html = await fixture.fetch('/falsy').then((res) => res.text());
const $ = cheerio.load(html);
const $ = doc(result.contents);
// test 1: Expected {true && <span id="true" />} to render
expect($('#true')).toHaveLength(1);
assert.equal($('#true').length, 1, `Expected {true && <span id="true" />} to render`);
assert.equal($('#zero').text(), '0', `Expected {0 && "VALUE"} to render "0"`);
assert.equal($('#false').length, 0, `Expected {false && <span id="false" />} not to render`);
assert.equal($('#null').length, 0, `Expected {null && <span id="null" />} not to render`);
assert.equal($('#undefined').length, 0, `Expected {undefined && <span id="undefined" />} not to render`);
// test 2: Expected {0 && "VALUE"} to render "0"
expect($('#zero').text()).toBe('0');
// test 3: Expected {false && <span id="false" />} not to render
expect($('#false')).toHaveLength(0);
// test 4: Expected {null && <span id="null" />} not to render
expect($('#null')).toHaveLength(0);
// test 5: Expected {undefined && <span id="undefined" />} not to render
expect($('#undefined')).toHaveLength(0);
// Inside of a component
assert.equal($('#frag-true').length, 1, `Expected {true && <span id="true" />} to render`);
assert.equal($('#frag-false').length, 0, `Expected {false && <span id="false" />} not to render`);
assert.equal($('#frag-null').length, 0, `Expected {null && <span id="null" />} not to render`);
assert.equal($('#frag-undefined').length, 0, `Expected {undefined && <span id="undefined" />} not to render`);
});
Expressions.run();
// test 6: Expected {true && <span id="true" />} to render
expect($('#frag-true')).toHaveLength(1);
// test 7: Expected {false && <span id="false" />} not to render
expect($('#frag-false')).toHaveLength(0);
// test 8: Expected {null && <span id="null" />} not to render
expect($('#frag-null')).toHaveLength(0);
// test 9: Expected {undefined && <span id="undefined" />} not to render
expect($('#frag-undefined')).toHaveLength(0);
});
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.close();
});
});

View file

@ -1,18 +1,26 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const Fallback = suite('Dynamic component fallback');
describe('Dynamic component fallback', () => {
let fixture;
let devServer;
setup(Fallback, './fixtures/astro-fallback');
beforeAll(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-fallback',
renderers: ['@astrojs/renderer-preact'],
});
devServer = await fixture.dev();
});
Fallback('Shows static content', async (context) => {
const result = await context.runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
test('Shows static content', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
expect($('#fallback').text()).toBe('static');
});
const $ = doc(result.contents);
assert.equal($('#fallback').text(), 'static');
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.close();
});
});
Fallback.run();

View file

@ -1,13 +1,20 @@
import { suite } from 'uvu';
import { setupBuild } from './helpers.js';
import { loadFixture } from './helpers.js';
const GetStaticPaths = suite('getStaticPaths()');
describe('getStaticPaths()', () => {
let fixture;
setupBuild(GetStaticPaths, './fixtures/astro-get-static-paths');
beforeAll(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-get-static-paths/',
buildOptions: {
site: 'https://mysite.dev/blog/',
sitemap: false,
},
});
});
GetStaticPaths('is only called once during build', async (context) => {
test('is only called once during build', async () => {
// It would throw if this was not true
await context.build();
expect(() => fixture.build()).not.toThrow();
});
});
GetStaticPaths.run();

View file

@ -1,23 +0,0 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
const GlobalBuild = suite('Astro.* built');
setup(GlobalBuild, './fixtures/astro-global', {
runtimeOptions: {
mode: 'production',
},
});
GlobalBuild('Astro.resolve in the build', async (context) => {
const result = await context.runtime.load('/resolve');
assert.ok(!result.error, `build error: ${result.error}`);
const html = result.contents;
const $ = doc(html);
assert.equal($('img').attr('src'), '/blog/_astro/src/images/penguin.png');
});
GlobalBuild.run();

View file

@ -1,23 +1,36 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const Global = suite('Astro.*');
describe('Astro.*', () => {
let fixture;
setup(Global, './fixtures/astro-global');
beforeAll(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-global/',
buildOptions: {
site: 'https://mysite.dev/blog/',
sitemap: false,
},
});
});
Global('Astro.request.url', async (context) => {
const result = await context.runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
describe('dev', () => {
let devServer;
const $ = doc(result.contents);
assert.equal($('#pathname').text(), '/');
assert.equal($('#child-pathname').text(), '/');
assert.equal($('#nested-child-pathname').text(), '/');
});
beforeAll(async () => {
devServer = await fixture.dev();
});
Global('Astro.request.canonicalURL', async (context) => {
test('Astro.request.url', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
expect($('#pathname').text()).toBe('/');
expect($('#child-pathname').text()).toBe('/');
expect($('#nested-child-pathname').text()).toBe('/');
});
test('Astro.request.canonicalURL', async () => {
// given a URL, expect the following canonical URL
const canonicalURLs = {
'/': 'https://mysite.dev/blog/',
@ -27,28 +40,40 @@ Global('Astro.request.canonicalURL', async (context) => {
};
for (const [url, canonicalURL] of Object.entries(canonicalURLs)) {
const result = await context.runtime.load(url);
const $ = doc(result.contents);
assert.equal($('link[rel="canonical"]').attr('href'), canonicalURL);
const result = await fixture.fetch(url).then((res) => res.text());
const $ = cheerio.load(result.contents);
expect($('link[rel="canonical"]').attr('href')).toBe(canonicalURL);
}
});
test('Astro.site', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
expect($('#site').attr('href')).toBe('https://mysite.dev/blog/');
});
test('Astro.resolve in development', async () => {
const html = await fixture.fetch('/resolve').then((res) => res.text());
const $ = cheerio.load(html);
expect($('img').attr('src')).toBe('/_astro/src/images/penguin.png');
expect($('#inner-child img').attr('src')).toBe('/_astro/src/components/nested/images/penguin.png');
});
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.stop();
});
});
describe('build', () => {
beforeAll(async () => {
await fixture.build();
});
test('Astro.resolve in the build', async () => {
const html = await fixture.readFile('/resolve/index.html');
const $ = cheerio.load(html);
expect($('img').attr('src')).toBe('/blog/_astro/src/images/penguin.png');
});
});
});
Global('Astro.site', async (context) => {
const result = await context.runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal($('#site').attr('href'), 'https://mysite.dev/blog/');
});
Global('Astro.resolve in development', async (context) => {
const result = await context.runtime.load('/resolve');
assert.ok(!result.error, `build error: ${result.error}`);
const html = result.contents;
const $ = doc(html);
assert.equal($('img').attr('src'), '/_astro/src/images/penguin.png');
assert.equal($('#inner-child img').attr('src'), '/_astro/src/components/nested/images/penguin.png');
});
Global.run();

View file

@ -1,52 +1,46 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
/**
* note(drew): test was commented-out as this is now handled by Vite. Do we need any tests here?
*/
const HMR = suite('HMR tests');
import { loadFixture } from './test-utils.js';
setup(HMR, './fixtures/astro-hmr', {
runtimeOptions: {
mode: 'development',
},
});
describe.skip('HMR tests', () => {
let fixture;
HMR('Honors the user provided port', async ({ runtime }) => {
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-hmr/' });
});
test('Honors the user provided port', async () => {
const result = await runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
const html = result.contents;
assert.ok(/window\.HMR_WEBSOCKET_PORT = 5555/.test(html), "Uses the user's websocket port");
});
});
HMR('Does not override script added by the user', async ({ runtime }) => {
test('Does not override script added by the user', async () => {
const result = await runtime.load('/manual');
assert.ok(!result.error, `build error: ${result.error}`);
const html = result.contents;
assert.ok(/window\.HMR_WEBSOCKET_URL = 'wss:\/\/example.com:3333'/.test(html), "User's script included");
assert.ok(/window\.HMR_WEBSOCKET_PORT = 5555/.test(html), 'Ignored when window.HMR_WEBSOCKET_URL set');
});
});
HMR('Adds script to static pages too', async ({ runtime }) => {
test('Adds script to static pages too', async () => {
const result = await runtime.load('/static');
assert.ok(!result.error, `build error: ${result.error}`);
const html = result.contents;
const $ = doc(html);
assert.equal($('[src="/_snowpack/hmr-client.js"]').length, 1);
assert.ok(/window\.HMR_WEBSOCKET_PORT/.test(html), 'websocket port added');
});
});
HMR("Adds script to pages even if there aren't any elements in the template", async ({ runtime }) => {
test("Adds script to pages even if there aren't any elements in the template", async () => {
const result = await runtime.load('/no-elements');
assert.ok(!result.error, `build error: ${result.error}`);
const html = result.contents;
const $ = doc(html);
assert.equal($('[src="/_snowpack/hmr-client.js"]').length, 1);
assert.ok(/window\.HMR_WEBSOCKET_PORT/.test(html), 'websocket port added');
});
});
HMR.run();

View file

@ -1,29 +1,52 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup, setupBuild } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const MarkdownPlugin = suite('Astro Markdown plugin tests');
describe('Astro Markdown plugins', () => {
let fixture;
let devServer;
setup(MarkdownPlugin, './fixtures/astro-markdown-plugins');
setupBuild(MarkdownPlugin, './fixtures/astro-markdown-plugins');
beforeAll(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-markdown-plugins/',
renderers: ['@astrojs/renderer-preact'],
markdownOptions: {
remarkPlugins: ['remark-code-titles', 'remark-slug', ['rehype-autolink-headings', { behavior: 'prepend' }]],
rehypePlugins: [
['rehype-toc', { headings: ['h2', 'h3'] }],
['rehype-add-classes', { 'h1,h2,h3': 'title' }],
],
},
buildOptions: {
sitemap: false,
},
});
devServer = await fixture.dev();
});
MarkdownPlugin('Can render markdown with plugins', async ({ runtime }) => {
const result = await runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
test('Can render markdown with plugins', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
const $ = doc(result.contents);
assert.equal($('.toc').length, 1, 'Added a TOC');
assert.ok($('#hello-world').hasClass('title'), 'Added .title to h1');
// test 1: Added a TOC
expect($('.toc')).toHaveLength(1);
// teste 2: Added .title to h1
expect($('#hello-world').hasClass('title')).toBeTrue();
});
test('Can render Astro <Markdown> with plugins', async () => {
const html = await fixture.fetch('/astro').then((res) => res.text());
const $ = cheerio.load(html);
// test 1: Added a TOC
expect($('.toc')).toHaveLength(1);
// teste 2: Added .title to h1
expect($('#hello-world').hasClass('title')).toBeTrue();
});
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.stop();
});
});
MarkdownPlugin('Can render Astro <Markdown> with plugins', async ({ runtime }) => {
const result = await runtime.load('/astro');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal($('.toc').length, 1, 'Added a TOC');
assert.ok($('#hello-world').hasClass('title'), 'Added .title to h1');
});
MarkdownPlugin.run();

View file

@ -1,141 +1,180 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup, setupBuild } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const Markdown = suite('Astro Markdown tests');
describe('Astro Markdown', () => {
let fixture;
setup(Markdown, './fixtures/astro-markdown');
setupBuild(Markdown, './fixtures/astro-markdown');
beforeAll(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-markdown/',
renderers: ['@astrojs/renderer-preact'],
buildOptions: {
sitemap: false,
},
});
});
Markdown('Can load markdown pages with Astro', async ({ runtime }) => {
const result = await runtime.load('/post');
assert.ok(!result.error, `build error: ${result.error}`);
describe('dev', () => {
let devServer;
const $ = doc(result.contents);
assert.ok($('#first').length, 'There is a div added in markdown');
assert.ok($('#test').length, 'There is a div added via a component from markdown');
beforeAll(async () => {
devServer = await fixture.dev();
});
test('Can load markdown pages with Astro', async () => {
const html = await fixture.fetch('/post').then((res) => res.text());
const $ = cheerio.load(html);
// test 1: There is a div added in markdown
expect($('#first').length).toBeTruthy();
// test 2: There is a div added via a component from markdown
expect($('#test').length).toBeTruthy();
});
test('Can load more complex jsxy stuff', async () => {
const html = await fixture.fetch('/complex').then((res) => res.text());
const $ = cheerio.load(html);
expect($('#test').text()).toBe('Hello world');
});
test('Empty code blocks do not fail', async () => {
const html = await fixture.fetch('/empty-code').then((res) => res.text());
const $ = cheerio.load(html);
// test 1: There is not a `<code>` in the codeblock
expect($('pre')[0].children).toHaveLength(1);
// test 2: The empty `<pre>` failed to render
expect($('pre')[1].children).toHaveLength(0);
});
test('Runs code blocks through syntax highlighter', async () => {
const html = await fixture.fetch('/code').then((res) => res.text());
const $ = cheerio.load(html);
// test 1: There are child spans in code blocks
expect($('code span').length).toBeGreaterThan(0);
});
test('Scoped styles should not break syntax highlight', async () => {
const html = await fixture.fetch('/scopedStyles-code').then((res) => res.text());
const $ = cheerio.load(html);
// test 1: <pre> tag has scopedStyle class passed down
expect($('pre').is('[class]')).toBe(true);
// test 2: <pre> tag has correct language
expect($('pre').hasClass('language-js')).toBe(true);
// test 3: <code> tag has correct language
expect($('code').hasClass('language-js')).toBe(true);
// test 4: There are child spans in code blocks
expect($('code span').length).toBeGreaterThan(0);
});
test('Renders correctly when deeply nested on a page', async () => {
const html = await fixture.fetch('/deep').then((res) => res.text());
const $ = cheerio.load(html);
// test 1: Rendered all children
expect($('#deep').children()).toHaveLength(3);
// tests 24: Only rendered title in each section
assert.equal($('.a').children()).toHaveLength(1);
assert.equal($('.b').children()).toHaveLength(1);
assert.equal($('.c').children()).toHaveLength(1);
// test 57: Rendered title in correct section
assert.equal($('.a > h2').text()).toBe('A');
assert.equal($('.b > h2').text()).toBe('B');
assert.equal($('.c > h2').text()).toBe('C');
});
test('Renders recursively', async () => {
const html = await fixture.fetch('/recursive').then((res) => res.text());
const $ = cheerio.load(html);
// tests 12: Rendered title correctly
expect($('.a > h1').text()).toBe('A');
expect($('.b > h1').text()).toBe('B');
expect($('.c > h1').text()).toBe('C');
});
test('Renders dynamic content though the content attribute', async () => {
const html = await fixture.fetch('/external').then((res) => res.text());
const $ = cheerio.load(html);
// test 1: Rendered markdown content
expect($('#outer')).toHaveLength(1);
// test 2: Nested markdown content
expect($('#inner')).toHaveLength(1);
// test 3: Scoped class passed down
expect($('#inner').is('[class]')).toBe(true);
});
test('Renders curly braces correctly', async () => {
const html = await fixture.fetch('/braces').then((res) => res.text());
const $ = cheerio.load(html);
// test 1: Rendered curly braces markdown content
expect($('code')).toHaveLength(3);
// test 2: Rendered curly braces markdown content
expect($('code:first-child').text()).toBe('({})');
// test 3: Rendered curly braces markdown content
expect($('code:nth-child(2)').text()).toBe('{...props}');
// test 4: Rendered curly braces markdown content
expect($('code:last-child').text()).toBe('{/* JavaScript */}');
});
test('Does not close parent early when using content attribute (#494)', async () => {
const html = await fixture.fetch('/close').then((res) => res.text());
const $ = cheerio.load(html);
// test <Markdown content /> closed div#target early
expect($('#target').children()).toHaveLength(2);
});
test('Can render markdown with --- for horizontal rule', async () => {
const result = await fixture.fetch('/dash');
expect(result.statusCode).toBe(200);
});
test('Can render markdown content prop (#1259)', async () => {
const html = await fixture.fetch('/content').then((res) => res.text());
const $ = cheerio.load(html);
// test Markdown rendered correctly via content prop
expect($('h1').text()).toBe('Foo');
});
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.stop();
});
});
describe('build', () => {
beforeAll(async () => {
await fixture.build();
});
test('Bundles client-side JS for prod', async () => {
const complexHtml = await fixture.readFile('/complex/index.html');
// test 1: Counter.js is loaded from page
expect(complexHtml).toEqual(expect.stringContaining(`import("/_astro/src/components/Counter.js"`));
// test 2: Counter.jsx is bundled for prod
const counterJs = await fixture.readFile('/_astro/src/components/Counter.js');
expect(counterJs).toBeTruthy();
});
});
});
Markdown('Can load more complex jsxy stuff', async ({ runtime }) => {
const result = await runtime.load('/complex');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
const $el = $('#test');
assert.equal($el.text(), 'Hello world');
});
Markdown('Runs code blocks through syntax highlighter', async ({ runtime }) => {
const result = await runtime.load('/code');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
const $el = $('code span');
assert.ok($el.length > 0, 'There are child spans in code blocks');
});
Markdown('Empty code blocks do not fail', async ({ runtime }) => {
const result = await runtime.load('/empty-code');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
const $el = $('pre');
assert.ok($el[0].children.length === 1, 'There is not a `<code>` in the codeblock');
assert.ok($el[1].children.length === 0, 'The empty `<pre>` failed to render');
});
Markdown('Scoped styles should not break syntax highlight', async ({ runtime }) => {
const result = await runtime.load('/scopedStyles-code');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.ok($('pre').is('[class]'), 'Pre tag has scopedStyle class passed down');
assert.ok($('pre').hasClass('language-js'), 'Pre tag has correct language');
assert.ok($('code').hasClass('language-js'), 'Code tag has correct language');
assert.ok($('code span').length > 0, 'There are child spans in code blocks');
});
Markdown('Bundles client-side JS for prod', async (context) => {
await context.build();
const complexHtml = await context.readFile('/complex/index.html');
assert.match(complexHtml, `import("/_astro/src/components/Counter.js"`);
const counterJs = await context.readFile('/_astro/src/components/Counter.js');
assert.ok(counterJs, 'Counter.jsx is bundled for prod');
});
Markdown('Renders correctly when deeply nested on a page', async ({ runtime }) => {
const result = await runtime.load('/deep');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal($('#deep').children().length, 3, 'Rendered all children');
assert.equal($('.a').children().length, 1, 'Only rendered title in each section');
assert.equal($('.b').children().length, 1, 'Only rendered title in each section');
assert.equal($('.c').children().length, 1, 'Only rendered title in each section');
assert.equal($('.a > h2').text(), 'A', 'Rendered title in correct section');
assert.equal($('.b > h2').text(), 'B', 'Rendered title in correct section');
assert.equal($('.c > h2').text(), 'C', 'Rendered title in correct section');
});
Markdown('Renders recursively', async ({ runtime }) => {
const result = await runtime.load('/recursive');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal($('.a > h1').text(), 'A', 'Rendered title .a correctly');
assert.equal($('.b > h1').text(), 'B', 'Rendered title .b correctly');
assert.equal($('.c > h1').text(), 'C', 'Rendered title .c correctly');
});
Markdown('Renders dynamic content though the content attribute', async ({ runtime }) => {
const result = await runtime.load('/external');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal($('#outer').length, 1, 'Rendered markdown content');
assert.equal($('#inner').length, 1, 'Nested markdown content');
assert.ok($('#inner').is('[class]'), 'Scoped class passed down');
});
Markdown('Renders curly braces correctly', async ({ runtime }) => {
const result = await runtime.load('/braces');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal($('code').length, 3, 'Rendered curly braces markdown content');
assert.equal($('code:first-child').text(), '({})', 'Rendered curly braces markdown content');
assert.equal($('code:nth-child(2)').text(), '{...props}', 'Rendered curly braces markdown content');
assert.equal($('code:last-child').text(), '{/* JavaScript */}', 'Rendered curly braces markdown content');
});
Markdown('Does not close parent early when using content attribute (#494)', async ({ runtime }) => {
const result = await runtime.load('/close');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal($('#target').children().length, 2, '<Markdown content /> closed div#target early');
});
Markdown('Can render markdown with --- for horizontal rule', async ({ runtime }) => {
const result = await runtime.load('/dash');
assert.ok(!result.error, `build error: ${result.error}`);
// It works!
});
Markdown('Can render markdown content prop (#1259)', async ({ runtime }) => {
const result = await runtime.load('/content');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal($('h1').text(), 'Foo', 'Markdown rendered correctly via content prop');
// It works!
});
Markdown.run();

View file

@ -1,16 +1,22 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { setupBuild } from './helpers.js';
import { loadFixture } from './test-utils.js';
const PageDirectoryUrl = suite('pageUrlFormat');
describe('pageUrlFormat', () => {
let fixture;
setupBuild(PageDirectoryUrl, './fixtures/astro-page-directory-url');
beforeAll(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-page-directory-url',
buildOptions: {
pageUrlFormat: 'file',
},
});
PageDirectoryUrl('outputs', async ({ build, readFile }) => {
await build();
assert.ok(await readFile('/client.html'));
assert.ok(await readFile('/nested-md.html'));
assert.ok(await readFile('/nested-astro.html'));
await fixture.build();
});
test('outputs', async () => {
expect(await fixture.readFile('/client.html')).toBeTruthy();
expect(await fixture.readFile('/nested-md.html')).toBeTruthy();
expect(await fixture.readFile('/nested-astro.html')).toBeTruthy();
});
});
PageDirectoryUrl.run();

View file

@ -1,24 +1,43 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup, setupBuild } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const Pages = suite('Pages tests');
describe('Pages', () => {
let fixture;
setup(Pages, './fixtures/astro-pages');
setupBuild(Pages, './fixtures/astro-pages');
Pages('Can find page with "index" at the end file name', async ({ build, runtime }) => {
await build().catch((err) => {
assert.ok(!err, 'Error during the build');
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-pages/' });
});
const result = await runtime.load('posts/name-with-index');
if (result.error) throw new Error(result.error);
describe('dev', () => {
let devServer;
const $ = doc(result.contents);
beforeAll(async () => {
devServer = await fixture.dev();
});
assert.equal($('h1').text(), 'Name with index');
test('Can find page with "index" at the end file name', async () => {
const html = await fixture.fetch('/posts/name-with-index').then((res) => res.text());
const $ = cheerio.load(html);
expect($('h1').text()).toBe('Name with index');
});
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.stop();
});
});
describe('build', () => {
beforeAll(async () => {
await fixture.build();
});
test('Can find page with "index" at the end file name', async () => {
const html = await fixture.readFile('/posts/name-with-index/index.html');
const $ = cheerio.load(html);
expect($('h1').text()).toBe('Name with index');
});
});
});
Pages.run();

View file

@ -1,67 +1,54 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const Global = suite('Astro.*');
describe('Pagination', () => {
let fixture;
let devServer;
setup(Global, './fixtures/astro-pagination');
beforeAll(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-pagination/',
buildOptions: {
site: 'https://mysite.dev/blog/',
sitemap: false,
},
});
devServer = await fixture.dev();
});
Global('optional root page', async (context) => {
{
const result = await context.runtime.load('/posts/optional-root-page/');
assert.ok(!result.error, `build error: ${result.error}`);
test('optional root page', async () => {
const results = await Promise.all([fixture.fetch('/posts/optional-root-page/'), fixture.fetch('/posts/optional-root-page/2'), fixture.fetch('/posts/optional-root-page/3')]);
for (const result of results) {
expect(result.statusCode).toBe(200);
}
{
const result = await context.runtime.load('/posts/optional-root-page/2');
assert.ok(!result.error, `build error: ${result.error}`);
}
{
const result = await context.runtime.load('/posts/optional-root-page/3');
assert.ok(!result.error, `build error: ${result.error}`);
});
test('named root page', async () => {
const results = await Promise.all([fixture.fetch('/posts/named-root-page/1'), fixture.fetch('/posts/named-root-page/2'), fixture.fetch('/posts/named-root-page/3')]);
for (const result of results) {
expect(result.statusCode).toBe(200);
}
});
test('multiple params', async () => {
const params = [
{ color: 'red', p: '1' },
{ color: 'blue', p: '1' },
{ color: 'blue', p: '2' },
];
await Promise.all(
params.map(({ color, p }) => {
const html = await fixture.fetch(`/posts/${color}/${p}`).then((res) => res.text());
const $ = cheerio.load(html);
expect($('#page-a').text()).toBe(p);
expect($('#page-b').text()).toBe(p);
expect($('#filter').text()).toBe(color);
})
);
});
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.stop();
});
});
Global('named root page', async (context) => {
{
const result = await context.runtime.load('/posts/named-root-page/1');
assert.ok(!result.error, `build error: ${result.error}`);
}
{
const result = await context.runtime.load('/posts/named-root-page/2');
assert.ok(!result.error, `build error: ${result.error}`);
}
{
const result = await context.runtime.load('/posts/named-root-page/3');
assert.ok(!result.error, `build error: ${result.error}`);
}
});
Global('multiple params', async (context) => {
{
const result = await context.runtime.load('/posts/red/1');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal($('#page-a').text(), '1');
assert.equal($('#page-b').text(), '1');
assert.equal($('#filter').text(), 'red');
}
{
const result = await context.runtime.load('/posts/blue/1');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal($('#page-a').text(), '1');
assert.equal($('#page-b').text(), '1');
assert.equal($('#filter').text(), 'blue');
}
{
const result = await context.runtime.load('/posts/blue/2');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal($('#page-a').text(), '2');
assert.equal($('#page-b').text(), '2');
assert.equal($('#filter').text(), 'blue');
}
});
Global.run();

View file

@ -1,21 +1,17 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { setup, setupBuild } from './helpers.js';
import { loadFixture } from './test-utils.js';
const Public = suite('Public');
describe('Public', () => {
let fixture;
setup(Public, './fixtures/astro-public');
setupBuild(Public, './fixtures/astro-public');
Public('css and js files do not get bundled', async ({ build, readFile }) => {
await build().catch((err) => {
assert.ok(!err, 'Error during the build');
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-public/' });
await fixture.build();
});
let indexHtml = await readFile('/index.html');
assert.ok(indexHtml.includes('<script src="/example.js"></script>'));
assert.ok(indexHtml.includes('<link href="/example.css" ref="stylesheet">'));
assert.ok(indexHtml.includes('<img src="/images/twitter.png">'));
test('css and js files do not get bundled', async () => {
let indexHtml = await fixture.readFile('/index.html');
expect(indexHtml).toEqual(expect.stringContaining('<script src="/example.js"></script>'));
expect(indexHtml).toEqual(expect.stringContaining('<link href="/example.css" ref="stylesheet">'));
expect(indexHtml).toEqual(expect.stringContaining('<img src="/images/twitter.png">'));
});
});
Public.run();

View file

@ -1,17 +1,20 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { setupBuild } from './helpers.js';
import { loadFixture } from './test-utils.js';
const RSS = suite('RSS Generation');
describe('RSS Generation', () => {
let fixture;
setupBuild(RSS, './fixtures/astro-rss');
beforeAll(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/astro-rss/',
buildOptions: {
site: 'https://mysite.dev',
},
});
await fixture.build();
});
const snapshot = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title><![CDATA[MF Doomcast]]></title><description><![CDATA[The podcast about the things you find on a picnic, or at a picnic table]]></description><link>https://mysite.dev/custom/feed.xml</link><language>en-us</language><itunes:author>MF Doom</itunes:author><item><title><![CDATA[Rap Snitch Knishes (feat. Mr. Fantastik)]]></title><link>https://mysite.dev/episode/rap-snitch-knishes/</link><description><![CDATA[Complex named this song the “22nd funniest rap song of all time.”]]></description><pubDate>Tue, 16 Nov 2004 00:00:00 GMT</pubDate><itunes:episodeType>music</itunes:episodeType><itunes:duration>172</itunes:duration><itunes:explicit>true</itunes:explicit></item><item><title><![CDATA[Fazers]]></title><link>https://mysite.dev/episode/fazers/</link><description><![CDATA[Rhapsody ranked Take Me to Your Leader 17th on its list “Hip-Hops Best Albums of the Decade”]]></description><pubDate>Thu, 03 Jul 2003 00:00:00 GMT</pubDate><itunes:episodeType>music</itunes:episodeType><itunes:duration>197</itunes:duration><itunes:explicit>true</itunes:explicit></item><item><title><![CDATA[Rhymes Like Dimes (feat. Cucumber Slice)]]></title><link>https://mysite.dev/episode/rhymes-like-dimes/</link><description><![CDATA[Operation: Doomsday has been heralded as an underground classic that established MF Doom's rank within the underground hip-hop scene during the early to mid-2000s.\n]]></description><pubDate>Tue, 19 Oct 1999 00:00:00 GMT</pubDate><itunes:episodeType>music</itunes:episodeType><itunes:duration>259</itunes:duration><itunes:explicit>true</itunes:explicit></item></channel></rss>`;
RSS('Generates RSS correctly', async (context) => {
await context.build();
let rss = await context.readFile('/custom/feed.xml');
assert.match(rss, snapshot);
it('generates RSS correctly', async () => {
const rss = await fixture.readFile('/custom/feed.xml');
expect(rss).toMatchSnapshot();
});
});
RSS.run();

View file

@ -1,46 +1,44 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { scopeRule } from '#astro/compiler';
/**
* note(drew): TODO: update this test for compiler
*/
const ScopedStyles = suite('Astro PostCSS Scoped Styles Plugin');
// import { scopeRule } from '../dist/compiler';
const className = 'astro-abcd1234';
ScopedStyles('Scopes rules correctly', () => {
// Note: assume all selectors have no unnecessary spaces (i.e. must be minified)
const tests = {
'.class': `.class.${className}`,
h1: `h1.${className}`,
'.nav h1': `.nav.${className} h1.${className}`,
'.class+.class': `.class.${className}+.class.${className}`,
'.class~:global(a)': `.class.${className}~a`,
'.class *': `.class.${className} .${className}`,
'.class>*': `.class.${className}>.${className}`,
'.class button:focus': `.class.${className} button.${className}:focus`,
'.class h3::before': `.class.${className} h3.${className}::before`,
'button:focus::before': `button.${className}:focus::before`,
'.class :global(*)': `.class.${className} *`,
'.class :global(.nav:not(.is-active))': `.class.${className} .nav:not(.is-active)`, // preserve nested parens
'.class :global(ul li)': `.class.${className} ul li`, // allow doubly-scoped selectors
':global(body:not(.is-light)).is-dark,:global(body:not(.is-dark)).is-light': `body:not(.is-light).is-dark,body:not(.is-dark).is-light`, // :global() can contain parens, and can be chained off of
':global(.foo):global(.bar)': '.foo.bar', // more :global() shenanigans
'.class:global(.bar)': `.class.bar`, // this is technically a “useless“ :global() but it should still be extracted
'.class:not(.is-active):not(.is-disabled)': `.class.${className}:not(.is-active):not(.is-disabled)`, // Note: the :not() selector can NOT contain multiple classes, so this is correct; if this causes issues for some people then its worth a discussion
':hover.a:focus': `.${className}:hover.a:focus`, // weird but still valid (yes, its valid)
'*:hover': `.${className}:hover`,
':not(.is-disabled).a': `.${className}:not(.is-disabled).a`, // also valid
'body h1': `body h1.${className}`, // body shouldnt be scoped; its not a component
'html,body': `html,body`,
from: 'from', // ignore keyframe keywords (below)
to: 'to',
'55%': '55%',
'.class\\:class': `.class\\:class.${className}`, // classes can contain special characters if escaped
'.class\\:class:focus': `.class\\:class.${className}:focus`,
};
for (const [given, expected] of Object.entries(tests)) {
assert.equal(scopeRule(given, className), expected);
}
describe.skip('Scoped styles', () => {
// test('Scopes rules correctly', () => {
// const className = 'astro-abcd1234';
// // Note: assume all selectors have no unnecessary spaces (i.e. must be minified)
// const tests = {
// '.class': `.class.${className}`,
// h1: `h1.${className}`,
// '.nav h1': `.nav.${className} h1.${className}`,
// '.class+.class': `.class.${className}+.class.${className}`,
// '.class~:global(a)': `.class.${className}~a`,
// '.class *': `.class.${className} .${className}`,
// '.class>*': `.class.${className}>.${className}`,
// '.class button:focus': `.class.${className} button.${className}:focus`,
// '.class h3::before': `.class.${className} h3.${className}::before`,
// 'button:focus::before': `button.${className}:focus::before`,
// '.class :global(*)': `.class.${className} *`,
// '.class :global(.nav:not(.is-active))': `.class.${className} .nav:not(.is-active)`, // preserve nested parens
// '.class :global(ul li)': `.class.${className} ul li`, // allow doubly-scoped selectors
// ':global(body:not(.is-light)).is-dark,:global(body:not(.is-dark)).is-light': `body:not(.is-light).is-dark,body:not(.is-dark).is-light`, // :global() can contain parens, and can be chained off of
// ':global(.foo):global(.bar)': '.foo.bar', // more :global() shenanigans
// '.class:global(.bar)': `.class.bar`, // this is technically a “useless“ :global() but it should still be extracted
// '.class:not(.is-active):not(.is-disabled)': `.class.${className}:not(.is-active):not(.is-disabled)`, // Note: the :not() selector can NOT contain multiple classes, so this is correct; if this causes issues for some people then its worth a discussion
// ':hover.a:focus': `.${className}:hover.a:focus`, // weird but still valid (yes, its valid)
// '*:hover': `.${className}:hover`,
// ':not(.is-disabled).a': `.${className}:not(.is-disabled).a`, // also valid
// 'body h1': `body h1.${className}`, // body shouldnt be scoped; its not a component
// 'html,body': `html,body`,
// from: 'from', // ignore keyframe keywords (below)
// to: 'to',
// '55%': '55%',
// '.class\\:class': `.class\\:class.${className}`, // classes can contain special characters if escaped
// '.class\\:class:focus': `.class\\:class.${className}:focus`,
// };
// for (const [given, expected] of Object.entries(tests)) {
// expect(scopeRule(given, className)).toBe(expected);
// }
// });
});
ScopedStyles.run();

View file

@ -1,62 +1,79 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { setup, setupBuild } from './helpers.js';
import { doc } from './test-utils.js';
import cheerio from 'cheerio';
import path from 'path';
import { loadFixture } from './test-utils.js';
const Scripts = suite('Hoisted scripts');
describe('Hoisted scripts', () => {
let fixture;
setup(Scripts, './fixtures/astro-scripts');
setupBuild(Scripts, './fixtures/astro-scripts');
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-scripts/' });
});
Scripts('Moves external scripts up', async ({ runtime }) => {
const result = await runtime.load('/external');
if (result.error) throw new Error(result.error);
assert.equal(result.statusCode, 200);
const html = result.contents;
describe('dev', () => {
let devServer;
beforeAll(async () => {
devServer = await fixture.dev();
});
test('Moves external scripts up', async () => {
const html = await fixture.fetch('/external').then((res) => res.text());
const $ = doc(html);
assert.equal($('head script[type="module"][data-astro="hoist"]').length, 2);
assert.equal($('body script').length, 0);
});
Scripts('Moves inline scripts up', async ({ runtime }) => {
const result = await runtime.load('/inline');
if (result.error) throw new Error(result.error);
assert.equal(result.statusCode, 200);
const html = result.contents;
expect($('head script[type="module"][data-astro="hoist"]')).toHaveLength(2);
expect($('body script')).toHaveLength(0);
});
test('Moves inline scripts up', async () => {
const html = await fixture.fetch('/inline').then((res) => res.text());
const $ = doc(html);
assert.equal($('head script[type="module"][data-astro="hoist"]').length, 1);
assert.equal($('body script').length, 0);
});
Scripts('Builds the scripts to a single bundle', async ({ build, readFile }) => {
try {
await build();
} catch (err) {
console.error(err.stack);
assert.ok(!err);
return;
}
expect($('head script[type="module"][data-astro="hoist"]')).toHaveLength(1);
expect($('body script')).toHaveLength(0);
});
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.stop();
});
});
describe('build', () => {
beforeAll(async () => {
await fixture.build();
});
test('Inline page builds the scripts to a single bundle', async () => {
/* Inline page */
let inline = await readFile('/inline/index.html');
let $ = doc(inline);
assert.equal($('script').length, 1, 'Just one entry module');
assert.equal($('script').attr('data-astro'), undefined, 'attr removed');
let entryURL = path.join('inline', $('script').attr('src'));
let inlineEntryJS = await readFile(entryURL);
assert.ok(inlineEntryJS, 'The JS exists');
let inline = await fixture.readFile('/inline/index.html');
let $ = cheerio.load(inline);
/* External page */
let external = await readFile('/external/index.html');
// test 1: Just one entry module
assert.equal($('script')).toHaveLength(1);
// test 2: attr removed
expect($('script').attr('data-astro')).toBe(undefined);
let entryURL = path.join('inline', $('script').attr('src'));
let inlineEntryJS = await fixture.readFile(entryURL);
// test 3: the JS exists
expect(inlineEntryJS).toBeTruthy();
});
test('External page builds the scripts to a single bundle', async () => {
let external = await fixture.readFile('/external/index.html');
$ = doc(external);
assert.equal($('script').length, 2, 'There are two scripts');
// test 1: there are two scripts
assert.equal($('script')).toHaveLength(2);
let el = $('script').get(1);
entryURL = path.join('external', $(el).attr('src'));
let externalEntryJS = await readFile(entryURL);
assert.ok(externalEntryJS, 'got JS');
});
Scripts.run();
// test 2: the JS exists
expect(externalEntryJS).toBeTruthy();
});
});
});

View file

@ -1,29 +0,0 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { setup } from './helpers.js';
const Search = suite('Search paths');
setup(Search, './fixtures/astro-basic');
Search('Finds the root page', async ({ runtime }) => {
const result = await runtime.load('/');
assert.equal(result.statusCode, 200);
});
Search('Matches pathname to filename', async ({ runtime }) => {
assert.equal((await runtime.load('/news')).statusCode, 200);
assert.equal((await runtime.load('/news/')).statusCode, 200);
});
Search('Matches pathname to a nested index.astro file', async ({ runtime }) => {
assert.equal((await runtime.load('/nested-astro')).statusCode, 200);
assert.equal((await runtime.load('/nested-astro/')).statusCode, 200);
});
Search('Matches pathname to a nested index.md file', async ({ runtime }) => {
assert.equal((await runtime.load('/nested-md')).statusCode, 200);
assert.equal((await runtime.load('/nested-md/')).statusCode, 200);
});
Search.run();

View file

@ -1,17 +1,15 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { setupBuild } from './helpers.js';
import { loadFixture } from './test-utils.js';
const Sitemap = suite('Sitemap Generation');
describe('Sitemap Generation', () => {
let fixture;
setupBuild(Sitemap, './fixtures/astro-rss');
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-rss/' });
await fixture.build();
});
const snapshot = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>https://mysite.dev/episode/fazers/</loc></url><url><loc>https://mysite.dev/episode/rap-snitch-knishes/</loc></url><url><loc>https://mysite.dev/episode/rhymes-like-dimes/</loc></url><url><loc>https://mysite.dev/episodes/</loc></url></urlset>\n`;
Sitemap('Generates Sitemap correctly', async (context) => {
await context.build();
let sitemap = await context.readFile('/sitemap.xml');
assert.match(sitemap, snapshot);
test('Generates Sitemap correctly', async () => {
let sitemap = await fixture.readFile('/sitemap.xml');
expect(sitemap).toMatchSnapshot();
});
});
Sitemap.run();

View file

@ -1,133 +1,120 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup, setupBuild } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const Slots = suite('Slot test');
describe('Slots', () => {
let fixture;
let devServer;
setup(Slots, './fixtures/astro-slots', {
runtimeOptions: {
mode: 'development',
},
});
setupBuild(Slots, './fixtures/astro-slots');
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-slots/' });
devServer = await fixture.dev();
});
Slots('Basic named slots work', async ({ runtime }) => {
const result = await runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
test('Basic named slots work', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
const $ = doc(result.contents);
expect($('#a').text()).toBe('A');
expect($('#b').text()).toBe('B');
expect($('#c').text()).toBe('C');
expect($('#default').text()).toBe('Default');
});
assert.equal($('#a').text(), 'A');
assert.equal($('#b').text(), 'B');
assert.equal($('#c').text(), 'C');
assert.equal($('#default').text(), 'Default');
});
test('Dynamic named slots work', async () => {
const html = await fixture.fetch('/dynamic').then((res) => res.text());
const $ = cheerio.load(html);
Slots('Dynamic named slots work', async ({ runtime }) => {
const result = await runtime.load('/dynamic');
assert.ok(!result.error, `build error: ${result.error}`);
expect($('#a').text()).toBe('A');
expect($('#b').text()).toBe('B');
expect($('#c').text()).toBe('C');
expect($('#default').text()).toBe('Default');
});
const $ = doc(result.contents);
test('Slots render fallback content by default', async () => {
const html = await fixture.fetch('/fallback').then((res) => res.text());
const $ = cheerio.load(html);
assert.equal($('#a').text(), 'A');
assert.equal($('#b').text(), 'B');
assert.equal($('#c').text(), 'C');
assert.equal($('#default').text(), 'Default');
});
expect($('#default')).toHaveLength(1);
});
Slots('Slots render fallback content by default', async ({ runtime }) => {
const result = await runtime.load('/fallback');
assert.ok(!result.error, `build error: ${result.error}`);
test('Slots override fallback content', async () => {
const html = await fixture.fetch('/fallback-override').then((res) => res.text());
const $ = cheerio.load(html);
const $ = doc(result.contents);
expect($('#override')).toHaveLength(1);
});
assert.equal($('#default').length, 1);
});
test('Slots work with multiple elements', async () => {
const html = await fixture.fetch('/multiple').then((res) => res.text());
const $ = cheerio.load(html);
Slots('Slots override fallback content', async ({ runtime }) => {
const result = await runtime.load('/fallback-override');
assert.ok(!result.error, `build error: ${result.error}`);
expect($('#a').text()).toBe('ABC');
});
const $ = doc(result.contents);
test('Slots work on Components', async () => {
const html = await fixture.fetch('/component').then((res) => res.text());
const $ = cheerio.load(html);
assert.equal($('#override').length, 1);
});
// test 1: #a renders
expect($('#a')).toHaveLength(1);
Slots('Slots work with multiple elements', async ({ runtime }) => {
const result = await runtime.load('/multiple');
assert.ok(!result.error, `build error: ${result.error}`);
// test 2: Slotted component into #a
expect($('#a').children('astro-component')).toHaveLength(1);
const $ = doc(result.contents);
// test 3: Slotted component into default slot
expect($('#default').children('astro-component')).toHaveLength(1);
});
assert.equal($('#a').text(), 'ABC');
});
Slots('Slots work on Components', async ({ runtime }) => {
const result = await runtime.load('/component');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal($('#a').length, 1);
assert.equal($('#a').children('astro-component').length, 1, 'Slotted component into #a');
assert.equal($('#default').children('astro-component').length, 1, 'Slotted component into default slot');
});
Slots('Slots API work on Components', async ({ runtime }) => {
test('Slots API work on Components', async () => {
// IDs will exist whether the slots are filled or not
{
const result = await runtime.load('/slottedapi-default');
assert.ok(!result.error, `build error: ${result.error}`);
const html = await fixture.fetch('/slottedapi-default').then((res) => res.text());
const $ = cheerio.load(html);
const $ = doc(result.contents);
assert.equal($('#a').length, 1);
assert.equal($('#b').length, 1);
assert.equal($('#c').length, 1);
assert.equal($('#default').length, 1);
expect($('#a')).toHaveLength(1);
expect($('#b')).toHaveLength(1);
expect($('#c')).toHaveLength(1);
expect($('#default')).toHaveLength(1);
}
// IDs will not exist because the slots are not filled
{
const result = await runtime.load('/slottedapi-empty');
assert.ok(!result.error, `build error: ${result.error}`);
const html = await fixture.fetch('/slottedapi-empty').then((res) => res.text());
const $ = cheerio.load(html);
const $ = doc(result.contents);
assert.equal($('#a').length, 0);
assert.equal($('#b').length, 0);
assert.equal($('#c').length, 0);
assert.equal($('#default').length, 0);
expect($('#a')).toHaveLength(0);
expect($('#b')).toHaveLength(0);
expect($('#c')).toHaveLength(0);
expect($('#default')).toHaveLength(0);
}
// IDs will exist because the slots are filled
{
const result = await runtime.load('/slottedapi-filled');
assert.ok(!result.error, `build error: ${result.error}`);
const html = await fixture.fetch('/slottedapi-filled').then((res) => res.text());
const $ = cheerio.load(html);
const $ = doc(result.contents);
expect($('#a')).toHaveLength(1);
expect($('#b')).toHaveLength(1);
expect($('#c')).toHaveLength(1);
assert.equal($('#a').length, 1);
assert.equal($('#b').length, 1);
assert.equal($('#c').length, 1);
assert.equal($('#default').length, 0); // the default slot is not filled
expect($('#default')).toHaveLength(0); // the default slot is not filled
}
// Default ID will exist because the default slot is filled
{
const result = await runtime.load('/slottedapi-default-filled');
assert.ok(!result.error, `build error: ${result.error}`);
const html = await fixture.fetch('/slottedapi-default-filled').then((res) => res.text());
const $ = cheerio.load(html);
const $ = doc(result.contents);
expect($('#a')).toHaveLength(0);
expect($('#b')).toHaveLength(0);
expect($('#c')).toHaveLength(0);
assert.equal($('#a').length, 0);
assert.equal($('#b').length, 0);
assert.equal($('#c').length, 0);
assert.equal($('#default').length, 1); // the default slot is filled
expect($('#default')).toHaveLength(1); // the default slot is filled
}
});
});
Slots.run();
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.stop();
});
});

View file

@ -1,9 +1,5 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
const StylesSSR = suite('Styles SSR');
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
/** Basic CSS minification; removes some flakiness in testing CSS */
function cssMinify(css) {
@ -15,35 +11,37 @@ function cssMinify(css) {
.replace(/;}/g, '}'); // collapse block
}
setup(StylesSSR, './fixtures/astro-styles-ssr');
describe('Styles SSR', () => {
let fixture;
let devServer;
StylesSSR('Has <link> tags', async ({ runtime }) => {
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-styles-ssr/' });
devServer = await fixture.dev();
});
test('Has <link> tags', async () => {
const MUST_HAVE_LINK_TAGS = [
'/_astro/src/components/ReactCSS.css',
'/_astro/src/components/ReactModules.module.css',
'/_astro/src/components/SvelteScoped.svelte.css',
'/_astro/src/components/VueCSS.vue.css',
'/_astro/src/components/VueModules.vue.css',
'/_astro/src/components/VueScoped.vue.css',
'/src/components/ReactCSS.css',
'/src/components/ReactModules.module.css',
'/src/components/SvelteScoped.css',
'/src/components/VueCSS.css',
'/src/components/VueModules.css',
'/src/components/VueScoped.css',
];
const result = await runtime.load('/');
const $ = doc(result.contents);
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
for (const href of MUST_HAVE_LINK_TAGS) {
const el = $(`link[href="${href}"]`);
assert.equal(el.length, 1);
expect(el).toHaveLength(1);
}
});
});
StylesSSR('Has correct CSS classes', async ({ runtime }) => {
// TODO: remove this (temporary CI patch)
if (process.version.startsWith('v14.')) {
return;
}
const result = await runtime.load('/');
const $ = doc(result.contents);
test('Has correct CSS classes', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
const MUST_HAVE_CLASSES = {
'#react-css': 'react-title',
@ -58,29 +56,29 @@ StylesSSR('Has correct CSS classes', async ({ runtime }) => {
const el = $(selector);
if (selector === '#react-modules' || selector === '#vue-modules') {
// this will generate differently on Unix vs Windows. Here we simply test that it has transformed
assert.match(el.attr('class'), new RegExp(`^_${className}_[A-Za-z0-9-_]+`)); // className should be transformed, surrounded by underscores and other stuff
expect(el.attr('class')).toEqual(expect.stringMatching(new RegExp(`^_${className}_[A-Za-z0-9-_]+`))); // className should be transformed, surrounded by underscores and other stuff
} else {
// if this is not a CSS module, it should remain as expected
assert.ok(el.attr('class').includes(className));
expect(el.attr('class')).toEqual(expect.stringContaining(className));
}
// addl test: Vue Scoped styles should have data-v-* attribute
if (selector === '#vue-scoped') {
const { attribs } = el.get(0);
const scopeId = Object.keys(attribs).find((k) => k.startsWith('data-v-'));
assert.ok(scopeId);
expect(scopeId).toBeTruthy();
}
// addl test: Svelte should have another class
if (selector === '#svelte-title') {
assert.not.equal(el.attr('class'), className);
expect(el.attr('class')).not.toBe(className);
}
}
});
});
StylesSSR('CSS Module support in .astro', async ({ runtime }) => {
const result = await runtime.load('/');
const $ = doc(result.contents);
test('CSS Module support in .astro', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
let scopedClass;
@ -94,16 +92,16 @@ StylesSSR('CSS Module support in .astro', async ({ runtime }) => {
})
);
assert.match(css, `.wrapper${scopedClass}{margin-left:auto;margin-right:auto;max-width:1200px}`);
expect(css).toBe(`.wrapper${scopedClass}{margin-left:auto;margin-right:auto;max-width:1200px}`);
// test 2: element received .astro-XXXXXX class (this selector will succeed if transformed correctly)
const wrapper = $(`.wrapper${scopedClass}`);
assert.equal(wrapper.length, 1);
});
expect(wrapper).toHaveLength(1);
});
StylesSSR('Astro scoped styles', async ({ runtime }) => {
const result = await runtime.load('/');
const $ = doc(result.contents);
test('Astro scoped styles', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
const el1 = $('#dynamic-class');
const el2 = $('#dynamic-vis');
@ -117,24 +115,30 @@ StylesSSR('Astro scoped styles', async ({ runtime }) => {
return match;
});
assert.ok(scopedClass, `Astro component missing scoped class`);
assert.match(el1.attr('class'), `blue ${scopedClass}`);
assert.match(el2.attr('class'), `visible ${scopedClass}`);
// test 1: Astro component missing scoped class
expect(scopedClass).toBe(``);
const { contents: css } = await runtime.load('/_astro/src/components/Astro.astro.css');
assert.match(cssMinify(css.toString()), `.blue.${scopedClass}{color:powderblue}.color\\:blue.${scopedClass}{color:powderblue}.visible.${scopedClass}{display:block}`);
});
// test 23: children get scoped class
expect(el1.attr('class')).toBe(`blue ${scopedClass}`);
expect(el2.attr('class')).toBe(`visible ${scopedClass}`);
StylesSSR('Astro scoped styles skipped without <style>', async ({ runtime }) => {
const result = await runtime.load('/');
const $ = doc(result.contents);
const { contents: css } = await fixture.fetch('/src/components/Astro.astro.css').then((res) => res.text());
assert.type($('#no-scope').attr('class'), 'undefined', `Astro component without <style> should not include scoped class`);
});
// test 4: CSS generates as expected
expect(cssMinify(css.toString())).toBe(`.blue.${scopedClass}{color:powderblue}.color\\:blue.${scopedClass}{color:powderblue}.visible.${scopedClass}{display:block}`);
});
StylesSSR('Astro scoped styles can be passed to child components', async ({ runtime }) => {
const result = await runtime.load('/');
const $ = doc(result.contents);
test('Astro scoped styles skipped without <style>', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
// test 1: Astro component without <style> should not include scoped class
expect($('#no-scope').attr('class')).toBe(undefined);
});
test('Astro scoped styles can be passed to child components', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
let scopedClass;
$('style')
@ -144,7 +148,11 @@ StylesSSR('Astro scoped styles can be passed to child components', async ({ runt
return match;
});
assert.match($('#passed-in').attr('class'), `outer ${scopedClass}`);
});
expect($('#passed-in').attr('class')).toBe(`outer ${scopedClass}`);
});
StylesSSR.run();
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.stop();
});
});

View file

@ -1,27 +1,25 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup, setupBuild } from './helpers.js';
import { loadFixture } from './test-utils.js';
const Throwable = suite('Throw test');
describe('Throw', () => {
let fixture;
let devServer;
setup(Throwable, './fixtures/astro-throw', {
runtimeOptions: {
mode: 'development',
},
});
setupBuild(Throwable, './fixtures/astro-throw');
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-throw/' });
devServer = await fixture.dev();
});
Throwable('Can throw an error from an `.astro` file', async ({ runtime }) => {
const result = await runtime.load('/');
assert.equal(result.statusCode, 500);
assert.equal(result.error.message, 'Oops!');
});
test('Can throw an error from an `.astro` file', async () => {
const result = await fixture.fetch('/');
expect(result.statusCode).toBe(500);
});
Throwable('Does not complete build when Error is thrown', async ({ build }) => {
await build().catch((e) => {
assert.ok(e, 'Build threw');
test('Does not complete build when Error is thrown', async () => {
expect(() => fixture.build()).toThrow();
});
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.stop();
});
});
Throwable.run();

View file

@ -1,19 +1,22 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const Builtins = suite('Node builtins with polyfillNode option');
describe('Node builtins with polyfillNode option', () => {
let fixture;
setup(Builtins, './fixtures/builtins-polyfillnode');
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/builtins-polyfillnode/' });
});
Builtins('Doesnt alias to node: prefix', async ({ runtime }) => {
const result = await runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
test('Doesnt alias to node: prefix', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
const $ = doc(result.contents);
expect($('#url').text()).toBe('unicorn.jpg');
});
assert.match($('#url').text(), new RegExp('unicorn.jpg'));
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.stop();
});
});
Builtins.run();

View file

@ -1,30 +1,35 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const Builtins = suite('Node builtins');
describe('Node builtins', () => {
let fixture;
let devServer;
setup(Builtins, './fixtures/builtins');
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/builtins/' });
devServer = await fixture.dev();
});
Builtins('Can be used with the node: prefix', async ({ runtime }) => {
test('Can be used with the node: prefix', async () => {
// node:fs/promise is not supported in Node v12. Test currently throws.
if (process.versions.node <= '13') {
return;
}
const result = await runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
const result = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
const $ = doc(result.contents);
expect($('#version').text()).toBe('1.2.0');
expect($('#dep-version').text()).toBe('0.0.1');
});
assert.equal($('#version').text(), '1.2.0');
assert.equal($('#dep-version').text(), '0.0.1');
test('Throw if using the non-prefixed version', async () => {
const result = await fixture.fetch('/bare');
expect(result.statusCode).toBe(500);
expect(result.body).toEqual(expect.stringContaining('Use node:fs instead'));
});
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.stop();
});
});
Builtins('Throw if using the non-prefixed version', async ({ runtime }) => {
const result = await runtime.load('/bare');
assert.ok(result.error, 'Produced an error');
assert.ok(/Use node:fs instead/.test(result.error.message));
});
Builtins.run();

View file

@ -1,43 +0,0 @@
import { fileURLToPath } from 'url';
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { runDevServer } from './helpers.js';
import { loadConfig } from '#astro/config';
const ConfigPort = suite('Config hostname');
const MAX_TEST_TIME = 10000; // max time this test suite may take
const root = new URL('./fixtures/config-hostname/', import.meta.url);
const timers = {};
ConfigPort.before.each(({ __test__ }) => {
timers[__test__] = setTimeout(() => {
throw new Error(`Test "${__test__}" did not finish within allowed time`);
}, MAX_TEST_TIME);
});
ConfigPort('can be specified in the astro config', async (context) => {
const astroConfig = await loadConfig(fileURLToPath(root));
assert.equal(astroConfig.devOptions.hostname, '0.0.0.0');
});
ConfigPort('can be specified via --hostname flag', async (context) => {
const args = ['--hostname', '127.0.0.1'];
const proc = runDevServer(root, args);
proc.stdout.setEncoding('utf8');
for await (const chunk of proc.stdout) {
if (/Local:/.test(chunk)) {
assert.ok(/:127.0.0.1/.test(chunk), 'Using the right hostname');
break;
}
}
proc.kill();
});
ConfigPort.after.each(({ __test__ }) => {
clearTimeout(timers[__test__]);
});
ConfigPort.run();

View file

@ -1,37 +0,0 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { runDevServer } from './helpers.js';
const ConfigPath = suite('Config path');
const MAX_TEST_TIME = 10000; // max time this test suite may take
const root = new URL('./fixtures/config-path/', import.meta.url);
const timers = {};
ConfigPath.before.each(({ __test__ }) => {
timers[__test__] = setTimeout(() => {
throw new Error(`Test "${__test__}" did not finish within allowed time`);
}, MAX_TEST_TIME);
});
ConfigPath('can be passed via --config', async (context) => {
const configPath = new URL('./config/my-config.mjs', root).pathname;
const args = ['--config', configPath];
const process = runDevServer(root, args);
process.stdout.setEncoding('utf8');
for await (const chunk of process.stdout) {
if (/Server started/.test(chunk)) {
break;
}
}
process.kill();
assert.ok(true, 'Server started');
});
ConfigPath.after.each(({ __test__ }) => {
clearTimeout(timers[__test__]);
});
ConfigPath.run();

View file

@ -1,43 +0,0 @@
import { fileURLToPath } from 'url';
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { runDevServer } from './helpers.js';
import { loadConfig } from '#astro/config';
const ConfigPort = suite('Config path');
const MAX_TEST_TIME = 10000; // max time this test suite may take
const root = new URL('./fixtures/config-port/', import.meta.url);
const timers = {};
ConfigPort.before.each(({ __test__ }) => {
timers[__test__] = setTimeout(() => {
throw new Error(`Test "${__test__}" did not finish within allowed time`);
}, MAX_TEST_TIME);
});
ConfigPort('can be specified in the astro config', async (context) => {
const astroConfig = await loadConfig(fileURLToPath(root));
assert.equal(astroConfig.devOptions.port, 3001);
});
ConfigPort('can be specified via --port flag', async (context) => {
const args = ['--port', '3002'];
const proc = runDevServer(root, args);
proc.stdout.setEncoding('utf8');
for await (const chunk of proc.stdout) {
if (/Local:/.test(chunk)) {
assert.ok(/:3002/.test(chunk), 'Using the right port');
break;
}
}
proc.kill();
});
ConfigPort.after.each(({ __test__ }) => {
clearTimeout(timers[__test__]);
});
ConfigPort.run();

View file

@ -0,0 +1,79 @@
import { loadConfig } from '../dist/config';
import { devCLI, loadFixture } from './test-utils.js';
describe('config', () => {
describe('hostname', () => {
const cwd = './fixtures/config-hostname/';
const cwdURL = new URL(cwd, import.meta.url);
test('can be specified in astro.config.mjs', async () => {
const fixture = await loadFixture({
projectRoot: cwd,
devOptions: { hostname: '0.0.0.0' },
});
expect(fixture.config.devOptions.hostname).toBe('0.0.0.0');
});
test('can be specified via --hostname flag', async () => {
const args = ['--hostname', '127.0.0.1'];
const proc = devCLI(cwdURL, args);
proc.stdout.setEncoding('utf8');
for await (const chunk of proc.stdout) {
if (/Local:/.test(chunk)) {
expect(chunk).toEqual(expect.stringContaining('127.0.0.1'));
break;
}
}
proc.kill();
});
});
describe('path', () => {
const cwd = './fixtures/config-path/';
const cwdURL = new URL(cwd, import.meta.url);
test('can be passed via --config', async () => {
const configPath = new URL('./config/my-config.mjs', cwdURL).pathname;
const args = ['--config', configPath];
const process = devCLI(cwdURL, args);
process.stdout.setEncoding('utf8');
for await (const chunk of process.stdout) {
if (/Server started/.test(chunk)) {
break;
}
}
process.kill();
// test will time out if the server never started
});
});
describe('port', () => {
const cwd = './fixtures/config-port/';
const cwdURL = new URL(cwd, import.meta.url);
test.skip('can be specified in astro.config.mjs', async () => {
const config = await loadConfig(cwdURL);
expect(config.devOptions.port).toEqual(5001);
});
test.skip('can be specified via --port flag', async () => {
const args = ['--port', '5002']; // note: this should be on the higher-end of possible ports
const proc = devCLI(cwdURL, args);
proc.stdout.setEncoding('utf8');
for await (const chunk of proc.stdout) {
if (/Local:/.test(chunk)) {
expect(chunk).toEqual(expect.stringContaining(':5002'));
break;
}
}
proc.kill();
// test will time out on a different port
});
});
});

View file

@ -1,83 +1,96 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const CustomElements = suite('Custom Elements');
describe('Custom Elements', () => {
let fixture;
let devServer;
setup(CustomElements, './fixtures/custom-elements');
beforeAll(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/custom-elements/',
renderers: ['@astrojs/test-custom-element-renderer'],
});
devServer = await fixture.dev();
});
CustomElements('Work as constructors', async ({ runtime }) => {
const result = await runtime.load('/ctr');
assert.ok(!result.error, `build error: ${result.error}`);
test('Work as constructors', async () => {
const html = await fixture.fetch('/ctr').then((res) => res.text());
const $ = cheerio.load(html);
const $ = doc(result.contents);
assert.equal($('my-element').length, 1, 'Element rendered');
assert.equal($('my-element template[shadowroot=open]').length, 1, 'shadow rendered');
});
// test 1: Element rendered
expect($('my-element')).toHaveLength(1);
CustomElements('Works with exported tagName', async ({ runtime }) => {
const result = await runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
// test 2: shadow rendererd
expect($('my-element template[shadowroot=open]')).toHaveLength(1);
});
const $ = doc(result.contents);
assert.equal($('my-element').length, 1, 'Element rendered');
assert.equal($('my-element template[shadowroot=open]').length, 1, 'shadow rendered');
});
test('Works with exported tagName', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
CustomElements('Hydration works with exported tagName', async ({ runtime }) => {
const result = await runtime.load('/load');
assert.ok(!result.error, `build error: ${result.error}`);
// test 1: Element rendered
expect($('my-element')).toHaveLength(1);
const html = result.contents;
const $ = doc(html);
// test 2: shadow rendered
expect($('my-element template[shadowroot=open]')).toHaveLength(1);
});
test('Hydration works with exported tagName', async () => {
const html = await fixture.fetch('/load').then((res) => res.text());
const $ = cheerio.load(html);
// SSR
assert.equal($('my-element').length, 1, 'Element rendered');
assert.equal($('my-element template[shadowroot=open]').length, 1, 'shadow rendered');
// test 1: Element rendered
expect($('my-element')).toHaveLength(1);
// test 2: shadow rendered
expect($('my-element template[shadowroot=open]')).toHaveLength(1);
// Hydration
assert.ok(new RegExp('/_astro/src/components/my-element.js').test(html), 'Component URL is included');
// test 3: Component URL is included
expect(html).toEqual(expect.stringContaining('/src/components/my-element.js'));
});
test('Polyfills are added before the hydration script', async () => {
const html = await fixture.fetch('/load').then((res) => res.text());
const $ = cheerio.load(html);
expect($('script[type=module]')).toHaveLength(2);
expect($('script[type=module]').attr('src')).toBe('/_snowpack/link/packages/astro/test/fixtures/custom-elements/my-component-lib/polyfill.js');
expect($($('script[type=module]').get(1)).html()).toEqual(
expect.stringContaining('/_snowpack/link/packages/astro/test/fixtures/custom-elements/my-component-lib/hydration-polyfill.js')
);
});
test('Polyfills are added even if not hydrating', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
expect($('script[type=module]')).toHaveLength(1);
expect($('script[type=module]').attr('src')).toBe('/_snowpack/link/packages/astro/test/fixtures/custom-elements/my-component-lib/polyfill.js');
expect($($('script[type=module]').get(1)).html()).not.toEqual(
expect.stringContaining('/_snowpack/link/packages/astro/test/fixtures/custom-elements/my-component-lib/hydration-polyfill.js')
);
});
test('Custom elements not claimed by renderer are rendered as regular HTML', async () => {
const html = await fixture.fetch('/nossr').then((res) => res.text());
const $ = cheerio.load(html);
// test 1: Rendered the client-only element
expect($('client-element')).toHaveLength(1);
});
test('Can import a client-only element that is nested in JSX', async () => {
const html = await fixture.fetch('/nested').then((res) => res.text());
const $ = cheerio.load(html);
// test 1: Element rendered
expect($('client-only-element')).toHaveLength(1);
});
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.stop();
});
});
CustomElements('Polyfills are added before the hydration script', async ({ runtime }) => {
const result = await runtime.load('/load');
assert.ok(!result.error, `build error: ${result.error}`);
const html = result.contents;
const $ = doc(html);
assert.equal($('script[type=module]').length, 2);
assert.equal($('script[type=module]').attr('src'), '/_snowpack/link/packages/astro/test/fixtures/custom-elements/my-component-lib/polyfill.js');
assert.match($($('script[type=module]').get(1)).html(), new RegExp('/_snowpack/link/packages/astro/test/fixtures/custom-elements/my-component-lib/hydration-polyfill.js'));
});
CustomElements('Polyfills are added even if not hydrating', async ({ runtime }) => {
const result = await runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
const html = result.contents;
const $ = doc(html);
assert.equal($('script[type=module]').length, 1);
assert.equal($('script[type=module]').attr('src'), '/_snowpack/link/packages/astro/test/fixtures/custom-elements/my-component-lib/polyfill.js');
assert.not.match($($('script[type=module]').get(1)).html(), new RegExp('/_snowpack/link/packages/astro/test/fixtures/custom-elements/my-component-lib/hydration-polyfill.js'));
});
CustomElements('Custom elements not claimed by renderer are rendered as regular HTML', async ({ runtime }) => {
const result = await runtime.load('/nossr');
assert.ok(!result.error, `build error: ${result.error}`);
const $ = doc(result.contents);
assert.equal($('client-element').length, 1, 'Rendered the client-only element');
});
CustomElements('Can import a client-only element that is nested in JSX', async ({ runtime }) => {
const result = await runtime.load('/nested');
assert.ok(!result.error, 'No error loading');
const html = result.contents;
const $ = doc(html);
assert.equal($('client-only-element').length, 1, 'Element rendered');
});
CustomElements.run();

View file

@ -1,18 +1,23 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const Fetch = suite('Global Fetch');
describe('Global Fetch', () => {
let fixture;
let devServer;
setup(Fetch, './fixtures/fetch');
beforeAll(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/fetch/' });
devServer = await fixture.dev();
});
Fetch('Is available in non-Astro components.', async ({ runtime }) => {
const result = await runtime.load('/');
assert.ok(!result.error, `build error: ${result.error}`);
test('Is available in non-Astro components.', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerio.load(html);
expect($('#jsx').text()).toBe('function');
});
const $ = doc(result.contents);
assert.equal($('#jsx').text(), 'function');
// important: close dev server (free up port and connection)
afterAll(async () => {
await devServer.stop();
});
});
Fetch.run();

View file

@ -5,12 +5,7 @@
</style>
<body>
<h1>Icons</h1>
<img src="/_astro/src/images/twitter.png" srcset="/_astro/src/images/twitter.png 1x, /_astro/src/images/twitter@2x.png 2x, /_astro/src/images/twitter@3x.png 3x" />
<!--
In Astro (0.20.4 and below) these srcsets will cause a build fail.
Error (for Assets Test)
[build] file:///astro/packages/astro/test/fixtures/astro-assets/src/pages/index.astro: could not find "/_astro/src/pages/h-300/medium_cafe_B1iTdD0C.jpg"
-->
<img src="../images/twitter.png" srcset="../images/twitter.png 1x, ../images/twitter@2x.png 2x, ../images/twitter@3x.png 3x" />
<img srcset="https://ik.imagekit.io/demo/tr:w-300,h-300/medium_cafe_B1iTdD0C.jpg, https://ik.imagekit.io/demo/tr:w-450,h-450/medium_cafe_B1iTdD0C.jpg 600w, https://ik.imagekit.io/demo/tr:w-600,h-600/medium_cafe_B1iTdD0C.jpg 800w">
<img srcset="https://ik.imagekit.io/demo/tr:w-300,h-300/medium_cafe_B1iTdD0C.jpg, https://ik.imagekit.io/demo/tr:w-450,h-450/medium_cafe_B1iTdD0C.jpg 1.5x, https://ik.imagekit.io/demo/tr:w-600,h-600/medium_cafe_B1iTdD0C.jpg 2x">
<!--
@ -18,14 +13,6 @@
as no checking is done for this tag (a good way to circumvent the check.)
-->
<picture>
<!--
This will cause build fail
[build] file:///astro/packages/astro/test/fixtures/astro-assets/src/pages/index.astro: could not find "/demo/tr:w-300,h-300/medium_cafe_B1iTdD0C.jpg"
-->
<!-- <source srcset="/demo/tr:w-300,h-300/medium_cafe_B1iTdD0C.jpg, /demo/tr:w-450,h-450/medium_cafe_B1iTdD0C.jpg 600w, /demo/tr:w-600,h-600/medium_cafe_B1iTdD0C.jpg 800w"> -->
<!--
This will pass
-->
<source srcset="https://ik.imagekit.io/demo/tr:w-300,h-300/medium_cafe_B1iTdD0C.jpg, https://ik.imagekit.io/demo/tr:w-450,h-450/medium_cafe_B1iTdD0C.jpg 600w, https://ik.imagekit.io/demo/tr:w-600,h-600/medium_cafe_B1iTdD0C.jpg 800w">
</picture>
</body>

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,7 +0,0 @@
export default {
renderers: [
'@astrojs/renderer-preact',
'@astrojs/renderer-vue',
'@astrojs/renderer-svelte',
],
};

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,8 +0,0 @@
export default {
buildOptions: {
sitemap: false,
},
renderers: [
'@astrojs/renderer-svelte',
],
};

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,5 +0,0 @@
export default {
buildOptions: {
sitemap: false,
},
};

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,5 +0,0 @@
export default {
renderers: [
'@astrojs/renderer-preact'
]
}

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,5 +0,0 @@
export default {
renderers: [
'@astrojs/renderer-preact'
]
}

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,6 +0,0 @@
export default {
buildOptions: {
site: 'https://mysite.dev/blog/',
sitemap: false,
},
};

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,6 +0,0 @@
export default {
buildOptions: {
site: 'https://mysite.dev/blog/',
sitemap: false,
},
};

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,6 +0,0 @@
{
"workspaceRoot": "../../../../../",
"devOptions": {
"hmrPort": 5555
}
}

View file

@ -1,19 +0,0 @@
export default {
renderers: [
'@astrojs/renderer-preact'
],
markdownOptions: {
remarkPlugins: [
'remark-code-titles',
'remark-slug',
['rehype-autolink-headings', { behavior: 'prepend' }],
],
rehypePlugins: [
['rehype-toc', { headings: ["h2", "h3"] }],
['rehype-add-classes', { 'h1,h2,h3': 'title', }],
]
},
buildOptions: {
sitemap: false,
},
};

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,8 +0,0 @@
export default {
renderers: [
'@astrojs/renderer-preact'
],
buildOptions: {
sitemap: false,
},
};

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,5 +0,0 @@
export default {
buildOptions: {
pageUrlFormat: 'file'
}
};

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,6 +0,0 @@
export default {
buildOptions: {
site: 'https://mysite.dev/blog/',
sitemap: false,
},
};

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,5 +0,0 @@
export default {
buildOptions: {
site: 'https://mysite.dev',
},
};

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,6 +0,0 @@
{
"workspaceRoot": "../../../../../",
"packageOptions": {
"polyfillNode": false
}
}

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,6 +1,5 @@
export default {
devOptions: {
port: 3001
port: 5001
}
}

View file

@ -1,6 +0,0 @@
export default {
renderers: [
'@astrojs/test-custom-element-renderer'
]
}

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

View file

@ -1,17 +0,0 @@
export default {
// projectRoot: '.', // Where to resolve all URLs relative to. Useful if you have a monorepo project.
// pages: './src/pages', // Path to Astro components, pages, and data
// dist: './dist', // When running `astro build`, path to final static output
// public: './public', // A folder of static files Astro will copy to the root. Useful for favicons, images, and other files that dont need processing.
buildOptions: {
// site: 'http://example.com', // Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs.
// sitemap: true, // Generate sitemap (set to "false" to disable)
},
devOptions: {
// port: 3000, // The port to run the dev server on.
// tailwindConfig: '', // Path to tailwind.config.js if used, e.g. './tailwind.config.js'
},
renderers: [
'@astrojs/renderer-lit'
]
};

View file

@ -1,3 +0,0 @@
{
"workspaceRoot": "../../../../../"
}

Some files were not shown because too many files have changed in this diff Show more