Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/full-flies-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { ShadowRootHost } from '../lib/shadow-root-host';
import { requireShadowRoot } from '../test/dom-stubs';
import { globalHandlers } from '../test/mocks/handlers';
import { waitForElement, waitForShadowRoot } from '../test/storybook-dom';
import { BibleVersionPicker } from './bible-version-picker';

type PortalStrategy = 'local-inline' | 'local-top-layer';
Expand Down Expand Up @@ -110,28 +110,12 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;

async function getComponentRoot(container: ParentNode): Promise<ShadowRoot> {
return waitFor(() => requireShadowRoot(container));
}

async function waitForElement<ElementType extends Element>(
container: ParentNode,
selector: string,
errorMessage: string,
): Promise<ElementType> {
return waitFor(() => {
const element = container.querySelector<ElementType>(selector);
if (!element) throw new Error(errorMessage);
return element;
});
}

async function getTrigger(root: ShadowRoot): Promise<HTMLElement> {
return waitForElement(root, '[data-slot="popover-trigger"]', 'picker trigger not rendered');
}

async function openPicker(container: ParentNode) {
const root = await getComponentRoot(container);
const root = await waitForShadowRoot(container);
const trigger = await getTrigger(root);
void expect(root.querySelector('[data-yv-shadow-local-overlay]')).toBeNull();
await userEvent.click(trigger);
Expand Down Expand Up @@ -264,7 +248,7 @@ export const InlineControlIsClippedByItsAncestor: Story = {
'[data-testid="clipping-container"]',
'clipping container not rendered',
);
const root = await getComponentRoot(clippingContainer);
const root = await waitForShadowRoot(clippingContainer);
void expect(root.querySelector('[data-yv-shadow-inline-overlay]')).toBeNull();
await userEvent.click(await getTrigger(root));
const inlineContainer = await waitForElement<HTMLElement>(
Expand Down Expand Up @@ -520,8 +504,8 @@ export const MultiplePickersCreateIndependentLazyContainers: Story = {
const first = canvasElement.querySelector<HTMLElement>('[data-testid="first-picker"]');
const second = canvasElement.querySelector<HTMLElement>('[data-testid="second-picker"]');
if (!first || !second) throw new Error('picker harness not rendered');
const firstRoot = await getComponentRoot(first);
const secondRoot = await getComponentRoot(second);
const firstRoot = await waitForShadowRoot(first);
const secondRoot = await waitForShadowRoot(second);
void expect(firstRoot.querySelector('[data-yv-shadow-local-overlay]')).toBeNull();
void expect(secondRoot.querySelector('[data-yv-shadow-local-overlay]')).toBeNull();

Expand Down Expand Up @@ -552,7 +536,7 @@ export const MultiplePickersCreateIndependentLazyContainers: Story = {
export const MultiplePopoversShareOneIslandContainer: Story = {
render: () => <TwoPickersInOneIsland />,
play: async ({ canvasElement }) => {
const root = await getComponentRoot(canvasElement);
const root = await waitForShadowRoot(canvasElement);
const triggers = Array.from(
root.querySelectorAll<HTMLElement>('[data-slot="popover-trigger"]'),
);
Expand Down Expand Up @@ -698,7 +682,7 @@ export const SameOriginIframeTopLayerRemainsInteractive: Story = {
play: async ({ canvasElement }) => {
const iframe = canvasElement.querySelector<HTMLIFrameElement>('[data-testid="iframe"]');
if (!iframe?.contentDocument) throw new Error('same-origin iframe document not available');
const componentRoot = await getComponentRoot(iframe.contentDocument);
const componentRoot = await waitForShadowRoot(iframe.contentDocument);
const trigger = await getTrigger(componentRoot);
await userEvent.click(trigger);
const topLayer = await waitForElement<HTMLElement>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { http, HttpResponse } from 'msw';
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { expect, userEvent, waitFor } from 'storybook/test';
import { ShadowRootHost } from '../lib/shadow-root-host';
import { waitForElement } from '../test/storybook-dom';
import { Textarea } from './ui/textarea';
import { YouVersionAuthButton } from './YouVersionAuthButton';

Expand Down Expand Up @@ -35,14 +36,6 @@ function requireElement<ElementType extends Element>(
return element;
}

async function waitForElement<ElementType extends Element>(
container: ParentNode,
selector: string,
message: string,
): Promise<ElementType> {
return waitFor(() => requireElement<ElementType>(container, selector, message));
}

async function requireShadowHost(container: ParentNode): Promise<HTMLElement> {
return waitFor(() => {
const host = requireElement<HTMLElement>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { http, HttpResponse } from 'msw';
import { useCallback, useEffect, useRef, useState } from 'react';
import { expect, userEvent, waitFor } from 'storybook/test';
import { ShadowRootHost } from '../lib/shadow-root-host';
import { requireShadowRoot } from '../test/dom-stubs';
import { waitForElement, waitForShadowRoot } from '../test/storybook-dom';
import { HighlightPermissionDialog } from './highlight-permission-dialog';
import { Dialog, DialogContent, DialogTitle } from './ui/dialog';
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
Expand Down Expand Up @@ -209,20 +209,7 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;

async function waitForElement<ElementType extends Element>(
container: ParentNode,
selector: string,
message: string,
): Promise<ElementType> {
return waitFor(
() => {
const element = container.querySelector<ElementType>(selector);
if (!element) throw new Error(message);
return element;
},
{ timeout: 5_000 },
);
}
const productionWaitOptions = { timeout: 5_000 } as const;

function getPermissionDialog(topLayer: HTMLElement): HTMLElement | null {
return topLayer.querySelector<HTMLElement>(
Expand Down Expand Up @@ -314,8 +301,9 @@ async function getPrimaryHarness(canvasElement: HTMLElement): Promise<PrimaryHar
canvasElement,
'[data-testid="primary-island"]',
'primary island not rendered',
productionWaitOptions,
);
const root = await waitFor(() => requireShadowRoot(primaryIsland));
const root = await waitForShadowRoot(primaryIsland);
const contentWrapper = root.querySelector<HTMLElement>('[data-yv-shadow-content-wrapper]');
if (!contentWrapper) throw new Error('shadow content wrapper not rendered');
void expect(root.querySelector('[data-yv-shadow-local-overlay]')).toBeNull();
Expand All @@ -331,11 +319,13 @@ async function openVerseActionPopover(
root,
'[data-yv-shadow-local-overlay]',
'shadow-local top layer not created',
productionWaitOptions,
);
const versePopover = await waitForElement<HTMLElement>(
topLayer,
'[data-slot="verse-action-popover"]',
'verse action popover not rendered',
productionWaitOptions,
);
await waitFor(() => void expect(topLayer.matches(':popover-open')).toBe(true));
return { topLayer, versePopover };
Expand Down Expand Up @@ -394,21 +384,25 @@ export const PopoverOpensDialogEvidence: Story = {
root,
'[data-testid="prior-control"]',
'prior focus control not rendered',
productionWaitOptions,
);
const verse = await waitForElement<HTMLElement>(
root,
'[data-testid="verse-1"]',
'verse not rendered',
productionWaitOptions,
);
const closeAllOverlays = await waitForElement<HTMLButtonElement>(
canvasElement,
'[data-testid="close-all-overlays"]',
'close-all control not rendered',
productionWaitOptions,
);
const closeAllRequests = await waitForElement<HTMLOutputElement>(
canvasElement,
'[data-testid="close-all-requests"]',
'close-all request count not rendered',
productionWaitOptions,
);
const exitAnimationStyles: HTMLStyleElement[] = [];
let topLayer!: HTMLElement;
Expand Down Expand Up @@ -538,11 +532,13 @@ export const DialogContainsPopoverEvidence: Story = {
root,
'[data-testid="open-notes-dialog"]',
'notes opener not rendered',
productionWaitOptions,
);
const noteActionCount = await waitForElement<HTMLOutputElement>(
canvasElement,
'[data-testid="note-action-count"]',
'note action count not rendered',
productionWaitOptions,
);

let notesDialog!: HTMLElement;
Expand All @@ -555,22 +551,26 @@ export const DialogContainsPopoverEvidence: Story = {
root,
'[data-yv-shadow-local-overlay]',
'shadow-local top layer not created',
productionWaitOptions,
);
notesDialog = await waitForElement<HTMLElement>(
topLayer,
'[data-testid="notes-dialog"]',
'notes dialog not rendered',
productionWaitOptions,
);
notesTrigger = await waitForElement<HTMLButtonElement>(
notesDialog,
'[data-testid="notes-popover-trigger"]',
'notes popover trigger not rendered',
productionWaitOptions,
);
await userEvent.click(notesTrigger);
notesPopover = await waitForElement<HTMLElement>(
topLayer,
'[data-testid="notes-popover"]',
'notes popover not rendered',
productionWaitOptions,
);
const restorePosition = placeOverlayCenterUnderneath(notesDialog, notesPopover);
try {
Expand All @@ -582,6 +582,7 @@ export const DialogContainsPopoverEvidence: Story = {
notesPopover,
'[data-testid="notes-popover-action"]',
'notes popover action not rendered',
productionWaitOptions,
);
await userEvent.click(notesAction);
await waitFor(() => void expect(noteActionCount.getAttribute('data-count')).toBe('1'));
Expand Down Expand Up @@ -618,16 +619,19 @@ export const TwoIndependentOverlaysEvidence: Story = {
root,
'[data-testid="verse-1"]',
'verse not rendered',
productionWaitOptions,
);
const independentTrigger = await waitForElement<HTMLButtonElement>(
root,
'[data-testid="independent-popover-trigger"]',
'independent popover trigger not rendered',
productionWaitOptions,
);
const closeAllOverlays = await waitForElement<HTMLButtonElement>(
canvasElement,
'[data-testid="close-all-overlays"]',
'close-all control not rendered',
productionWaitOptions,
);

await step('Open a second same-root peer and tear down cleanly', async () => {
Expand All @@ -637,6 +641,7 @@ export const TwoIndependentOverlaysEvidence: Story = {
topLayer,
'[data-testid="independent-popover"]',
'independent popover not rendered',
productionWaitOptions,
);
await waitFor(() => {
const focused = root.activeElement;
Expand All @@ -655,23 +660,27 @@ export const TwoIndependentOverlaysEvidence: Story = {
canvasElement,
'[data-testid="secondary-island"]',
'secondary island not rendered',
productionWaitOptions,
);
const secondaryRoot = await waitFor(() => requireShadowRoot(secondaryIsland));
const secondaryRoot = await waitForShadowRoot(secondaryIsland);
const secondaryTrigger = await waitForElement<HTMLButtonElement>(
secondaryRoot,
'[data-testid="secondary-popover-trigger"]',
'secondary popover trigger not rendered',
productionWaitOptions,
);
await userEvent.click(secondaryTrigger);
const secondaryTopLayer = await waitForElement<HTMLElement>(
secondaryRoot,
'[data-yv-shadow-local-overlay]',
'secondary island top layer not created',
productionWaitOptions,
);
const secondaryPopover = await waitForElement<HTMLElement>(
secondaryTopLayer,
'[data-testid="secondary-popover"]',
'secondary popover not rendered',
productionWaitOptions,
);

const opened = await openVerseActionPopover(root, verse);
Expand All @@ -681,6 +690,7 @@ export const TwoIndependentOverlaysEvidence: Story = {
canvasElement,
'[data-testid="unmount-primary"]',
'primary island unmount control not rendered',
productionWaitOptions,
);
unmountPrimary.click();
await waitFor(() => void expect(primaryIsland.isConnected).toBe(false));
Expand All @@ -694,6 +704,7 @@ export const TwoIndependentOverlaysEvidence: Story = {
secondaryTopLayer,
'[data-testid="secondary-popover"]',
'secondary popover did not reopen after primary teardown',
productionWaitOptions,
);
void expect(reopenedSecondaryPopover).toHaveAttribute('data-state', 'open');
void expect(secondaryTopLayer.matches(':popover-open')).toBe(true);
Expand All @@ -711,6 +722,7 @@ export const RapidCloseReopenDuringExitEvidence: Story = {
root,
'[data-testid="run-rapid-close-reopen"]',
'rapid close/reopen control not rendered',
productionWaitOptions,
);
const exitAnimationStyle = installUnequalExitDurations(root, {
dialog: 400,
Expand All @@ -727,6 +739,7 @@ export const RapidCloseReopenDuringExitEvidence: Story = {
root,
'[data-yv-shadow-local-overlay]',
'shadow-local top layer not created',
productionWaitOptions,
);
firstDialog = await waitFor(() => {
const dialog = getPermissionDialog(topLayer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { http, HttpResponse } from 'msw';
import { useState } from 'react';
import { expect, userEvent, waitFor } from 'storybook/test';
import { ShadowRootHost } from '../lib/shadow-root-host';
import { requireShadowRoot } from '../test/dom-stubs';
import { waitForShadowRoot } from '../test/storybook-dom';
import { SignInDialog } from './sign-in-dialog';

function SignInDialogHarness(): React.ReactNode {
Expand Down Expand Up @@ -92,7 +92,7 @@ export const IsolatesFocusInertnessBackdropAndRestoration: Story = {
return { outsideControl: outside, clippingContainer: clipping };
});

const root = await waitFor(() => requireShadowRoot(canvasElement));
const root = await waitForShadowRoot(canvasElement);
const exitAnimationStyle = canvasElement.ownerDocument.createElement('style');
exitAnimationStyle.textContent = `
[role='dialog'][data-state='closed'] { animation-duration: 400ms !important; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useRef, useState } from 'react';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import i18n from '../i18n';
import { ShadowRootHost } from '../lib/shadow-root-host';
import { requireShadowRoot } from '../test/dom-stubs';
import { waitForElement, waitForShadowRoot } from '../test/storybook-dom';
import { VerseActionPopover } from './verse-action-popover';

function IsolatedVerseActionPopover(): React.ReactNode {
Expand Down Expand Up @@ -142,18 +142,6 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;

async function waitForElement<ElementType extends Element>(
container: ParentNode,
selector: string,
message: string,
): Promise<ElementType> {
return waitFor(() => {
const element = container.querySelector<ElementType>(selector);
if (!element) throw new Error(message);
return element;
});
}

async function waitForClosed(topLayer: HTMLElement): Promise<void> {
await waitFor(() => {
void expect(topLayer.querySelector('[role="dialog"]')).toBeNull();
Expand All @@ -167,7 +155,7 @@ function getCopyButton(dialog: HTMLElement): HTMLButtonElement {

export const PortalPlacementDockingReanchoringAndFocusRestoration: Story = {
play: async ({ canvasElement }) => {
const shadowRoot = await waitFor(() => requireShadowRoot(canvasElement));
const shadowRoot = await waitForShadowRoot(canvasElement);
const priorControl = await waitForElement<HTMLButtonElement>(
shadowRoot,
'[data-testid="prior-control"]',
Expand Down
Loading
Loading