feat(image): throw if alt text is missing (#4511)
* feat(image): throw if no `alt` is provided * chore: add changeset * docs(image): update README * updated alt text stuff throughout * fixing with-mdx test suite * warn for missing alt text, will throw an error in a future release * final README tweaks Co-authored-by: Tony Sullivan <tony.f.sullivan@outlook.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
df402ddc93
commit
72c760e9b8
25 changed files with 391 additions and 50 deletions
5
.changeset/polite-pears-hope.md
Normal file
5
.changeset/polite-pears-hope.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@astrojs/image': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: throw if alt text is missing
|
|
@ -18,7 +18,7 @@ This **[Astro integration][astro-integration]** makes it easy to optimize images
|
||||||
|
|
||||||
Images play a big role in overall site performance and usability. Serving properly sized images makes all the difference but is often tricky to automate.
|
Images play a big role in overall site performance and usability. Serving properly sized images makes all the difference but is often tricky to automate.
|
||||||
|
|
||||||
This integration provides `<Image />` and `<Picture>` components as well as a basic image transformer powered by [sharp](https://sharp.pixelplumbing.com/), with full support for static sites and server-side rendering. The built-in `sharp` transformer is also replacable, opening the door for future integrations that work with your favorite hosted image service.
|
This integration provides `<Image />` and `<Picture>` components as well as a basic image transformer powered by [sharp](https://sharp.pixelplumbing.com/), with full support for static sites and server-side rendering. The built-in `sharp` transformer is also replaceable, opening the door for future integrations that work with your favorite hosted image service.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -90,6 +90,10 @@ import { Image, Picture } from '@astrojs/image/components';
|
||||||
|
|
||||||
The included `sharp` transformer supports resizing images and encoding them to different image formats. Third-party image services will be able to add support for custom transformations as well (ex: `blur`, `filter`, `rotate`, etc).
|
The included `sharp` transformer supports resizing images and encoding them to different image formats. Third-party image services will be able to add support for custom transformations as well (ex: `blur`, `filter`, `rotate`, etc).
|
||||||
|
|
||||||
|
Astro’s <Image /> and <Picture /> components require the alt attribute which provides descriptive text for images. A warning will be logged if "alt" text is missing, and a future release of the integration will throw an error if no alt text is provided.
|
||||||
|
|
||||||
|
If the image is merely decorative (i.e. doesn’t contribute to the understanding of the page), set alt="" so that the image is properly understood and ignored by screen readers.
|
||||||
|
|
||||||
### `<Image />`
|
### `<Image />`
|
||||||
|
|
||||||
The built-in `<Image />` component is used to create an optimized `<img />` for both remote images hosted on other domains as well as local images imported from your project's `src` directory.
|
The built-in `<Image />` component is used to create an optimized `<img />` for both remote images hosted on other domains as well as local images imported from your project's `src` directory.
|
||||||
|
@ -112,6 +116,18 @@ For images located in your project's `src`: use the file path relative to the `s
|
||||||
|
|
||||||
For remote images, provide the full URL. (e.g. `src="https://astro.build/assets/blog/astro-1-release-update.avif"`)
|
For remote images, provide the full URL. (e.g. `src="https://astro.build/assets/blog/astro-1-release-update.avif"`)
|
||||||
|
|
||||||
|
#### alt
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
**Type:** `string`<br>
|
||||||
|
**Required:** `true`
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Defines an alternative text description of the image.
|
||||||
|
|
||||||
|
Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel).
|
||||||
|
|
||||||
#### format
|
#### format
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
@ -186,17 +202,23 @@ A `number` can also be provided, useful when the aspect ratio is calculated at b
|
||||||
|
|
||||||
Source for the original image file.
|
Source for the original image file.
|
||||||
|
|
||||||
For images in your project's repository, use the path relative to the `src` or `public` directory. For remote images, provide the full URL.
|
For images located in your project's `src`: use the file path relative to the `src` directory. (e.g. `src="../assets/source-pic.png"`)
|
||||||
|
|
||||||
|
For images located in your `public` directory: use the URL path relative to the `public` directory. (e.g. `src="/images/public-image.jpg"`)
|
||||||
|
|
||||||
|
For remote images, provide the full URL. (e.g. `src="https://astro.build/assets/blog/astro-1-release-update.avif"`)
|
||||||
|
|
||||||
#### alt
|
#### alt
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
**Type:** `string`<br>
|
**Type:** `string`<br>
|
||||||
**Default:** `undefined`
|
**Required:** `true`
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
If provided, the `alt` string will be included on the built `<img />` element.
|
Defines an alternative text description of the image.
|
||||||
|
|
||||||
|
Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel).
|
||||||
|
|
||||||
#### sizes
|
#### sizes
|
||||||
|
|
||||||
|
@ -266,7 +288,7 @@ const { src } = await getImage('../assets/hero.png');
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link rel="preload" as="image" href={src}>
|
<link rel="preload" as="image" href={src} alt="alt text">
|
||||||
</head>
|
</head>
|
||||||
</html>
|
</html>
|
||||||
```
|
```
|
||||||
|
@ -330,19 +352,19 @@ import heroImage from '../assets/hero.png';
|
||||||
---
|
---
|
||||||
|
|
||||||
// optimized image, keeping the original width, height, and image format
|
// optimized image, keeping the original width, height, and image format
|
||||||
<Image src={heroImage} />
|
<Image src={heroImage} alt="descriptive text" />
|
||||||
|
|
||||||
// height will be recalculated to match the original aspect ratio
|
// height will be recalculated to match the original aspect ratio
|
||||||
<Image src={heroImage} width={300} />
|
<Image src={heroImage} width={300} alt="descriptive text" />
|
||||||
|
|
||||||
// cropping to a specific width and height
|
// cropping to a specific width and height
|
||||||
<Image src={heroImage} width={300} height={600} />
|
<Image src={heroImage} width={300} height={600} alt="descriptive text" />
|
||||||
|
|
||||||
// cropping to a specific aspect ratio and converting to an avif format
|
// cropping to a specific aspect ratio and converting to an avif format
|
||||||
<Image src={heroImage} aspectRatio="16:9" format="avif" />
|
<Image src={heroImage} aspectRatio="16:9" format="avif" alt="descriptive text" />
|
||||||
|
|
||||||
// image imports can also be inlined directly
|
// image imports can also be inlined directly
|
||||||
<Image src={import('../assets/hero.png')} />
|
<Image src={import('../assets/hero.png')} alt="descriptive text" />
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Images in `/public`
|
#### Images in `/public`
|
||||||
|
@ -360,7 +382,7 @@ import socialImage from '/social.png';
|
||||||
---
|
---
|
||||||
// In static builds: the image will be built and optimized to `/dist`.
|
// In static builds: the image will be built and optimized to `/dist`.
|
||||||
// In SSR builds: the image will be optimized by the server when requested by a browser.
|
// In SSR builds: the image will be optimized by the server when requested by a browser.
|
||||||
<Image src={socialImage} width={1280} aspectRatio="16:9" />
|
<Image src={socialImage} width={1280} aspectRatio="16:9" alt="descriptive text" />
|
||||||
```
|
```
|
||||||
|
|
||||||
### Remote images
|
### Remote images
|
||||||
|
@ -375,13 +397,13 @@ const imageUrl = 'https://www.google.com/images/branding/googlelogo/2x/googlelog
|
||||||
---
|
---
|
||||||
|
|
||||||
// cropping to a specific width and height
|
// cropping to a specific width and height
|
||||||
<Image src={imageUrl} width={544} height={184} />
|
<Image src={imageUrl} width={544} height={184} alt="descriptive text" />
|
||||||
|
|
||||||
// height will be recalculated to match the aspect ratio
|
// height will be recalculated to match the aspect ratio
|
||||||
<Image src={imageUrl} width={300} aspectRatio={16/9} />
|
<Image src={imageUrl} width={300} aspectRatio={16/9} alt="descriptive text" />
|
||||||
|
|
||||||
// cropping to a specific height and aspect ratio and converting to an avif format
|
// cropping to a specific height and aspect ratio and converting to an avif format
|
||||||
<Image src={imageUrl} height={200} aspectRatio="16:9" format="avif" />
|
<Image src={imageUrl} height={200} aspectRatio="16:9" format="avif" alt="descriptive text" />
|
||||||
```
|
```
|
||||||
|
|
||||||
### Responsive pictures
|
### Responsive pictures
|
||||||
|
@ -401,13 +423,13 @@ const imageUrl = 'https://www.google.com/images/branding/googlelogo/2x/googlelog
|
||||||
---
|
---
|
||||||
|
|
||||||
// Local image with multiple sizes
|
// Local image with multiple sizes
|
||||||
<Picture src={hero} widths={[200, 400, 800]} sizes="(max-width: 800px) 100vw, 800px" alt="My hero image" />
|
<Picture src={hero} widths={[200, 400, 800]} sizes="(max-width: 800px) 100vw, 800px" alt="descriptive text" />
|
||||||
|
|
||||||
// Remote image (aspect ratio is required)
|
// Remote image (aspect ratio is required)
|
||||||
<Picture src={imageUrl} widths={[200, 400, 800]} aspectRatio="4:3" sizes="(max-width: 800px) 100vw, 800px" alt="My hero image" />
|
<Picture src={imageUrl} widths={[200, 400, 800]} aspectRatio="4:3" sizes="(max-width: 800px) 100vw, 800px" alt="descriptive text" />
|
||||||
|
|
||||||
// Inlined imports are supported
|
// Inlined imports are supported
|
||||||
<Picture src={import("../assets/hero.png")} widths={[200, 400, 800]} sizes="(max-width: 800px) 100vw, 800px" alt="My hero image" />
|
<Picture src={import("../assets/hero.png")} widths={[200, 400, 800]} sizes="(max-width: 800px) 100vw, 800px" alt="descriptive text" />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { getImage } from '../dist/index.js';
|
import { getImage } from '../dist/index.js';
|
||||||
|
import { warnForMissingAlt } from './index.js';
|
||||||
import type { ImgHTMLAttributes } from './index.js';
|
import type { ImgHTMLAttributes } from './index.js';
|
||||||
import type { ImageMetadata, TransformOptions, OutputFormat } from '../dist/index.js';
|
import type { ImageMetadata, TransformOptions, OutputFormat } from '../dist/index.js';
|
||||||
|
|
||||||
|
@ -8,10 +9,14 @@ interface LocalImageProps
|
||||||
extends Omit<TransformOptions, 'src'>,
|
extends Omit<TransformOptions, 'src'>,
|
||||||
Omit<ImgHTMLAttributes, 'src' | 'width' | 'height'> {
|
Omit<ImgHTMLAttributes, 'src' | 'width' | 'height'> {
|
||||||
src: ImageMetadata | Promise<{ default: ImageMetadata }>;
|
src: ImageMetadata | Promise<{ default: ImageMetadata }>;
|
||||||
|
/** Defines an alternative text description of the image. Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel). */
|
||||||
|
alt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RemoteImageProps extends TransformOptions, astroHTML.JSX.ImgHTMLAttributes {
|
interface RemoteImageProps extends TransformOptions, astroHTML.JSX.ImgHTMLAttributes {
|
||||||
src: string;
|
src: string;
|
||||||
|
/** Defines an alternative text description of the image. Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel). */
|
||||||
|
alt: string;
|
||||||
format: OutputFormat;
|
format: OutputFormat;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
@ -21,6 +26,10 @@ export type Props = LocalImageProps | RemoteImageProps;
|
||||||
|
|
||||||
const { loading = 'lazy', decoding = 'async', ...props } = Astro.props as Props;
|
const { loading = 'lazy', decoding = 'async', ...props } = Astro.props as Props;
|
||||||
|
|
||||||
|
if (props.alt === undefined || props.alt === null) {
|
||||||
|
warnForMissingAlt();
|
||||||
|
}
|
||||||
|
|
||||||
const attrs = await getImage(props);
|
const attrs = await getImage(props);
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
---
|
---
|
||||||
import { getPicture } from '../dist/index.js';
|
import { getPicture } from '../dist/index.js';
|
||||||
|
import { warnForMissingAlt } from './index.js';
|
||||||
import type { ImgHTMLAttributes, HTMLAttributes } from './index.js';
|
import type { ImgHTMLAttributes, HTMLAttributes } from './index.js';
|
||||||
import type { ImageMetadata, OutputFormat, TransformOptions } from '../dist/index.js';
|
import type { ImageMetadata, OutputFormat, TransformOptions } from '../dist/index.js';
|
||||||
|
|
||||||
|
@ -8,7 +9,8 @@ interface LocalImageProps
|
||||||
Omit<TransformOptions, 'src'>,
|
Omit<TransformOptions, 'src'>,
|
||||||
Pick<astroHTML.JSX.ImgHTMLAttributes, 'loading' | 'decoding'> {
|
Pick<astroHTML.JSX.ImgHTMLAttributes, 'loading' | 'decoding'> {
|
||||||
src: ImageMetadata | Promise<{ default: ImageMetadata }>;
|
src: ImageMetadata | Promise<{ default: ImageMetadata }>;
|
||||||
alt?: string;
|
/** Defines an alternative text description of the image. Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel). */
|
||||||
|
alt: string;
|
||||||
sizes: HTMLImageElement['sizes'];
|
sizes: HTMLImageElement['sizes'];
|
||||||
widths: number[];
|
widths: number[];
|
||||||
formats?: OutputFormat[];
|
formats?: OutputFormat[];
|
||||||
|
@ -19,7 +21,8 @@ interface RemoteImageProps
|
||||||
TransformOptions,
|
TransformOptions,
|
||||||
Pick<ImgHTMLAttributes, 'loading' | 'decoding'> {
|
Pick<ImgHTMLAttributes, 'loading' | 'decoding'> {
|
||||||
src: string;
|
src: string;
|
||||||
alt?: string;
|
/** Defines an alternative text description of the image. Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel). */
|
||||||
|
alt: string;
|
||||||
sizes: HTMLImageElement['sizes'];
|
sizes: HTMLImageElement['sizes'];
|
||||||
widths: number[];
|
widths: number[];
|
||||||
aspectRatio: TransformOptions['aspectRatio'];
|
aspectRatio: TransformOptions['aspectRatio'];
|
||||||
|
@ -40,6 +43,10 @@ const {
|
||||||
...attrs
|
...attrs
|
||||||
} = Astro.props as Props;
|
} = Astro.props as Props;
|
||||||
|
|
||||||
|
if (alt === undefined || alt === null) {
|
||||||
|
warnForMissingAlt();
|
||||||
|
}
|
||||||
|
|
||||||
const { image, sources } = await getPicture({ src, widths, formats, aspectRatio });
|
const { image, sources } = await getPicture({ src, widths, formats, aspectRatio });
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -11,3 +11,17 @@ export type HTMLAttributes = Omit<
|
||||||
astroHTML.JSX.HTMLAttributes,
|
astroHTML.JSX.HTMLAttributes,
|
||||||
'client:list' | 'set:text' | 'set:html' | 'is:raw'
|
'client:list' | 'set:text' | 'set:html' | 'is:raw'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
let altWarningShown = false;
|
||||||
|
|
||||||
|
export function warnForMissingAlt() {
|
||||||
|
if (altWarningShown === true) { return }
|
||||||
|
|
||||||
|
altWarningShown = true;
|
||||||
|
|
||||||
|
console.warn(`\n[@astrojs/image] "alt" text was not provided for an <Image> or <Picture> component.
|
||||||
|
|
||||||
|
A future release of @astrojs/image may throw a build error when "alt" text is missing.
|
||||||
|
|
||||||
|
The "alt" attribute holds a text description of the image, which isn't mandatory but is incredibly useful for accessibility. Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel).\n`);
|
||||||
|
}
|
||||||
|
|
|
@ -9,16 +9,16 @@ import { Image } from '@astrojs/image/components';
|
||||||
<!-- Head Stuff -->
|
<!-- Head Stuff -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<Image id="hero" src="/hero.jpg" width={768} height={414} format="webp" />
|
<Image id="hero" src="/hero.jpg" width={768} height={414} format="webp" alt="hero" />
|
||||||
<br />
|
<br />
|
||||||
<Image id="spaces" src={introJpg} width={768} height={414} format="webp" />
|
<Image id="spaces" src={introJpg} width={768} height={414} format="webp" alt="spaces" />
|
||||||
<br />
|
<br />
|
||||||
<Image id="social-jpg" src={socialJpg} width={506} height={253} />
|
<Image id="social-jpg" src={socialJpg} width={506} height={253} alt="social-jpg" />
|
||||||
<br />
|
<br />
|
||||||
<Image id="google" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" width={544} height={184} format="webp" />
|
<Image id="google" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" width={544} height={184} format="webp" alt="Google" />
|
||||||
<br />
|
<br />
|
||||||
<Image id="inline" src={import('../assets/social.jpg')} width={506} />
|
<Image id="inline" src={import('../assets/social.jpg')} width={506} alt="inline" />
|
||||||
<br />
|
<br />
|
||||||
<Image id="query" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png?token=abc" width={544} height={184} format="webp" />
|
<Image id="query" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png?token=abc" width={544} height={184} format="webp" alt="query" />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
8
packages/integrations/image/test/fixtures/no-alt-text-image/astro.config.mjs
vendored
Normal file
8
packages/integrations/image/test/fixtures/no-alt-text-image/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import image from '@astrojs/image';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
site: 'http://localhost:3000',
|
||||||
|
integrations: [image({ logLevel: 'silent' })]
|
||||||
|
});
|
10
packages/integrations/image/test/fixtures/no-alt-text-image/package.json
vendored
Normal file
10
packages/integrations/image/test/fixtures/no-alt-text-image/package.json
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"name": "@test/no-alt-text-image",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/image": "workspace:*",
|
||||||
|
"@astrojs/node": "workspace:*",
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
BIN
packages/integrations/image/test/fixtures/no-alt-text-image/public/favicon.ico
vendored
Normal file
BIN
packages/integrations/image/test/fixtures/no-alt-text-image/public/favicon.ico
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
44
packages/integrations/image/test/fixtures/no-alt-text-image/server/server.mjs
vendored
Normal file
44
packages/integrations/image/test/fixtures/no-alt-text-image/server/server.mjs
vendored
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { createServer } from 'http';
|
||||||
|
import fs from 'fs';
|
||||||
|
import mime from 'mime';
|
||||||
|
import { handler as ssrHandler } from '../dist/server/entry.mjs';
|
||||||
|
|
||||||
|
const clientRoot = new URL('../dist/client/', import.meta.url);
|
||||||
|
|
||||||
|
async function handle(req, res) {
|
||||||
|
ssrHandler(req, res, async (err) => {
|
||||||
|
if (err) {
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end(err.stack);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let local = new URL('.' + req.url, clientRoot);
|
||||||
|
try {
|
||||||
|
const data = await fs.promises.readFile(local);
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': mime.getType(req.url),
|
||||||
|
});
|
||||||
|
res.end(data);
|
||||||
|
} catch {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = createServer((req, res) => {
|
||||||
|
handle(req, res).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
res.writeHead(500, {
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
});
|
||||||
|
res.end(err.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(8085);
|
||||||
|
console.log('Serving at http://localhost:8085');
|
||||||
|
|
||||||
|
// Silence weird <time> warning
|
||||||
|
console.error = () => {};
|
BIN
packages/integrations/image/test/fixtures/no-alt-text-image/src/assets/social.jpg
vendored
Normal file
BIN
packages/integrations/image/test/fixtures/no-alt-text-image/src/assets/social.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
13
packages/integrations/image/test/fixtures/no-alt-text-image/src/pages/index.astro
vendored
Normal file
13
packages/integrations/image/test/fixtures/no-alt-text-image/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
import socialJpg from '../assets/social.jpg';
|
||||||
|
import { Image } from '@astrojs/image/components';
|
||||||
|
---
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!-- Head Stuff -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Image id="social-jpg" src={socialJpg} width={506} height={253} />
|
||||||
|
</body>
|
||||||
|
</html>
|
8
packages/integrations/image/test/fixtures/no-alt-text-picture/astro.config.mjs
vendored
Normal file
8
packages/integrations/image/test/fixtures/no-alt-text-picture/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import image from '@astrojs/image';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
site: 'http://localhost:3000',
|
||||||
|
integrations: [image({ logLevel: 'silent' })]
|
||||||
|
});
|
10
packages/integrations/image/test/fixtures/no-alt-text-picture/package.json
vendored
Normal file
10
packages/integrations/image/test/fixtures/no-alt-text-picture/package.json
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"name": "@test/no-alt-text-picture",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/image": "workspace:*",
|
||||||
|
"@astrojs/node": "workspace:*",
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
BIN
packages/integrations/image/test/fixtures/no-alt-text-picture/public/favicon.ico
vendored
Normal file
BIN
packages/integrations/image/test/fixtures/no-alt-text-picture/public/favicon.ico
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
44
packages/integrations/image/test/fixtures/no-alt-text-picture/server/server.mjs
vendored
Normal file
44
packages/integrations/image/test/fixtures/no-alt-text-picture/server/server.mjs
vendored
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { createServer } from 'http';
|
||||||
|
import fs from 'fs';
|
||||||
|
import mime from 'mime';
|
||||||
|
import { handler as ssrHandler } from '../dist/server/entry.mjs';
|
||||||
|
|
||||||
|
const clientRoot = new URL('../dist/client/', import.meta.url);
|
||||||
|
|
||||||
|
async function handle(req, res) {
|
||||||
|
ssrHandler(req, res, async (err) => {
|
||||||
|
if (err) {
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end(err.stack);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let local = new URL('.' + req.url, clientRoot);
|
||||||
|
try {
|
||||||
|
const data = await fs.promises.readFile(local);
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': mime.getType(req.url),
|
||||||
|
});
|
||||||
|
res.end(data);
|
||||||
|
} catch {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = createServer((req, res) => {
|
||||||
|
handle(req, res).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
res.writeHead(500, {
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
});
|
||||||
|
res.end(err.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(8085);
|
||||||
|
console.log('Serving at http://localhost:8085');
|
||||||
|
|
||||||
|
// Silence weird <time> warning
|
||||||
|
console.error = () => {};
|
BIN
packages/integrations/image/test/fixtures/no-alt-text-picture/src/assets/social.jpg
vendored
Normal file
BIN
packages/integrations/image/test/fixtures/no-alt-text-picture/src/assets/social.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
13
packages/integrations/image/test/fixtures/no-alt-text-picture/src/pages/index.astro
vendored
Normal file
13
packages/integrations/image/test/fixtures/no-alt-text-picture/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
import socialJpg from '../assets/social.jpg';
|
||||||
|
import { Picture } from '@astrojs/image/components';
|
||||||
|
---
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!-- Head Stuff -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Picture id="social-jpg" src={socialJpg} sizes="(min-width: 640px) 50vw, 100vw" />
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -7,42 +7,42 @@ import { Image } from '@astrojs/image/components';
|
||||||
<!-- Head Stuff -->
|
<!-- Head Stuff -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<Image id='landscape-0' src={import('../assets/Landscape_0.jpg')} />
|
<Image id='landscape-0' src={import('../assets/Landscape_0.jpg')} alt="landscape-0" />
|
||||||
<br />
|
<br />
|
||||||
<Image id='landscape-1' src={import('../assets/Landscape_1.jpg')} />
|
<Image id='landscape-1' src={import('../assets/Landscape_1.jpg')} alt="landscape-1" />
|
||||||
<br />
|
<br />
|
||||||
<Image id='landscape-2' src={import('../assets/Landscape_2.jpg')} />
|
<Image id='landscape-2' src={import('../assets/Landscape_2.jpg')} alt="landscape-2" />
|
||||||
<br />
|
<br />
|
||||||
<Image id='landscape-3' src={import('../assets/Landscape_3.jpg')} />
|
<Image id='landscape-3' src={import('../assets/Landscape_3.jpg')} alt="landscape-3" />
|
||||||
<br />
|
<br />
|
||||||
<Image id='landscape-4' src={import('../assets/Landscape_4.jpg')} />
|
<Image id='landscape-4' src={import('../assets/Landscape_4.jpg')} alt="landscape-4" />
|
||||||
<br />
|
<br />
|
||||||
<Image id='landscape-5' src={import('../assets/Landscape_5.jpg')} />
|
<Image id='landscape-5' src={import('../assets/Landscape_5.jpg')} alt="landscape-5" />
|
||||||
<br />
|
<br />
|
||||||
<Image id='landscape-6' src={import('../assets/Landscape_6.jpg')} />
|
<Image id='landscape-6' src={import('../assets/Landscape_6.jpg')} alt="landscape-6" />
|
||||||
<br />
|
<br />
|
||||||
<Image id='landscape-7' src={import('../assets/Landscape_7.jpg')} />
|
<Image id='landscape-7' src={import('../assets/Landscape_7.jpg')} alt="landscape-7" />
|
||||||
<br />
|
<br />
|
||||||
<Image id='landscape-8' src={import('../assets/Landscape_8.jpg')} />
|
<Image id='landscape-8' src={import('../assets/Landscape_8.jpg')} alt="landscape-8" />
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<Image id='portrait-0' src={import('../assets/Portrait_0.jpg')} />
|
<Image id='portrait-0' src={import('../assets/Portrait_0.jpg')} alt="portrait-0" />
|
||||||
<br />
|
<br />
|
||||||
<Image id='portrait-1' src={import('../assets/Portrait_1.jpg')} />
|
<Image id='portrait-1' src={import('../assets/Portrait_1.jpg')} alt="portrait-1" />
|
||||||
<br />
|
<br />
|
||||||
<Image id='portrait-2' src={import('../assets/Portrait_2.jpg')} />
|
<Image id='portrait-2' src={import('../assets/Portrait_2.jpg')} alt="portrait-2" />
|
||||||
<br />
|
<br />
|
||||||
<Image id='portrait-3' src={import('../assets/Portrait_3.jpg')} />
|
<Image id='portrait-3' src={import('../assets/Portrait_3.jpg')} alt="portrait-3" />
|
||||||
<br />
|
<br />
|
||||||
<Image id='portrait-4' src={import('../assets/Portrait_4.jpg')} />
|
<Image id='portrait-4' src={import('../assets/Portrait_4.jpg')} alt="portrait-4" />
|
||||||
<br />
|
<br />
|
||||||
<Image id='portrait-5' src={import('../assets/Portrait_5.jpg')} />
|
<Image id='portrait-5' src={import('../assets/Portrait_5.jpg')} alt="portrait-5" />
|
||||||
<br />
|
<br />
|
||||||
<Image id='portrait-6' src={import('../assets/Portrait_6.jpg')} />
|
<Image id='portrait-6' src={import('../assets/Portrait_6.jpg')} alt="portrait-6" />
|
||||||
<br />
|
<br />
|
||||||
<Image id='portrait-7' src={import('../assets/Portrait_7.jpg')} />
|
<Image id='portrait-7' src={import('../assets/Portrait_7.jpg')} alt="portrait-7" />
|
||||||
<br />
|
<br />
|
||||||
<Image id='portrait-8' src={import('../assets/Portrait_8.jpg')} />
|
<Image id='portrait-8' src={import('../assets/Portrait_8.jpg')} alt="portrait-8" />
|
||||||
<br />
|
<br />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -6,12 +6,12 @@ import socialJpg from '../assets/social.jpg';
|
||||||
import { Image } from '@astrojs/image/components';
|
import { Image } from '@astrojs/image/components';
|
||||||
|
|
||||||
|
|
||||||
<Image id="hero" src="/hero.jpg" width={768} height={414} format="webp" />
|
<Image id="hero" src="/hero.jpg" width={768} height={414} format="webp" alt="hero" />
|
||||||
<br />
|
<br />
|
||||||
<Image id="social-jpg" src={socialJpg} width={506} height={253} />
|
<Image id="social-jpg" src={socialJpg} width={506} height={253} alt="social-jpg" />
|
||||||
<br />
|
<br />
|
||||||
<Image id="google" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" width={544} height={184} format="webp" />
|
<Image id="google" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" width={544} height={184} format="webp" alt="Google" />
|
||||||
<br />
|
<br />
|
||||||
<Image id="inline" src={import('../assets/social.jpg')} width={506} />
|
<Image id="inline" src={import('../assets/social.jpg')} width={506} alt="inline" />
|
||||||
<br />
|
<br />
|
||||||
<Image id="query" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png?token=abc" width={544} height={184} format="webp" />
|
<Image id="query" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png?token=abc" width={544} height={184} format="webp" alt="query" />
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { loadFixture } from './test-utils.js';
|
||||||
|
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
const errorMessage =
|
||||||
|
'The <Image> component requires you provide alt text. If this image does not require an accessible label, set alt="".';
|
||||||
|
|
||||||
|
/** TODO: enable the test once missing alt text throws an error instead of a console warning */
|
||||||
|
describe.skip('SSG image without alt text', function () {
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({ root: './fixtures/no-alt-text-image/' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws during build', async () => {
|
||||||
|
try {
|
||||||
|
await fixture.build();
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).to.equal(errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail(0, 1, 'Exception not thrown');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { loadFixture } from './test-utils.js';
|
||||||
|
import testAdapter from '../../../astro/test/test-adapter.js';
|
||||||
|
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
const errorMessage =
|
||||||
|
'The <Image> component requires you provide alt text. If this image does not require an accessible label, set alt="".';
|
||||||
|
|
||||||
|
/** TODO: enable the test once missing alt text throws an error instead of a console warning */
|
||||||
|
describe.skip('SSR image without alt text', function () {
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/no-alt-text-image/',
|
||||||
|
adapter: testAdapter({ streaming: false }),
|
||||||
|
output: 'server',
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws during build', async () => {
|
||||||
|
try {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/');
|
||||||
|
const response = await app.render(request);
|
||||||
|
await response.text();
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).to.equal(errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail(0, 1, 'Exception not thrown');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { loadFixture } from './test-utils.js';
|
||||||
|
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
const errorMessage =
|
||||||
|
'The <Picture> component requires you provide alt text. If this picture does not require an accessible label, set alt="".';
|
||||||
|
|
||||||
|
/** TODO: enable the test once missing alt text throws an error instead of a console warning */
|
||||||
|
describe.skip('SSG picture without alt text', function () {
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({ root: './fixtures/no-alt-text-picture/' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws during build', async () => {
|
||||||
|
try {
|
||||||
|
await fixture.build();
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).to.equal(errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail(0, 1, 'Exception not thrown');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { loadFixture } from './test-utils.js';
|
||||||
|
import testAdapter from '../../../astro/test/test-adapter.js';
|
||||||
|
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
const errorMessage =
|
||||||
|
'The <Picture> component requires you provide alt text. If this picture does not require an accessible label, set alt="".';
|
||||||
|
|
||||||
|
/** TODO: enable the test once missing alt text throws an error instead of a console warning */
|
||||||
|
describe.skip('SSR picture without alt text', function () {
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: './fixtures/no-alt-text-picture/',
|
||||||
|
adapter: testAdapter({ streaming: false }),
|
||||||
|
output: 'server',
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws during build', async () => {
|
||||||
|
try {
|
||||||
|
const app = await fixture.loadTestAdapterApp();
|
||||||
|
const request = new Request('http://example.com/');
|
||||||
|
const response = await app.render(request);
|
||||||
|
await response.text();
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).to.equal(errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail(0, 1, 'Exception not thrown');
|
||||||
|
});
|
||||||
|
});
|
|
@ -2253,6 +2253,26 @@ importers:
|
||||||
'@astrojs/node': link:../../../../node
|
'@astrojs/node': link:../../../../node
|
||||||
astro: link:../../../../../astro
|
astro: link:../../../../../astro
|
||||||
|
|
||||||
|
packages/integrations/image/test/fixtures/no-alt-text-image:
|
||||||
|
specifiers:
|
||||||
|
'@astrojs/image': workspace:*
|
||||||
|
'@astrojs/node': workspace:*
|
||||||
|
astro: workspace:*
|
||||||
|
dependencies:
|
||||||
|
'@astrojs/image': link:../../..
|
||||||
|
'@astrojs/node': link:../../../../node
|
||||||
|
astro: link:../../../../../astro
|
||||||
|
|
||||||
|
packages/integrations/image/test/fixtures/no-alt-text-picture:
|
||||||
|
specifiers:
|
||||||
|
'@astrojs/image': workspace:*
|
||||||
|
'@astrojs/node': workspace:*
|
||||||
|
astro: workspace:*
|
||||||
|
dependencies:
|
||||||
|
'@astrojs/image': link:../../..
|
||||||
|
'@astrojs/node': link:../../../../node
|
||||||
|
astro: link:../../../../../astro
|
||||||
|
|
||||||
packages/integrations/image/test/fixtures/rotation:
|
packages/integrations/image/test/fixtures/rotation:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/image': workspace:*
|
'@astrojs/image': workspace:*
|
||||||
|
|
Loading…
Reference in a new issue