import { expect, within } from "storybook/test"; import type { Meta, StoryObj } from "@storybook/react-vite"; import { BookOpenRegularIcon } from "alouette-icons/phosphor-icons/BookOpen"; import type { ReactNode } from "react"; import type { Accent } from "../../core/AlouetteConfig"; import { Paragraph } from "../primitives/Text"; import { View } from "../primitives/View"; import { Story, accentsWithoutNeutral, neutralAndAccents, } from "../story-components/Story"; import { StoryGrid } from "../story-components/StoryGrid"; import { ExternalLinkText } from "./ExternalLinkText"; type ThisStory = StoryObj; export default { title: "alouette/Actions/ExternalLinkText", component: ExternalLinkText, parameters: { componentSubtitle: "Text link to an external destination, with a leading affordance icon — the lightweight alternative to ExternalLinkButton", docs: { description: { component: `Opens through \`ExternalLink\`: a real \`\` on web, the themed in-app browser sheet on native. Pass \`openLinkBehavior\` for same-tab web navigation or the native \`Linking\` app switch. ### Usage ~~~tsx ~~~`, }, }, }, argTypes: { size: { control: "select", options: ["sm", "md"] }, accent: { control: "select", options: neutralAndAccents, }, disabled: { control: "boolean" }, }, } satisfies Meta; export const PreviewExternalLinkTextStory: ThisStory = { name: "ExternalLinkText Preview", parameters: { layout: "padded", chromatic: { disableSnapshot: true }, }, args: { href: "https://storybook.js.org/", text: "Open Storybook", }, }; function AccentVariant({ accent }: { accent?: Accent }): ReactNode { return ( ); } export const VariantsExternalLinkTextStory: ThisStory = { name: "ExternalLinkText Variants", render: () => ( {(["sm", "md"] as const).map((size) => ( ))} {accentsWithoutNeutral.map((accent) => ( ))} } href="https://storybook.js.org/docs" text="Read the documentation" /> Every component is documented with its props, its variants and a play test. {"Every component is documented with its props and variants. See "} { " for the conventions behind stories and play functions, then open an issue if something is missing." } {"Live updates are not enabled. Contact "} { " to enable them on your workspace, or check the status page for the planned rollout of the feature." } ), }; export const TestsExternalLinkTextStory: ThisStory = { name: "ExternalLinkText Tests", render: () => ( {"Contact "} {" to enable it."} ), play: async ({ canvasElement }) => { const canvas = within(canvasElement); const link = canvas.getByRole("link", { name: "Open Storybook" }); await expect(link.tagName).toBe("A"); await expect(link).toHaveAttribute("href", "https://storybook.js.org/"); await expect(link).toHaveAttribute("target", "_blank"); await expect(link).toHaveAttribute("rel", "noopener noreferrer"); const disabledLink = canvas.getByRole("link", { name: "Open Storybook disabled", }); await expect(disabledLink).toHaveAttribute("aria-disabled", "true"); await expect(disabledLink).not.toHaveAttribute("href"); await expect(link.querySelector("svg")).not.toBeNull(); const noIconLink = canvas.getByRole("link", { name: "No icon" }); await expect(noIconLink.querySelector("svg")).toBeNull(); // Same font size on both sides, so equal glyph boxes mean equal baselines. const textNodeBottom = (element: Element, text: string): number => { const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT); while (walker.nextNode()) { const node = walker.currentNode; if (node.textContent === text) { const range = document.createRange(); range.selectNodeContents(node); return range.getBoundingClientRect().bottom; } } throw new Error(`Text node "${text}" not found`); }; const inlineLink = canvas.getByRole("link", { name: "support" }); const paragraph = inlineLink.closest("p"); if (!paragraph) throw new Error("Paragraph not found"); await expect( Math.abs( textNodeBottom(inlineLink, "support") - textNodeBottom(paragraph, "Contact "), ), ).toBeLessThanOrEqual(1); }, };