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
5 changes: 5 additions & 0 deletions .changeset/ready-taxis-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---

---


118 changes: 113 additions & 5 deletions packages/agentic/components/src/components/card/card.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ReactNode } from 'react';
import { StyleSheet, Text, View } from 'react-native';

import type { Meta, StoryObj } from '@storybook/react-native';
import type { WdioStory } from '@fluentui-react-native/storybook-desktop/testing';

import { Button } from '../button/button';
import { Card } from './card';
Expand Down Expand Up @@ -146,10 +147,117 @@ const meta: Meta<typeof Card> = {

export default meta;

type Story = StoryObj<typeof Card>;
type Story = WdioStory<StoryObj<typeof Card>>;

export const Default: Story = {};

export const NativeStructure: Story = {
render: () => (
<View style={styles.story}>
<Card
accessibilityLabel="Unavailable report"
disabled
selected
layout="structured"
testID="card-spec-root"
header={{ accessible: true, accessibilityLabel: 'Report heading', testID: 'card-spec-header', children: <Text>Report</Text> }}
content={{ accessible: true, accessibilityLabel: 'Report summary', testID: 'card-spec-content', children: <Text>Summary</Text> }}
content02={{ accessible: true, accessibilityLabel: 'Report details', testID: 'card-spec-details', children: <Text>Details</Text> }}
footer={{ children: <Button content="Help" testID="card-spec-help" /> }}
/>
</View>
),
parameters: {
docs: {
description: {
story:
'The geometry check temporarily requests the primary-screen top-left and room for the native preview and addons, allowing native screen constraints before restoring the original window bounds. Failures report window, preview, and section rectangles before restoration; Card styles and strict reading-order checks remain unchanged.',
},
},
},
wdio: {
'CRD-002 renders structured sections in reading order': async ({ browser, expect }) => {
expect(browser.capabilities.setWindowRect).toBe(true);
const originalWindow = await browser.getWindowRect();
const requestedWindow = {
x: 0,
y: 0,
width: Math.max(originalWindow.width, 1024),
height: Math.max(originalWindow.height, 768),
};
let returnedWindow: typeof originalWindow | undefined;
let readBackWindow: typeof originalWindow | undefined;
try {
// The native setter positions before resizing; the old offset left too little room above the addons panel.
returnedWindow = await browser.setWindowRect(requestedWindow.x, requestedWindow.y, requestedWindow.width, requestedWindow.height);
readBackWindow = await browser.getWindowRect();
const header = await browser.$('~card-spec-header');
const content = await browser.$('~card-spec-content');
const details = await browser.$('~card-spec-details');
const help = await browser.$('~card-spec-help');
await browser.waitUntil(async () => (await browser.getElementRect(await header.elementId)).height > 0);
const headerRect = await browser.getElementRect(await header.elementId);
const contentRect = await browser.getElementRect(await content.elementId);
const detailsRect = await browser.getElementRect(await details.elementId);
const helpRect = await browser.getElementRect(await help.elementId);
expect(headerRect.height).toBeGreaterThan(0);
expect(contentRect.height).toBeGreaterThan(0);
expect(detailsRect.height).toBeGreaterThan(0);
expect(helpRect.height).toBeGreaterThan(0);
expect(contentRect.y).toBeGreaterThanOrEqual(headerRect.y + headerRect.height);
expect(detailsRect.y).toBeGreaterThanOrEqual(contentRect.y + contentRect.height);
expect(helpRect.y).toBeGreaterThanOrEqual(detailsRect.y + detailsRect.height);
} catch (cause) {
const observe = async (read: () => Promise<unknown>) => {
const observedAt = new Date().toISOString();
try {
return { status: 'observed', observedAt, rect: await read() };
} catch (error) {
return {
status: 'error',
observedAt,
message: (error instanceof Error ? error.message : String(error)).slice(0, 500),
};
}
};
const windowAtFailure = await observe(() => browser.getWindowRect());
const elements: Record<string, unknown> = {};
for (const [name, selector] of [
['preview', '~agentic-storybook-story-root'],
['header', '~card-spec-header'],
['content', '~card-spec-content'],
['details', '~card-spec-details'],
['help', '~card-spec-help'],
]) {
elements[name] = await observe(async () => {
const element = await browser.$(selector);
return browser.getElementRect(await element.elementId);
});
}
const diagnostics = {
phase: 'before-window-restoration',
originalWindow,
requestedWindow,
returnedWindow: returnedWindow === undefined ? { status: 'not-returned' } : returnedWindow,
readBackWindow: readBackWindow === undefined ? { status: 'not-read' } : readBackWindow,
windowAtFailure,
elements,
};
throw new Error(`${cause instanceof Error ? cause.message : String(cause)}\nCRD-002 geometry: ${JSON.stringify(diagnostics)}`, {
cause,
});
} finally {
await browser.setWindowRect(originalWindow.x, originalWindow.y, originalWindow.width, originalWindow.height);
}
},
'CRD-005 keeps a nested action enabled inside a disabled selectable surface': async ({ browser, expect }) => {
const help = await browser.$('~card-spec-help');
expect(await help.getTagName()).toBe('button');
await expect(help).toBeEnabled();
},
},
};

export const Overview: Story = {
render: () => (
<View style={styles.story}>
Expand Down Expand Up @@ -220,7 +328,7 @@ export const Interactive: Story = {
),
}}
layout="structured"
onPress={() => undefined}
onPress={(): void => undefined}
selected={false}
/>
),
Expand All @@ -234,7 +342,7 @@ export const Selected: Story = {
children: <Text style={styles.body}>Quarterly summary with selected state.</Text>,
}}
layout="structured"
onPress={() => undefined}
onPress={(): void => undefined}
selected
header={{
children: (
Expand Down Expand Up @@ -292,7 +400,7 @@ export const Disabled: Story = {
children: <Text style={styles.body}>This card is unavailable.</Text>,
}}
layout="structured"
onPress={() => undefined}
onPress={(): void => undefined}
disabled
header={{
children: (
Expand Down Expand Up @@ -331,7 +439,7 @@ export const Horizontal: Story = {
footer={{ children: <Button content="Open" /> }}
header={{ children: <Text style={styles.title}>Q3 report</Text> }}
layout="structured"
onPress={() => undefined}
onPress={(): void => undefined}
size="large"
/>
),
Expand Down
56 changes: 56 additions & 0 deletions packages/agentic/components/src/components/card/card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,62 @@ function getRootStyle(component: RenderResult): ViewStyle {
}

describe('Card', () => {
it.each([
[undefined, false],
[false, true],
[true, true],
] as const)('CRD-001 derives overlay presence from selected=%s without requiring onPress', async (selected, interactive) => {
const component = await renderCard({ selected, testID: 'card-root', accessibilityLabel: 'Report' });
expect(getRoot(component).props.accessible).toBe(false);
if (interactive) {
expect(component.getByRole('button').props.accessibilityState).toEqual({ disabled: false, selected });
} else {
expect(component.queryByRole('button')).toBeNull();
expect(getRoot(component).props.focusable).toBe(false);
}
});

it.each([
['default', ['content']],
['nested', ['content', 'content02']],
['structured', ['header', 'content', 'content02', 'footer']],
] as const)('CRD-002 renders only the %s layout slots in order after the overlay', async (layout, expected) => {
const component = await renderCard({
accessibilityLabel: 'Report',
layout,
selected: false,
testID: 'card-root',
header: { testID: 'header' },
content: { testID: 'content' },
content02: { testID: 'content02' },
footer: { testID: 'footer' },
});
const children = getRoot(component).children as { props: { testID?: string; accessibilityRole?: string } }[];
expect(children[0].props.accessibilityRole).toBe('button');
expect(children.slice(1).map((child) => child.props.testID)).toEqual(expected);
});

it('CRD-004 CRD-005 disables the overlay without disabling a nested action', async () => {
const onPress = jest.fn();
const onNestedPress = jest.fn();
const component = await renderCard({
accessibilityLabel: 'Unavailable report',
disabled: true,
selected: true,
layout: 'structured',
onPress,
footer: { children: <Button content="Help" onPress={onNestedPress} /> },
});
const overlay = component.getByRole('button', { name: 'Unavailable report' });
expect(overlay).toBeDisabled();
expect(overlay.props.focusable).toBe(false);
expect(overlay.props.accessibilityState.selected).toBe(true);
await fireEvent.press(overlay);
await fireEvent.press(component.getByRole('button', { name: 'Help' }));
expect(onPress).not.toHaveBeenCalled();
expect(onNestedPress).toHaveBeenCalledTimes(1);
});

afterEach(() => {
jest.restoreAllMocks();
});
Expand Down
1 change: 1 addition & 0 deletions packages/agentic/components/tsconfig.stories.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"include": [
"src/components/avatar/avatar.stories.tsx",
"src/components/button/button.stories.tsx",
"src/components/card/card.stories.tsx",
"src/components/checkbox/checkbox.stories.tsx",
"src/components/dropdown/dropdown.stories.tsx",
"src/components/input/input.stories.tsx",
Expand Down
Loading