mirror of
https://github.com/withastro/astro.git
synced 2025-03-17 23:11:29 -05:00
feat: add responsive image component (#12381)
* feat: add experimental responsive images config option * Apply suggestions from code review Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update config types * Move config into `images` * Move jsdocs * wip: responsive image component * Improve srcset logic * Improve fixture * Lock * Update styling * Fix style prop handling * Update test (there's an extra style for images now) * Safely access the src props * Remove unused export * Handle priority images * Consolidate styles * Update tests * Comment * Refactor srcset * Avoid dupes of original image * Calculate missing dimensions * Bugfixes * Add tests * Fix test * Correct order * Lint * Fix fspath * Update test * Fix test * Conditional component per flag * Fix class concatenation * Remove logger * Rename helper * Add comments * Format * Fix markdoc tests * Add test for style tag --------- Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
ffe063c544
commit
e4a9c35c6a
21 changed files with 837 additions and 84 deletions
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
import { type LocalImageProps, type RemoteImageProps, getImage } from 'astro:assets';
|
||||
import { type LocalImageProps, type RemoteImageProps, getImage, imageConfig } from 'astro:assets';
|
||||
import { AstroError, AstroErrorData } from '../dist/core/errors/index.js';
|
||||
import type { HTMLAttributes } from '../types';
|
||||
|
||||
|
@ -33,6 +33,60 @@ if (image.srcSet.values.length > 0) {
|
|||
if (import.meta.env.DEV) {
|
||||
additionalAttributes['data-image-component'] = 'true';
|
||||
}
|
||||
|
||||
const { experimentalResponsiveImages } = imageConfig;
|
||||
|
||||
const layoutClassMap = {
|
||||
fixed: 'aim-fi',
|
||||
responsive: 'aim-re',
|
||||
};
|
||||
|
||||
const cssFitValues = ['fill', 'contain', 'cover', 'scale-down'];
|
||||
const objectFit = props.fit ?? imageConfig.experimentalObjectFit ?? 'cover';
|
||||
const objectPosition = props.position ?? imageConfig.experimentalObjectPosition ?? 'center';
|
||||
|
||||
// The style prop can't be spread when using define:vars, so we need to extract it here
|
||||
// @see https://github.com/withastro/compiler/issues/1050
|
||||
const { style = '', class: className, ...attrs } = { ...additionalAttributes, ...image.attributes };
|
||||
---
|
||||
|
||||
<img src={image.src} {...additionalAttributes} {...image.attributes} />
|
||||
{
|
||||
experimentalResponsiveImages ? (
|
||||
<img
|
||||
src={image.src}
|
||||
{...attrs}
|
||||
{style}
|
||||
class={`${layoutClassMap[props.layout ?? imageConfig.experimentalLayout] ?? ''} aim ${className ?? ''}`.trim()}
|
||||
/>
|
||||
) : (
|
||||
<img src={image.src} {...additionalAttributes} {...image.attributes} class={className} />
|
||||
)
|
||||
}
|
||||
|
||||
<style
|
||||
define:vars={experimentalResponsiveImages && {
|
||||
w: image.attributes.width ?? props.width ?? image.options.width,
|
||||
h: image.attributes.height ?? props.height ?? image.options.height,
|
||||
fit: cssFitValues.includes(objectFit) && objectFit,
|
||||
pos: objectPosition,
|
||||
}}
|
||||
>
|
||||
/* Shared by all Astro images */
|
||||
.aim {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: var(--fit);
|
||||
object-position: var(--pos);
|
||||
aspect-ratio: var(--w) / var(--h);
|
||||
}
|
||||
/* Styles for responsive layout */
|
||||
.aim-re {
|
||||
max-width: calc(var(--w) * 1px);
|
||||
max-height: calc(var(--h) * 1px);
|
||||
}
|
||||
/* Styles for fixed layout */
|
||||
.aim-fi {
|
||||
width: calc(var(--w) * 1px);
|
||||
height: calc(var(--h) * 1px);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,6 +12,12 @@ import {
|
|||
} from './types.js';
|
||||
import { isESMImportedImage, isRemoteImage, resolveSrc } from './utils/imageKind.js';
|
||||
import { inferRemoteSize } from './utils/remoteProbe.js';
|
||||
import {
|
||||
DEFAULT_RESOLUTIONS,
|
||||
getSizesAttribute,
|
||||
getWidths,
|
||||
LIMITED_RESOLUTIONS,
|
||||
} from './layout.js';
|
||||
|
||||
export async function getConfiguredImageService(): Promise<ImageService> {
|
||||
if (!globalThis?.astroAsset?.imageService) {
|
||||
|
@ -32,9 +38,13 @@ export async function getConfiguredImageService(): Promise<ImageService> {
|
|||
return globalThis.astroAsset.imageService;
|
||||
}
|
||||
|
||||
type ImageConfig = AstroConfig['image'] & {
|
||||
experimentalResponsiveImages: boolean;
|
||||
};
|
||||
|
||||
export async function getImage(
|
||||
options: UnresolvedImageTransform,
|
||||
imageConfig: AstroConfig['image'],
|
||||
imageConfig: ImageConfig,
|
||||
): Promise<GetImageResult> {
|
||||
if (!options || typeof options !== 'object') {
|
||||
throw new AstroError({
|
||||
|
@ -65,6 +75,10 @@ export async function getImage(
|
|||
src: await resolveSrc(options.src),
|
||||
};
|
||||
|
||||
let originalWidth: number | undefined;
|
||||
let originalHeight: number | undefined;
|
||||
let originalFormat: string | undefined;
|
||||
|
||||
// Infer size for remote images if inferSize is true
|
||||
if (
|
||||
options.inferSize &&
|
||||
|
@ -74,6 +88,9 @@ export async function getImage(
|
|||
const result = await inferRemoteSize(resolvedOptions.src); // Directly probe the image URL
|
||||
resolvedOptions.width ??= result.width;
|
||||
resolvedOptions.height ??= result.height;
|
||||
originalWidth = result.width;
|
||||
originalHeight = result.height;
|
||||
originalFormat = result.format;
|
||||
delete resolvedOptions.inferSize; // Delete so it doesn't end up in the attributes
|
||||
}
|
||||
|
||||
|
@ -88,8 +105,49 @@ export async function getImage(
|
|||
(resolvedOptions.src.clone ?? resolvedOptions.src)
|
||||
: resolvedOptions.src;
|
||||
|
||||
if (isESMImportedImage(clonedSrc)) {
|
||||
originalWidth = clonedSrc.width;
|
||||
originalHeight = clonedSrc.height;
|
||||
originalFormat = clonedSrc.format;
|
||||
}
|
||||
|
||||
if (originalWidth && originalHeight) {
|
||||
// Calculate any missing dimensions from the aspect ratio, if available
|
||||
const aspectRatio = originalWidth / originalHeight;
|
||||
if (resolvedOptions.height && !resolvedOptions.width) {
|
||||
resolvedOptions.width = Math.round(resolvedOptions.height * aspectRatio);
|
||||
} else if (resolvedOptions.width && !resolvedOptions.height) {
|
||||
resolvedOptions.height = Math.round(resolvedOptions.width / aspectRatio);
|
||||
} else if (!resolvedOptions.width && !resolvedOptions.height) {
|
||||
resolvedOptions.width = originalWidth;
|
||||
resolvedOptions.height = originalHeight;
|
||||
}
|
||||
}
|
||||
resolvedOptions.src = clonedSrc;
|
||||
|
||||
const layout = options.layout ?? imageConfig.experimentalLayout;
|
||||
|
||||
if (imageConfig.experimentalResponsiveImages && layout) {
|
||||
resolvedOptions.widths ||= getWidths({
|
||||
width: resolvedOptions.width,
|
||||
layout,
|
||||
originalWidth,
|
||||
breakpoints: isLocalService(service) ? LIMITED_RESOLUTIONS : DEFAULT_RESOLUTIONS,
|
||||
});
|
||||
resolvedOptions.sizes ||= getSizesAttribute({ width: resolvedOptions.width, layout });
|
||||
|
||||
if (resolvedOptions.priority) {
|
||||
resolvedOptions.loading ??= 'eager';
|
||||
resolvedOptions.decoding ??= 'sync';
|
||||
resolvedOptions.fetchpriority ??= 'high';
|
||||
} else {
|
||||
resolvedOptions.loading ??= 'lazy';
|
||||
resolvedOptions.decoding ??= 'async';
|
||||
resolvedOptions.fetchpriority ??= 'auto';
|
||||
}
|
||||
delete resolvedOptions.priority;
|
||||
}
|
||||
|
||||
const validatedOptions = service.validateOptions
|
||||
? await service.validateOptions(resolvedOptions, imageConfig)
|
||||
: resolvedOptions;
|
||||
|
@ -100,13 +158,23 @@ export async function getImage(
|
|||
: [];
|
||||
|
||||
let imageURL = await service.getURL(validatedOptions, imageConfig);
|
||||
|
||||
const matchesOriginal = (transform: ImageTransform) =>
|
||||
transform.width === originalWidth &&
|
||||
transform.height === originalHeight &&
|
||||
transform.format === originalFormat;
|
||||
|
||||
let srcSets: SrcSetValue[] = await Promise.all(
|
||||
srcSetTransforms.map(async (srcSet) => ({
|
||||
transform: srcSet.transform,
|
||||
url: await service.getURL(srcSet.transform, imageConfig),
|
||||
descriptor: srcSet.descriptor,
|
||||
attributes: srcSet.attributes,
|
||||
})),
|
||||
srcSetTransforms.map(async (srcSet) => {
|
||||
return {
|
||||
transform: srcSet.transform,
|
||||
url: matchesOriginal(srcSet.transform)
|
||||
? imageURL
|
||||
: await service.getURL(srcSet.transform, imageConfig),
|
||||
descriptor: srcSet.descriptor,
|
||||
attributes: srcSet.attributes,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
|
@ -120,12 +188,16 @@ export async function getImage(
|
|||
propsToHash,
|
||||
originalFilePath,
|
||||
);
|
||||
srcSets = srcSetTransforms.map((srcSet) => ({
|
||||
transform: srcSet.transform,
|
||||
url: globalThis.astroAsset.addStaticImage!(srcSet.transform, propsToHash, originalFilePath),
|
||||
descriptor: srcSet.descriptor,
|
||||
attributes: srcSet.attributes,
|
||||
}));
|
||||
srcSets = srcSetTransforms.map((srcSet) => {
|
||||
return {
|
||||
transform: srcSet.transform,
|
||||
url: matchesOriginal(srcSet.transform)
|
||||
? imageURL
|
||||
: globalThis.astroAsset.addStaticImage!(srcSet.transform, propsToHash, originalFilePath),
|
||||
descriptor: srcSet.descriptor,
|
||||
attributes: srcSet.attributes,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
118
packages/astro/src/assets/layout.ts
Normal file
118
packages/astro/src/assets/layout.ts
Normal file
|
@ -0,0 +1,118 @@
|
|||
import type { ImageLayout } from '../types/public/index.js';
|
||||
|
||||
// Common screen widths. These will be filtered according to the image size and layout
|
||||
export const DEFAULT_RESOLUTIONS = [
|
||||
640, // older and lower-end phones
|
||||
750, // iPhone 6-8
|
||||
828, // iPhone XR/11
|
||||
960, // older horizontal phones
|
||||
1080, // iPhone 6-8 Plus
|
||||
1280, // 720p
|
||||
1668, // Various iPads
|
||||
1920, // 1080p
|
||||
2048, // QXGA
|
||||
2560, // WQXGA
|
||||
3200, // QHD+
|
||||
3840, // 4K
|
||||
4480, // 4.5K
|
||||
5120, // 5K
|
||||
6016, // 6K
|
||||
];
|
||||
|
||||
// A more limited set of screen widths, for statically generated images
|
||||
export const LIMITED_RESOLUTIONS = [
|
||||
640, // older and lower-end phones
|
||||
750, // iPhone 6-8
|
||||
828, // iPhone XR/11
|
||||
1080, // iPhone 6-8 Plus
|
||||
1280, // 720p
|
||||
1668, // Various iPads
|
||||
2048, // QXGA
|
||||
2560, // WQXGA
|
||||
];
|
||||
|
||||
/**
|
||||
* Gets the breakpoints for an image, based on the layout and width
|
||||
*
|
||||
* The rules are as follows:
|
||||
*
|
||||
* - For full-width layout we return all breakpoints smaller than the original image width
|
||||
* - For fixed layout we return 1x and 2x the requested width, unless the original image is smaller than that.
|
||||
* - For responsive layout we return all breakpoints smaller than 2x the requested width, unless the original image is smaller than that.
|
||||
*/
|
||||
export const getWidths = ({
|
||||
width,
|
||||
layout,
|
||||
breakpoints = DEFAULT_RESOLUTIONS,
|
||||
originalWidth,
|
||||
}: {
|
||||
width?: number;
|
||||
layout: ImageLayout;
|
||||
breakpoints?: Array<number>;
|
||||
originalWidth?: number;
|
||||
}): Array<number> => {
|
||||
const smallerThanOriginal = (w: number) => !originalWidth || w <= originalWidth;
|
||||
|
||||
// For full-width layout we return all breakpoints smaller than the original image width
|
||||
if (layout === 'full-width') {
|
||||
return breakpoints.filter(smallerThanOriginal);
|
||||
}
|
||||
// For other layouts we need a width to generate breakpoints. If no width is provided, we return an empty array
|
||||
if (!width) {
|
||||
return [];
|
||||
}
|
||||
const doubleWidth = width * 2;
|
||||
// For fixed layout we want to return the 1x and 2x widths. We only do this if the original image is large enough to do this though.
|
||||
const maxSize = originalWidth ? Math.min(doubleWidth, originalWidth) : doubleWidth;
|
||||
if (layout === 'fixed') {
|
||||
return originalWidth && width > originalWidth ? [originalWidth] : [width, maxSize];
|
||||
}
|
||||
|
||||
// For responsive layout we want to return all breakpoints smaller than 2x requested width.
|
||||
if (layout === 'responsive') {
|
||||
return (
|
||||
[
|
||||
// Always include the image at 1x and 2x the specified width
|
||||
width,
|
||||
doubleWidth,
|
||||
...breakpoints,
|
||||
]
|
||||
// Filter out any resolutions that are larger than the double-resolution image or source image
|
||||
.filter((w) => w <= maxSize)
|
||||
// Sort the resolutions in ascending order
|
||||
.sort((a, b) => a - b)
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the `sizes` attribute for an image, based on the layout and width
|
||||
*/
|
||||
export const getSizesAttribute = ({
|
||||
width,
|
||||
layout,
|
||||
}: { width?: number; layout?: ImageLayout }): string | undefined => {
|
||||
if (!width || !layout) {
|
||||
return undefined;
|
||||
}
|
||||
switch (layout) {
|
||||
// If screen is wider than the max size then image width is the max size,
|
||||
// otherwise it's the width of the screen
|
||||
case `responsive`:
|
||||
return `(min-width: ${width}px) ${width}px, 100vw`;
|
||||
|
||||
// Image is always the same width, whatever the size of the screen
|
||||
case `fixed`:
|
||||
return `${width}px`;
|
||||
|
||||
// Image is always the width of the screen
|
||||
case `full-width`:
|
||||
return `100vw`;
|
||||
|
||||
case 'none':
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
|
@ -118,6 +118,8 @@ export type BaseServiceTransform = {
|
|||
quality?: string | null;
|
||||
};
|
||||
|
||||
const sortNumeric = (a: number, b: number) => a - b;
|
||||
|
||||
/**
|
||||
* Basic local service using the included `_image` endpoint.
|
||||
* This service intentionally does not implement `transform`.
|
||||
|
@ -224,9 +226,19 @@ export const baseService: Omit<LocalImageService, 'transform'> = {
|
|||
},
|
||||
getHTMLAttributes(options) {
|
||||
const { targetWidth, targetHeight } = getTargetDimensions(options);
|
||||
const { src, width, height, format, quality, densities, widths, formats, ...attributes } =
|
||||
options;
|
||||
|
||||
const {
|
||||
src,
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
quality,
|
||||
densities,
|
||||
widths,
|
||||
formats,
|
||||
layout,
|
||||
priority,
|
||||
...attributes
|
||||
} = options;
|
||||
return {
|
||||
...attributes,
|
||||
width: targetWidth,
|
||||
|
@ -235,12 +247,14 @@ export const baseService: Omit<LocalImageService, 'transform'> = {
|
|||
decoding: attributes.decoding ?? 'async',
|
||||
};
|
||||
},
|
||||
getSrcSet(options) {
|
||||
const srcSet: UnresolvedSrcSetValue[] = [];
|
||||
const { targetWidth } = getTargetDimensions(options);
|
||||
getSrcSet(options): Array<UnresolvedSrcSetValue> {
|
||||
const { targetWidth, targetHeight } = getTargetDimensions(options);
|
||||
const aspectRatio = targetWidth / targetHeight;
|
||||
const { widths, densities } = options;
|
||||
const targetFormat = options.format ?? DEFAULT_OUTPUT_FORMAT;
|
||||
|
||||
let transformedWidths = (widths ?? []).sort(sortNumeric);
|
||||
|
||||
// For remote images, we don't know the original image's dimensions, so we cannot know the maximum width
|
||||
// It is ultimately the user's responsibility to make sure they don't request images larger than the original
|
||||
let imageWidth = options.width;
|
||||
|
@ -250,8 +264,18 @@ export const baseService: Omit<LocalImageService, 'transform'> = {
|
|||
if (isESMImportedImage(options.src)) {
|
||||
imageWidth = options.src.width;
|
||||
maxWidth = imageWidth;
|
||||
|
||||
// We've already sorted the widths, so we'll remove any that are larger than the original image's width
|
||||
if (transformedWidths.length > 0 && transformedWidths.at(-1)! > maxWidth) {
|
||||
transformedWidths = transformedWidths.filter((width) => width <= maxWidth);
|
||||
// If we've had to remove some widths, we'll add the maximum width back in
|
||||
transformedWidths.push(maxWidth);
|
||||
}
|
||||
}
|
||||
|
||||
// Dedupe the widths
|
||||
transformedWidths = Array.from(new Set(transformedWidths));
|
||||
|
||||
// Since `widths` and `densities` ultimately control the width and height of the image,
|
||||
// we don't want the dimensions the user specified, we'll create those ourselves.
|
||||
const {
|
||||
|
@ -261,7 +285,10 @@ export const baseService: Omit<LocalImageService, 'transform'> = {
|
|||
} = options;
|
||||
|
||||
// Collect widths to generate from specified densities or widths
|
||||
const allWidths: { maxTargetWidth: number; descriptor: `${number}x` | `${number}w` }[] = [];
|
||||
let allWidths: Array<{
|
||||
width: number;
|
||||
descriptor: `${number}x` | `${number}w`;
|
||||
}> = [];
|
||||
if (densities) {
|
||||
// Densities can either be specified as numbers, or descriptors (ex: '1x'), we'll convert them all to numbers
|
||||
const densityValues = densities.map((density) => {
|
||||
|
@ -274,51 +301,31 @@ export const baseService: Omit<LocalImageService, 'transform'> = {
|
|||
|
||||
// Calculate the widths for each density, rounding to avoid floats.
|
||||
const densityWidths = densityValues
|
||||
.sort()
|
||||
.sort(sortNumeric)
|
||||
.map((density) => Math.round(targetWidth * density));
|
||||
|
||||
allWidths.push(
|
||||
...densityWidths.map((width, index) => ({
|
||||
maxTargetWidth: Math.min(width, maxWidth),
|
||||
descriptor: `${densityValues[index]}x` as const,
|
||||
})),
|
||||
);
|
||||
} else if (widths) {
|
||||
allWidths.push(
|
||||
...widths.map((width) => ({
|
||||
maxTargetWidth: Math.min(width, maxWidth),
|
||||
descriptor: `${width}w` as const,
|
||||
})),
|
||||
);
|
||||
allWidths = densityWidths.map((width, index) => ({
|
||||
width,
|
||||
descriptor: `${densityValues[index]}x`,
|
||||
}));
|
||||
} else if (transformedWidths.length > 0) {
|
||||
allWidths = transformedWidths.map((width) => ({
|
||||
width,
|
||||
descriptor: `${width}w`,
|
||||
}));
|
||||
}
|
||||
|
||||
// Caution: The logic below is a bit tricky, as we need to make sure we don't generate the same image multiple times
|
||||
// When making changes, make sure to test with different combinations of local/remote images widths, densities, and dimensions etc.
|
||||
for (const { maxTargetWidth, descriptor } of allWidths) {
|
||||
const srcSetTransform: ImageTransform = { ...transformWithoutDimensions };
|
||||
|
||||
// Only set the width if it's different from the original image's width, to avoid generating the same image multiple times
|
||||
if (maxTargetWidth !== imageWidth) {
|
||||
srcSetTransform.width = maxTargetWidth;
|
||||
} else {
|
||||
// If the width is the same as the original image's width, and we have both dimensions, it probably means
|
||||
// it's a remote image, so we'll use the user's specified dimensions to avoid recreating the original image unnecessarily
|
||||
if (options.width && options.height) {
|
||||
srcSetTransform.width = options.width;
|
||||
srcSetTransform.height = options.height;
|
||||
}
|
||||
}
|
||||
|
||||
srcSet.push({
|
||||
transform: srcSetTransform,
|
||||
return allWidths.map(({ width, descriptor }) => {
|
||||
const height = Math.round(width / aspectRatio);
|
||||
const transform = { ...transformWithoutDimensions, width, height };
|
||||
return {
|
||||
transform,
|
||||
descriptor,
|
||||
attributes: {
|
||||
type: `image/${targetFormat}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return srcSet;
|
||||
};
|
||||
});
|
||||
},
|
||||
getURL(options, imageConfig) {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
|
|
@ -162,6 +162,18 @@ type ImageSharedProps<T> = T & {
|
|||
position?: string;
|
||||
} & (
|
||||
| {
|
||||
/**
|
||||
* The layout type for responsive images. Overrides any default set in the Astro config.
|
||||
* Requires the `experimental.responsiveImages` flag to be enabled.
|
||||
*
|
||||
* - `responsive` - The image will scale to fit the container, maintaining its aspect ratio, but will not exceed the specified dimensions.
|
||||
* - `fixed` - The image will maintain its original dimensions.
|
||||
* - `full-width` - The image will scale to fit the container, maintaining its aspect ratio.
|
||||
*/
|
||||
layout?: ImageLayout;
|
||||
fit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down' | (string & {});
|
||||
position?: string;
|
||||
priority?: boolean;
|
||||
/**
|
||||
* A list of widths to generate images for. The value of this property will be used to assign the `srcset` property on the final `img` element.
|
||||
*
|
||||
|
@ -178,6 +190,9 @@ type ImageSharedProps<T> = T & {
|
|||
*/
|
||||
densities?: (number | `${number}x`)[];
|
||||
widths?: never;
|
||||
layout?: never;
|
||||
fit?: never;
|
||||
position?: never;
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('Content Collections - render()', () => {
|
|||
assert.equal($('ul li').length, 3);
|
||||
|
||||
// Includes styles
|
||||
assert.equal($('link[rel=stylesheet]').length, 1);
|
||||
assert.equal($('link[rel=stylesheet]').length, 2);
|
||||
});
|
||||
|
||||
it('Excludes CSS for non-rendered entries', async () => {
|
||||
|
@ -34,7 +34,7 @@ describe('Content Collections - render()', () => {
|
|||
const $ = cheerio.load(html);
|
||||
|
||||
// Excludes styles
|
||||
assert.equal($('link[rel=stylesheet]').length, 0);
|
||||
assert.equal($('link[rel=stylesheet]').length, 1);
|
||||
});
|
||||
|
||||
it('De-duplicates CSS used both in layout and directly in target page', async () => {
|
||||
|
@ -110,7 +110,7 @@ describe('Content Collections - render()', () => {
|
|||
assert.equal($('ul li').length, 3);
|
||||
|
||||
// Includes styles
|
||||
assert.equal($('link[rel=stylesheet]').length, 1);
|
||||
assert.equal($('link[rel=stylesheet]').length, 2);
|
||||
});
|
||||
|
||||
it('Exclude CSS for non-rendered entries', async () => {
|
||||
|
@ -121,7 +121,7 @@ describe('Content Collections - render()', () => {
|
|||
const $ = cheerio.load(html);
|
||||
|
||||
// Includes styles
|
||||
assert.equal($('link[rel=stylesheet]').length, 0);
|
||||
assert.equal($('link[rel=stylesheet]').length, 1);
|
||||
});
|
||||
|
||||
it('De-duplicates CSS used both in layout and directly in target page', async () => {
|
||||
|
@ -202,7 +202,7 @@ describe('Content Collections - render()', () => {
|
|||
assert.equal($('ul li').length, 3);
|
||||
|
||||
// Includes styles
|
||||
assert.equal($('head > style').length, 1);
|
||||
assert.equal($('head > style').length, 2);
|
||||
assert.ok($('head > style').text().includes("font-family: 'Comic Sans MS'"));
|
||||
});
|
||||
|
||||
|
|
349
packages/astro/test/core-image-layout.test.js
Normal file
349
packages/astro/test/core-image-layout.test.js
Normal file
|
@ -0,0 +1,349 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { Writable } from 'node:stream';
|
||||
import { after, before, describe, it } from 'node:test';
|
||||
import * as cheerio from 'cheerio';
|
||||
import parseSrcset from 'parse-srcset';
|
||||
import { Logger } from '../dist/core/logger/core.js';
|
||||
import { testImageService } from './test-image-service.js';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
import { testRemoteImageService } from './test-remote-image-service.js';
|
||||
|
||||
describe('astro:image:layout', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
describe('local image service', () => {
|
||||
/** @type {import('./test-utils').DevServer} */
|
||||
let devServer;
|
||||
/** @type {Array<{ type: any, level: 'error', message: string; }>} */
|
||||
let logs = [];
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/core-image-layout/',
|
||||
image: {
|
||||
service: testImageService({ foo: 'bar' }),
|
||||
domains: ['avatars.githubusercontent.com'],
|
||||
},
|
||||
});
|
||||
|
||||
devServer = await fixture.startDevServer({});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
describe('basics', () => {
|
||||
let $;
|
||||
before(async () => {
|
||||
let res = await fixture.fetch('/');
|
||||
let html = await res.text();
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
it('Adds the <img> tag', () => {
|
||||
let $img = $('#local img');
|
||||
assert.equal($img.length, 1);
|
||||
assert.equal($img.attr('src').startsWith('/_image'), true);
|
||||
});
|
||||
|
||||
it('includes lazy loading attributes', () => {
|
||||
let $img = $('#local img');
|
||||
assert.equal($img.attr('loading'), 'lazy');
|
||||
assert.equal($img.attr('decoding'), 'async');
|
||||
assert.equal($img.attr('fetchpriority'), 'auto');
|
||||
});
|
||||
|
||||
it('includes priority loading attributes', () => {
|
||||
let $img = $('#local-priority img');
|
||||
assert.equal($img.attr('loading'), 'eager');
|
||||
assert.equal($img.attr('decoding'), 'sync');
|
||||
assert.equal($img.attr('fetchpriority'), 'high');
|
||||
});
|
||||
|
||||
it('has width and height - no dimensions set', () => {
|
||||
let $img = $('#local img');
|
||||
assert.equal($img.attr('width'), '2316');
|
||||
assert.equal($img.attr('height'), '1544');
|
||||
});
|
||||
|
||||
it('has proper width and height - only width', () => {
|
||||
let $img = $('#local-width img');
|
||||
assert.equal($img.attr('width'), '350');
|
||||
assert.equal($img.attr('height'), '233');
|
||||
});
|
||||
|
||||
it('has proper width and height - only height', () => {
|
||||
let $img = $('#local-height img');
|
||||
assert.equal($img.attr('width'), '300');
|
||||
assert.equal($img.attr('height'), '200');
|
||||
});
|
||||
|
||||
it('has proper width and height - has both width and height', () => {
|
||||
let $img = $('#local-both img');
|
||||
assert.equal($img.attr('width'), '300');
|
||||
assert.equal($img.attr('height'), '400');
|
||||
});
|
||||
|
||||
it('sets the style', () => {
|
||||
let $img = $('#local-both img');
|
||||
assert.match($img.attr('style'), /--w: 300/);
|
||||
assert.match($img.attr('style'), /--h: 400/);
|
||||
const classes = $img.attr('class').split(' ');
|
||||
assert.ok(classes.includes('aim'));
|
||||
assert.ok(classes.includes('aim-re'));
|
||||
});
|
||||
|
||||
it('sets the style when no dimensions set', () => {
|
||||
let $img = $('#local img');
|
||||
assert.match($img.attr('style'), /--w: 2316/);
|
||||
assert.match($img.attr('style'), /--h: 1544/);
|
||||
const classes = $img.attr('class').split(' ');
|
||||
assert.ok(classes.includes('aim'));
|
||||
assert.ok(classes.includes('aim-re'));
|
||||
});
|
||||
|
||||
it('sets style for fixed image', () => {
|
||||
let $img = $('#local-fixed img');
|
||||
assert.match($img.attr('style'), /--w: 800/);
|
||||
assert.match($img.attr('style'), /--h: 600/);
|
||||
const classes = $img.attr('class').split(' ');
|
||||
assert.ok(classes.includes('aim'));
|
||||
assert.ok(classes.includes('aim-fi'));
|
||||
});
|
||||
|
||||
it('sets style for full-width image', () => {
|
||||
let $img = $('#local-full-width img');
|
||||
const classes = $img.attr('class').split(' ');
|
||||
assert.deepEqual(classes, ['aim']);
|
||||
});
|
||||
|
||||
it('passes in a parent class', () => {
|
||||
let $img = $('#local-class img');
|
||||
assert.match($img.attr('class'), /green/);
|
||||
});
|
||||
|
||||
it('passes in a parent style', () => {
|
||||
let $img = $('#local-style img');
|
||||
assert.match($img.attr('style'), /^border: 2px red solid/);
|
||||
});
|
||||
|
||||
it('injects a style tag', () => {
|
||||
const style = $('style').text();
|
||||
assert.match(style, /\.aim\[/);
|
||||
assert.match(style, /\.aim-re\[/);
|
||||
assert.match(style, /\.aim-fi\[/);
|
||||
})
|
||||
});
|
||||
|
||||
describe('srcsets', () => {
|
||||
let $;
|
||||
before(async () => {
|
||||
let res = await fixture.fetch('/');
|
||||
let html = await res.text();
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
it('has srcset', () => {
|
||||
let $img = $('#local img');
|
||||
assert.ok($img.attr('srcset'));
|
||||
const srcset = parseSrcset($img.attr('srcset'));
|
||||
assert.equal(srcset.length, 8);
|
||||
assert.equal(srcset[0].url.startsWith('/_image'), true);
|
||||
const widths = srcset.map((x) => x.w);
|
||||
assert.deepEqual(widths, [640, 750, 828, 1080, 1280, 1668, 2048, 2316]);
|
||||
});
|
||||
|
||||
it('constrained - has max of 2x requested size', () => {
|
||||
let $img = $('#local-constrained img');
|
||||
const widths = parseSrcset($img.attr('srcset')).map((x) => x.w);
|
||||
assert.equal(widths.at(-1), 1600);
|
||||
});
|
||||
|
||||
it('constrained - just has 1x and 2x when smaller than min breakpoint', () => {
|
||||
let $img = $('#local-both img');
|
||||
const widths = parseSrcset($img.attr('srcset')).map((x) => x.w);
|
||||
assert.deepEqual(widths, [300, 600]);
|
||||
});
|
||||
|
||||
it('fixed - has just 1x and 2x', () => {
|
||||
let $img = $('#local-fixed img');
|
||||
const widths = parseSrcset($img.attr('srcset')).map((x) => x.w);
|
||||
assert.deepEqual(widths, [800, 1600]);
|
||||
});
|
||||
|
||||
it('full-width: has all breakpoints below image size, ignoring dimensions', () => {
|
||||
let $img = $('#local-full-width img');
|
||||
const widths = parseSrcset($img.attr('srcset')).map((x) => x.w);
|
||||
assert.deepEqual(widths, [640, 750, 828, 1080, 1280, 1668, 2048]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remote images', () => {
|
||||
describe('srcset', () => {
|
||||
let $;
|
||||
before(async () => {
|
||||
let res = await fixture.fetch('/remote');
|
||||
let html = await res.text();
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
it('has srcset', () => {
|
||||
let $img = $('#constrained img');
|
||||
assert.ok($img.attr('srcset'));
|
||||
const srcset = parseSrcset($img.attr('srcset'));
|
||||
const widths = srcset.map((x) => x.w);
|
||||
assert.deepEqual(widths, [640, 750, 800, 828, 1080, 1280, 1600]);
|
||||
});
|
||||
|
||||
it('constrained - has max of 2x requested size', () => {
|
||||
let $img = $('#constrained img');
|
||||
const widths = parseSrcset($img.attr('srcset')).map((x) => x.w);
|
||||
assert.equal(widths.at(-1), 1600);
|
||||
});
|
||||
|
||||
it('constrained - just has 1x and 2x when smaller than min breakpoint', () => {
|
||||
let $img = $('#small img');
|
||||
const widths = parseSrcset($img.attr('srcset')).map((x) => x.w);
|
||||
assert.deepEqual(widths, [300, 600]);
|
||||
});
|
||||
|
||||
it('fixed - has just 1x and 2x', () => {
|
||||
let $img = $('#fixed img');
|
||||
const widths = parseSrcset($img.attr('srcset')).map((x) => x.w);
|
||||
assert.deepEqual(widths, [800, 1600]);
|
||||
});
|
||||
|
||||
it('full-width: has all breakpoints', () => {
|
||||
let $img = $('#full-width img');
|
||||
const widths = parseSrcset($img.attr('srcset')).map((x) => x.w);
|
||||
assert.deepEqual(widths, [640, 750, 828, 1080, 1280, 1668, 2048, 2560]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('remote image service', () => {
|
||||
/** @type {import('./test-utils').DevServer} */
|
||||
let devServer;
|
||||
/** @type {Array<{ type: any, level: 'error', message: string; }>} */
|
||||
let logs = [];
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/core-image-layout/',
|
||||
image: {
|
||||
service: testRemoteImageService({ foo: 'bar' }),
|
||||
domains: ['images.unsplash.com'],
|
||||
},
|
||||
});
|
||||
|
||||
devServer = await fixture.startDevServer({
|
||||
logger: new Logger({
|
||||
level: 'error',
|
||||
dest: new Writable({
|
||||
objectMode: true,
|
||||
write(event, _, callback) {
|
||||
logs.push(event);
|
||||
callback();
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
describe('srcsets', () => {
|
||||
let $;
|
||||
before(async () => {
|
||||
let res = await fixture.fetch('/');
|
||||
let html = await res.text();
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
|
||||
it('has full srcset', () => {
|
||||
let $img = $('#local img');
|
||||
assert.ok($img.attr('srcset'));
|
||||
const srcset = parseSrcset($img.attr('srcset'));
|
||||
assert.equal(srcset.length, 10);
|
||||
assert.equal(srcset[0].url.startsWith('/_image'), true);
|
||||
const widths = srcset.map((x) => x.w);
|
||||
assert.deepEqual(widths, [640, 750, 828, 960, 1080, 1280, 1668, 1920, 2048, 2316]);
|
||||
});
|
||||
|
||||
it('constrained - has max of 2x requested size', () => {
|
||||
let $img = $('#local-constrained img');
|
||||
const widths = parseSrcset($img.attr('srcset')).map((x) => x.w);
|
||||
assert.equal(widths.at(-1), 1600);
|
||||
});
|
||||
|
||||
it('constrained - just has 1x and 2x when smaller than min breakpoint', () => {
|
||||
let $img = $('#local-both img');
|
||||
const widths = parseSrcset($img.attr('srcset')).map((x) => x.w);
|
||||
assert.deepEqual(widths, [300, 600]);
|
||||
});
|
||||
|
||||
it('fixed - has just 1x and 2x', () => {
|
||||
let $img = $('#local-fixed img');
|
||||
const widths = parseSrcset($img.attr('srcset')).map((x) => x.w);
|
||||
assert.deepEqual(widths, [800, 1600]);
|
||||
});
|
||||
|
||||
it('full-width: has all breakpoints below image size, ignoring dimensions', () => {
|
||||
let $img = $('#local-full-width img');
|
||||
const widths = parseSrcset($img.attr('srcset')).map((x) => x.w);
|
||||
assert.deepEqual(widths, [640, 750, 828, 960, 1080, 1280, 1668, 1920, 2048]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remote', () => {
|
||||
describe('srcset', () => {
|
||||
let $;
|
||||
before(async () => {
|
||||
let res = await fixture.fetch('/remote');
|
||||
let html = await res.text();
|
||||
$ = cheerio.load(html);
|
||||
});
|
||||
it('has srcset', () => {
|
||||
let $img = $('#constrained img');
|
||||
assert.ok($img.attr('srcset'));
|
||||
const srcset = parseSrcset($img.attr('srcset'));
|
||||
assert.equal(srcset.length, 8);
|
||||
assert.equal(srcset[0].url.startsWith('/_image'), true);
|
||||
const widths = srcset.map((x) => x.w);
|
||||
assert.deepEqual(widths, [640, 750, 800, 828, 960, 1080, 1280, 1600]);
|
||||
});
|
||||
|
||||
it('constrained - has max of 2x requested size', () => {
|
||||
let $img = $('#constrained img');
|
||||
const widths = parseSrcset($img.attr('srcset')).map((x) => x.w);
|
||||
assert.equal(widths.at(-1), 1600);
|
||||
});
|
||||
|
||||
it('constrained - just has 1x and 2x when smaller than min breakpoint', () => {
|
||||
let $img = $('#small img');
|
||||
const widths = parseSrcset($img.attr('srcset')).map((x) => x.w);
|
||||
assert.deepEqual(widths, [300, 600]);
|
||||
});
|
||||
|
||||
it('fixed - has just 1x and 2x', () => {
|
||||
let $img = $('#fixed img');
|
||||
const widths = parseSrcset($img.attr('srcset')).map((x) => x.w);
|
||||
assert.deepEqual(widths, [800, 1600]);
|
||||
});
|
||||
|
||||
it('full-width: has all breakpoints', () => {
|
||||
let $img = $('#full-width img');
|
||||
const widths = parseSrcset($img.attr('srcset')).map((x) => x.w);
|
||||
assert.deepEqual(
|
||||
widths,
|
||||
[640, 750, 828, 960, 1080, 1280, 1668, 1920, 2048, 2560, 3200, 3840, 4480, 5120, 6016],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
12
packages/astro/test/fixtures/core-image-layout/astro.config.mjs
vendored
Normal file
12
packages/astro/test/fixtures/core-image-layout/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
image: {
|
||||
experimentalLayout: 'responsive',
|
||||
},
|
||||
|
||||
experimental: {
|
||||
responsiveImages: true
|
||||
},
|
||||
});
|
8
packages/astro/test/fixtures/core-image-layout/package.json
vendored
Normal file
8
packages/astro/test/fixtures/core-image-layout/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/core-image-layout",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
BIN
packages/astro/test/fixtures/core-image-layout/src/assets/penguin.jpg
vendored
Normal file
BIN
packages/astro/test/fixtures/core-image-layout/src/assets/penguin.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 253 KiB |
BIN
packages/astro/test/fixtures/core-image-layout/src/assets/walrus.jpg
vendored
Normal file
BIN
packages/astro/test/fixtures/core-image-layout/src/assets/walrus.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
50
packages/astro/test/fixtures/core-image-layout/src/pages/index.astro
vendored
Normal file
50
packages/astro/test/fixtures/core-image-layout/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
import { Image, Picture } from "astro:assets";
|
||||
import penguin from "../assets/penguin.jpg";
|
||||
---
|
||||
|
||||
|
||||
<div id="local">
|
||||
<Image src={penguin} alt="a penguin" />
|
||||
</div>
|
||||
<div id="local-priority">
|
||||
<Image src={penguin} alt="a penguin" priority />
|
||||
</div>
|
||||
|
||||
<div id="local-width">
|
||||
<Image src={penguin} alt="a penguin" width={350} />
|
||||
</div>
|
||||
|
||||
<div id="local-height">
|
||||
<Image src={penguin} alt="a penguin" height={200}/>
|
||||
</div>
|
||||
|
||||
<div id="local-both">
|
||||
<Image src={penguin} alt="a penguin" width={300} height={400}/>
|
||||
</div>
|
||||
|
||||
<div id="local-class">
|
||||
<Image src={penguin} alt="a penguin" width={300} height={400} class="green"/>
|
||||
</div>
|
||||
|
||||
<div id="local-style">
|
||||
<Image src={penguin} alt="a penguin" width={300} height={400} style="border: 2px red solid"/>
|
||||
</div>
|
||||
|
||||
<div id="local-constrained">
|
||||
<Image src={penguin} alt="a penguin" width={800} height={600} />
|
||||
</div>
|
||||
|
||||
<div id="local-fixed">
|
||||
<Image src={penguin} alt="a penguin" width={800} height={600} layout="fixed"/>
|
||||
</div>
|
||||
|
||||
<div id="local-full-width">
|
||||
<Image src={penguin} alt="a penguin" width={800} height={600} layout="full-width"/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.green {
|
||||
border: 2px green solid;
|
||||
}
|
||||
</style>
|
25
packages/astro/test/fixtures/core-image-layout/src/pages/remote.astro
vendored
Normal file
25
packages/astro/test/fixtures/core-image-layout/src/pages/remote.astro
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
import { Image, Picture } from "astro:assets";
|
||||
|
||||
const penguin = "https://images.unsplash.com/photo-1670392957807-b0504fc5160a?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
<div id="small">
|
||||
<Image src={penguin} alt="a penguin" width={300} height={400}/>
|
||||
</div>
|
||||
|
||||
<div id="constrained">
|
||||
<Image src={penguin} alt="a penguin" width={800} height={600} />
|
||||
</div>
|
||||
|
||||
<div id="fixed">
|
||||
<Image src={penguin} alt="a penguin" width={800} height={600} layout="fixed"/>
|
||||
</div>
|
||||
|
||||
<div id="full-width">
|
||||
<Image src={penguin} alt="a penguin" width={800} height={600} layout="full-width"/>
|
||||
</div>
|
||||
|
11
packages/astro/test/fixtures/core-image-layout/tsconfig.json
vendored
Normal file
11
packages/astro/test/fixtures/core-image-layout/tsconfig.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/assets/*": ["src/assets/*"]
|
||||
},
|
||||
},
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
26
packages/astro/test/test-remote-image-service.js
Normal file
26
packages/astro/test/test-remote-image-service.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import { baseService } from '../dist/assets/services/service.js';
|
||||
|
||||
/**
|
||||
* stub remote image service
|
||||
* @param {{ foo?: string }} [config]
|
||||
*/
|
||||
export function testRemoteImageService(config = {}) {
|
||||
return {
|
||||
entrypoint: fileURLToPath(import.meta.url),
|
||||
config,
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {import("../dist/types/public/index.js").LocalImageService} */
|
||||
export default {
|
||||
...baseService,
|
||||
propertiesToHash: [...baseService.propertiesToHash, 'data-custom'],
|
||||
getHTMLAttributes(options, serviceConfig) {
|
||||
options['data-service'] = 'my-custom-service';
|
||||
if (serviceConfig.service.config.foo) {
|
||||
options['data-service-config'] = serviceConfig.service.config.foo;
|
||||
}
|
||||
return baseService.getHTMLAttributes(options);
|
||||
},
|
||||
};
|
|
@ -101,7 +101,7 @@ describe('Content Collections - render()', () => {
|
|||
assert.equal($('ul li').length, 3);
|
||||
|
||||
// Rendered the styles
|
||||
assert.equal($('style').length, 1);
|
||||
assert.equal($('style').length, 2);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -158,7 +158,7 @@ describe('Content Collections - render()', () => {
|
|||
assert.equal($('ul li').length, 3);
|
||||
|
||||
// Rendered the styles
|
||||
assert.equal($('style').length, 1);
|
||||
assert.equal($('style').length, 2);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -225,7 +225,7 @@ describe('Content Collections - render()', () => {
|
|||
assert.equal($('ul li').length, 3);
|
||||
|
||||
// Rendered the styles
|
||||
assert.equal($('style').length, 1);
|
||||
assert.equal($('style').length, 2);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -291,7 +291,7 @@ describe('Content Collections - render()', () => {
|
|||
assert.equal($('ul li').length, 3);
|
||||
|
||||
// Rendered the styles
|
||||
assert.equal($('style').length, 1);
|
||||
assert.equal($('style').length, 2);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -38,7 +38,7 @@ describe('Markdoc - Image assets', () => {
|
|||
const { document } = parseHTML(html);
|
||||
assert.match(
|
||||
document.querySelector('#relative > img')?.src,
|
||||
/\/_image\?href=.*%2Fsrc%2Fassets%2Frelative%2Foar.jpg%3ForigWidth%3D420%26origHeight%3D630%26origFormat%3Djpg&f=webp/,
|
||||
/\/_image\?href=.*%2Fsrc%2Fassets%2Frelative%2Foar.jpg%3ForigWidth%3D420%26origHeight%3D630%26origFormat%3Djpg&w=420&h=630&f=webp/,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -48,7 +48,7 @@ describe('Markdoc - Image assets', () => {
|
|||
const { document } = parseHTML(html);
|
||||
assert.match(
|
||||
document.querySelector('#alias > img')?.src,
|
||||
/\/_image\?href=.*%2Fsrc%2Fassets%2Falias%2Fcityscape.jpg%3ForigWidth%3D420%26origHeight%3D280%26origFormat%3Djpg&f=webp/,
|
||||
/\/_image\?href=.*%2Fsrc%2Fassets%2Falias%2Fcityscape.jpg%3ForigWidth%3D420%26origHeight%3D280%26origFormat%3Djpg&w=420&h=280&f=webp/,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -45,12 +45,12 @@ describe('Markdoc - propagated assets', () => {
|
|||
let styleContents;
|
||||
if (mode === 'dev') {
|
||||
const styles = stylesDocument.querySelectorAll('style');
|
||||
assert.equal(styles.length, 1);
|
||||
styleContents = styles[0].textContent;
|
||||
assert.equal(styles.length, 2);
|
||||
styleContents = styles[1].textContent;
|
||||
} else {
|
||||
const links = stylesDocument.querySelectorAll('link[rel="stylesheet"]');
|
||||
assert.equal(links.length, 1);
|
||||
styleContents = await fixture.readFile(links[0].href);
|
||||
assert.equal(links.length, 2);
|
||||
styleContents = await fixture.readFile(links[1].href);
|
||||
}
|
||||
assert.equal(styleContents.includes('--color-base-purple: 269, 79%;'), true);
|
||||
});
|
||||
|
@ -58,10 +58,10 @@ describe('Markdoc - propagated assets', () => {
|
|||
it('[fails] Does not bleed styles to other page', async () => {
|
||||
if (mode === 'dev') {
|
||||
const styles = scriptsDocument.querySelectorAll('style');
|
||||
assert.equal(styles.length, 0);
|
||||
assert.equal(styles.length, 1);
|
||||
} else {
|
||||
const links = scriptsDocument.querySelectorAll('link[rel="stylesheet"]');
|
||||
assert.equal(links.length, 0);
|
||||
assert.equal(links.length, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,7 +28,7 @@ describe('Head injection w/ MDX', () => {
|
|||
const { document } = parseHTML(html);
|
||||
|
||||
const links = document.querySelectorAll('head link[rel=stylesheet]');
|
||||
assert.equal(links.length, 1);
|
||||
assert.equal(links.length, 2);
|
||||
|
||||
const scripts = document.querySelectorAll('script[type=module]');
|
||||
assert.equal(scripts.length, 1);
|
||||
|
@ -39,7 +39,7 @@ describe('Head injection w/ MDX', () => {
|
|||
const { document } = parseHTML(html);
|
||||
|
||||
const links = document.querySelectorAll('head link[rel=stylesheet]');
|
||||
assert.equal(links.length, 1);
|
||||
assert.equal(links.length, 2);
|
||||
});
|
||||
|
||||
it('injects content from a component using Content#render()', async () => {
|
||||
|
@ -47,7 +47,7 @@ describe('Head injection w/ MDX', () => {
|
|||
const { document } = parseHTML(html);
|
||||
|
||||
const links = document.querySelectorAll('head link[rel=stylesheet]');
|
||||
assert.equal(links.length, 1);
|
||||
assert.equal(links.length, 2);
|
||||
|
||||
const scripts = document.querySelectorAll('script[type=module]');
|
||||
assert.equal(scripts.length, 1);
|
||||
|
@ -67,7 +67,7 @@ describe('Head injection w/ MDX', () => {
|
|||
const $ = cheerio.load(html);
|
||||
|
||||
const headLinks = $('head link[rel=stylesheet]');
|
||||
assert.equal(headLinks.length, 1);
|
||||
assert.equal(headLinks.length, 2);
|
||||
|
||||
const bodyLinks = $('body link[rel=stylesheet]');
|
||||
assert.equal(bodyLinks.length, 0);
|
||||
|
@ -79,7 +79,7 @@ describe('Head injection w/ MDX', () => {
|
|||
const $ = cheerio.load(html);
|
||||
|
||||
const headLinks = $('head link[rel=stylesheet]');
|
||||
assert.equal(headLinks.length, 1);
|
||||
assert.equal(headLinks.length, 2);
|
||||
|
||||
const bodyLinks = $('body link[rel=stylesheet]');
|
||||
assert.equal(bodyLinks.length, 0);
|
||||
|
@ -92,7 +92,7 @@ describe('Head injection w/ MDX', () => {
|
|||
const $ = cheerio.load(html);
|
||||
|
||||
const headLinks = $('head link[rel=stylesheet]');
|
||||
assert.equal(headLinks.length, 1);
|
||||
assert.equal(headLinks.length, 2);
|
||||
|
||||
const bodyLinks = $('body link[rel=stylesheet]');
|
||||
assert.equal(bodyLinks.length, 0);
|
||||
|
|
|
@ -28,7 +28,7 @@ describe('MDX math', () => {
|
|||
const mjxContainer = document.querySelector('mjx-container[jax="SVG"]');
|
||||
assert.notEqual(mjxContainer, null);
|
||||
|
||||
const mjxStyle = document.querySelector('style').innerHTML;
|
||||
const mjxStyle = document.querySelectorAll('style')[1].innerHTML;
|
||||
assert.equal(
|
||||
mjxStyle.includes('mjx-container[jax="SVG"]'),
|
||||
true,
|
||||
|
@ -62,7 +62,7 @@ describe('MDX math', () => {
|
|||
const mjxContainer = document.querySelector('mjx-container[jax="CHTML"]');
|
||||
assert.notEqual(mjxContainer, null);
|
||||
|
||||
const mjxStyle = document.querySelector('style').innerHTML;
|
||||
const mjxStyle = document.querySelectorAll('style')[1].innerHTML;
|
||||
assert.equal(
|
||||
mjxStyle.includes('mjx-container[jax="CHTML"]'),
|
||||
true,
|
||||
|
|
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
|
@ -2722,6 +2722,12 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/core-image-layout:
|
||||
dependencies:
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/core-image-remark-imgattr:
|
||||
dependencies:
|
||||
astro:
|
||||
|
|
Loading…
Add table
Reference in a new issue