diff options
25 files changed, 391 insertions, 50 deletions
diff --git a/.changeset/polite-pears-hope.md b/.changeset/polite-pears-hope.md new file mode 100644 index 000000000..6facd8e89 --- /dev/null +++ b/.changeset/polite-pears-hope.md @@ -0,0 +1,5 @@ +--- +'@astrojs/image': minor +--- + +feat: throw if alt text is missing diff --git a/packages/integrations/image/README.md b/packages/integrations/image/README.md index 7557c4ecd..a1ffa25ae 100644 --- a/packages/integrations/image/README.md +++ b/packages/integrations/image/README.md @@ -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. -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 @@ -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). +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 />` 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. @@ -108,10 +112,22 @@ Source for the original image file. 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 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 + +<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 <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. -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 <p> **Type:** `string`<br> -**Default:** `undefined` +**Required:** `true` </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 @@ -266,7 +288,7 @@ const { src } = await getImage('../assets/hero.png'); <html> <head> - <link rel="preload" as="image" href={src}> + <link rel="preload" as="image" href={src} alt="alt text"> </head> </html> ``` @@ -330,19 +352,19 @@ import heroImage from '../assets/hero.png'; --- // 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 -<Image src={heroImage} width={300} /> +<Image src={heroImage} width={300} alt="descriptive text" /> // 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 -<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 src={import('../assets/hero.png')} /> +<Image src={import('../assets/hero.png')} alt="descriptive text" /> ``` #### Images in `/public` @@ -356,11 +378,11 @@ For example, use an image located at `public/social.png` in either static or SSR ```astro title="src/pages/page.astro" --- import { Image } from '@astrojs/image/components'; -import socialImage from '/social.png'; +import socialImage from '/social.png'; --- // 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. -<Image src={socialImage} width={1280} aspectRatio="16:9" /> +<Image src={socialImage} width={1280} aspectRatio="16:9" alt="descriptive text" /> ``` ### Remote images @@ -375,13 +397,13 @@ const imageUrl = 'https://www.google.com/images/branding/googlelogo/2x/googlelog --- // 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 -<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 -<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 @@ -401,13 +423,13 @@ const imageUrl = 'https://www.google.com/images/branding/googlelogo/2x/googlelog --- // 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) -<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 -<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 diff --git a/packages/integrations/image/components/Image.astro b/packages/integrations/image/components/Image.astro index 20efe6ee0..1777fffab 100644 --- a/packages/integrations/image/components/Image.astro +++ b/packages/integrations/image/components/Image.astro @@ -1,6 +1,7 @@ --- // @ts-ignore import { getImage } from '../dist/index.js'; +import { warnForMissingAlt } from './index.js'; import type { ImgHTMLAttributes } from './index.js'; import type { ImageMetadata, TransformOptions, OutputFormat } from '../dist/index.js'; @@ -8,10 +9,14 @@ interface LocalImageProps extends Omit<TransformOptions, 'src'>, Omit<ImgHTMLAttributes, 'src' | 'width' | 'height'> { 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 { 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; width: number; height: number; @@ -21,6 +26,10 @@ export type Props = LocalImageProps | RemoteImageProps; const { loading = 'lazy', decoding = 'async', ...props } = Astro.props as Props; +if (props.alt === undefined || props.alt === null) { + warnForMissingAlt(); +} + const attrs = await getImage(props); --- diff --git a/packages/integrations/image/components/Picture.astro b/packages/integrations/image/components/Picture.astro index 36eab92b8..7fe43d9db 100644 --- a/packages/integrations/image/components/Picture.astro +++ b/packages/integrations/image/components/Picture.astro @@ -1,5 +1,6 @@ --- import { getPicture } from '../dist/index.js'; +import { warnForMissingAlt } from './index.js'; import type { ImgHTMLAttributes, HTMLAttributes } from './index.js'; import type { ImageMetadata, OutputFormat, TransformOptions } from '../dist/index.js'; @@ -8,7 +9,8 @@ interface LocalImageProps Omit<TransformOptions, 'src'>, Pick<astroHTML.JSX.ImgHTMLAttributes, 'loading' | 'decoding'> { 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']; widths: number[]; formats?: OutputFormat[]; @@ -19,7 +21,8 @@ interface RemoteImageProps TransformOptions, Pick<ImgHTMLAttributes, 'loading' | 'decoding'> { 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']; widths: number[]; aspectRatio: TransformOptions['aspectRatio']; @@ -40,6 +43,10 @@ const { ...attrs } = Astro.props as Props; +if (alt === undefined || alt === null) { + warnForMissingAlt(); +} + const { image, sources } = await getPicture({ src, widths, formats, aspectRatio }); --- diff --git a/packages/integrations/image/components/index.ts b/packages/integrations/image/components/index.ts index 89d8edd03..6a8f420ad 100644 --- a/packages/integrations/image/components/index.ts +++ b/packages/integrations/image/components/index.ts @@ -11,3 +11,17 @@ export type HTMLAttributes = Omit< astroHTML.JSX.HTMLAttributes, '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`); +} diff --git a/packages/integrations/image/test/fixtures/basic-image/src/pages/index.astro b/packages/integrations/image/test/fixtures/basic-image/src/pages/index.astro index 4ded85521..6b203591b 100644 --- a/packages/integrations/image/test/fixtures/basic-image/src/pages/index.astro +++ b/packages/integrations/image/test/fixtures/basic-image/src/pages/index.astro @@ -9,16 +9,16 @@ import { Image } from '@astrojs/image/components'; <!-- Head Stuff --> </head> <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 /> - <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 /> - <Image id="social-jpg" src={socialJpg} width={506} height={253} /> + <Image id="social-jpg" src={socialJpg} width={506} height={253} alt="social-jpg" /> <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 /> - <Image id="inline" src={import('../assets/social.jpg')} width={506} /> + <Image id="inline" src={import('../assets/social.jpg')} width={506} alt="inline" /> <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> </html> diff --git a/packages/integrations/image/test/fixtures/no-alt-text-image/astro.config.mjs b/packages/integrations/image/test/fixtures/no-alt-text-image/astro.config.mjs new file mode 100644 index 000000000..7dafac3b6 --- /dev/null +++ b/packages/integrations/image/test/fixtures/no-alt-text-image/astro.config.mjs @@ -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' })] +}); diff --git a/packages/integrations/image/test/fixtures/no-alt-text-image/package.json b/packages/integrations/image/test/fixtures/no-alt-text-image/package.json new file mode 100644 index 000000000..10c615cd5 --- /dev/null +++ b/packages/integrations/image/test/fixtures/no-alt-text-image/package.json @@ -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:*" + } +} diff --git a/packages/integrations/image/test/fixtures/no-alt-text-image/public/favicon.ico b/packages/integrations/image/test/fixtures/no-alt-text-image/public/favicon.ico Binary files differnew file mode 100644 index 000000000..578ad458b --- /dev/null +++ b/packages/integrations/image/test/fixtures/no-alt-text-image/public/favicon.ico diff --git a/packages/integrations/image/test/fixtures/no-alt-text-image/server/server.mjs b/packages/integrations/image/test/fixtures/no-alt-text-image/server/server.mjs new file mode 100644 index 000000000..d7a0a7a40 --- /dev/null +++ b/packages/integrations/image/test/fixtures/no-alt-text-image/server/server.mjs @@ -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 = () => {}; diff --git a/packages/integrations/image/test/fixtures/no-alt-text-image/src/assets/social.jpg b/packages/integrations/image/test/fixtures/no-alt-text-image/src/assets/social.jpg Binary files differnew file mode 100644 index 000000000..906c76144 --- /dev/null +++ b/packages/integrations/image/test/fixtures/no-alt-text-image/src/assets/social.jpg diff --git a/packages/integrations/image/test/fixtures/no-alt-text-image/src/pages/index.astro b/packages/integrations/image/test/fixtures/no-alt-text-image/src/pages/index.astro new file mode 100644 index 000000000..d2df3e56e --- /dev/null +++ b/packages/integrations/image/test/fixtures/no-alt-text-image/src/pages/index.astro @@ -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> diff --git a/packages/integrations/image/test/fixtures/no-alt-text-picture/astro.config.mjs b/packages/integrations/image/test/fixtures/no-alt-text-picture/astro.config.mjs new file mode 100644 index 000000000..7dafac3b6 --- /dev/null +++ b/packages/integrations/image/test/fixtures/no-alt-text-picture/astro.config.mjs @@ -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' })] +}); diff --git a/packages/integrations/image/test/fixtures/no-alt-text-picture/package.json b/packages/integrations/image/test/fixtures/no-alt-text-picture/package.json new file mode 100644 index 000000000..aae332c94 --- /dev/null +++ b/packages/integrations/image/test/fixtures/no-alt-text-picture/package.json @@ -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:*" + } +} diff --git a/packages/integrations/image/test/fixtures/no-alt-text-picture/public/favicon.ico b/packages/integrations/image/test/fixtures/no-alt-text-picture/public/favicon.ico Binary files differnew file mode 100644 index 000000000..578ad458b --- /dev/null +++ b/packages/integrations/image/test/fixtures/no-alt-text-picture/public/favicon.ico diff --git a/packages/integrations/image/test/fixtures/no-alt-text-picture/server/server.mjs b/packages/integrations/image/test/fixtures/no-alt-text-picture/server/server.mjs new file mode 100644 index 000000000..d7a0a7a40 --- /dev/null +++ b/packages/integrations/image/test/fixtures/no-alt-text-picture/server/server.mjs @@ -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 = () => {}; diff --git a/packages/integrations/image/test/fixtures/no-alt-text-picture/src/assets/social.jpg b/packages/integrations/image/test/fixtures/no-alt-text-picture/src/assets/social.jpg Binary files differnew file mode 100644 index 000000000..906c76144 --- /dev/null +++ b/packages/integrations/image/test/fixtures/no-alt-text-picture/src/assets/social.jpg diff --git a/packages/integrations/image/test/fixtures/no-alt-text-picture/src/pages/index.astro b/packages/integrations/image/test/fixtures/no-alt-text-picture/src/pages/index.astro new file mode 100644 index 000000000..4f5a17ce8 --- /dev/null +++ b/packages/integrations/image/test/fixtures/no-alt-text-picture/src/pages/index.astro @@ -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> diff --git a/packages/integrations/image/test/fixtures/rotation/src/pages/index.astro b/packages/integrations/image/test/fixtures/rotation/src/pages/index.astro index 52124b5c0..5fa379926 100644 --- a/packages/integrations/image/test/fixtures/rotation/src/pages/index.astro +++ b/packages/integrations/image/test/fixtures/rotation/src/pages/index.astro @@ -7,42 +7,42 @@ import { Image } from '@astrojs/image/components'; <!-- Head Stuff --> </head> <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 /> - <Image id='landscape-1' src={import('../assets/Landscape_1.jpg')} /> + <Image id='landscape-1' src={import('../assets/Landscape_1.jpg')} alt="landscape-1" /> <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 /> - <Image id='landscape-3' src={import('../assets/Landscape_3.jpg')} /> + <Image id='landscape-3' src={import('../assets/Landscape_3.jpg')} alt="landscape-3" /> <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 /> - <Image id='landscape-5' src={import('../assets/Landscape_5.jpg')} /> + <Image id='landscape-5' src={import('../assets/Landscape_5.jpg')} alt="landscape-5" /> <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 /> - <Image id='landscape-7' src={import('../assets/Landscape_7.jpg')} /> + <Image id='landscape-7' src={import('../assets/Landscape_7.jpg')} alt="landscape-7" /> <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 /> - <Image id='portrait-0' src={import('../assets/Portrait_0.jpg')} /> + <Image id='portrait-0' src={import('../assets/Portrait_0.jpg')} alt="portrait-0" /> <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 /> - <Image id='portrait-2' src={import('../assets/Portrait_2.jpg')} /> + <Image id='portrait-2' src={import('../assets/Portrait_2.jpg')} alt="portrait-2" /> <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 /> - <Image id='portrait-4' src={import('../assets/Portrait_4.jpg')} /> + <Image id='portrait-4' src={import('../assets/Portrait_4.jpg')} alt="portrait-4" /> <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 /> - <Image id='portrait-6' src={import('../assets/Portrait_6.jpg')} /> + <Image id='portrait-6' src={import('../assets/Portrait_6.jpg')} alt="portrait-6" /> <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 /> - <Image id='portrait-8' src={import('../assets/Portrait_8.jpg')} /> + <Image id='portrait-8' src={import('../assets/Portrait_8.jpg')} alt="portrait-8" /> <br /> </body> </html> diff --git a/packages/integrations/image/test/fixtures/with-mdx/src/pages/index.mdx b/packages/integrations/image/test/fixtures/with-mdx/src/pages/index.mdx index a713f4418..7c155d158 100644 --- a/packages/integrations/image/test/fixtures/with-mdx/src/pages/index.mdx +++ b/packages/integrations/image/test/fixtures/with-mdx/src/pages/index.mdx @@ -6,12 +6,12 @@ import socialJpg from '../assets/social.jpg'; 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 /> -<Image id="social-jpg" src={socialJpg} width={506} height={253} /> +<Image id="social-jpg" src={socialJpg} width={506} height={253} alt="social-jpg" /> <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 /> -<Image id="inline" src={import('../assets/social.jpg')} width={506} /> +<Image id="inline" src={import('../assets/social.jpg')} width={506} alt="inline" /> <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" /> diff --git a/packages/integrations/image/test/no-alt-text-image-ssg.test.js b/packages/integrations/image/test/no-alt-text-image-ssg.test.js new file mode 100644 index 000000000..4c998bdb4 --- /dev/null +++ b/packages/integrations/image/test/no-alt-text-image-ssg.test.js @@ -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'); + }); +}); diff --git a/packages/integrations/image/test/no-alt-text-image-ssr.test.js b/packages/integrations/image/test/no-alt-text-image-ssr.test.js new file mode 100644 index 000000000..95d6572bc --- /dev/null +++ b/packages/integrations/image/test/no-alt-text-image-ssr.test.js @@ -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'); + }); +}); diff --git a/packages/integrations/image/test/no-alt-text-picture-ssg.test.js b/packages/integrations/image/test/no-alt-text-picture-ssg.test.js new file mode 100644 index 000000000..db0c5629f --- /dev/null +++ b/packages/integrations/image/test/no-alt-text-picture-ssg.test.js @@ -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'); + }); +}); diff --git a/packages/integrations/image/test/no-alt-text-picture-ssr.test.js b/packages/integrations/image/test/no-alt-text-picture-ssr.test.js new file mode 100644 index 000000000..82ea4daa2 --- /dev/null +++ b/packages/integrations/image/test/no-alt-text-picture-ssr.test.js @@ -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'); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff2367535..49c882959 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2253,6 +2253,26 @@ importers: '@astrojs/node': link:../../../../node 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: specifiers: '@astrojs/image': workspace:* |