Skip to content

Commit c4481d1

Browse files
srividyaramasSriRamas23Copilot
authored
Agentic Card: extend contract tests and native structure checks (#4411)
* test(card): cover SPEC anatomy, overlay ownership and native structure Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b66f3ef-85ac-4405-bd35-dcc167a077b1 * test(card): preserve intrinsic native story layout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b66f3ef-85ac-4405-bd35-dcc167a077b1 * test(card): allocate native preview space for geometry checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b66f3ef-85ac-4405-bd35-dcc167a077b1 * test(card): pass explicit native window rectangle coordinates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b66f3ef-85ac-4405-bd35-dcc167a077b1 * test(Card): capture native geometry before window restoration Record exact resize request, response and read-back bounds. On failure, include bounded window, preview and section measurements while retaining the original error as cause and restoring the original window in finally. This is diagnostic evidence, not a macOS geometry fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b66f3ef-85ac-4405-bd35-dcc167a077b1 * test(Card): position the native window before enlarging it Request the primary-screen top-left so the host can use its available screen height instead of retaining the old offset during native size clamping. Keep actual bounds diagnostics, positive section geometry, reading order, existing waits and exact original-window restoration unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b66f3ef-85ac-4405-bd35-dcc167a077b1 --------- Co-authored-by: Srividya Ramaswamy <sriramas@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b66f3ef-85ac-4405-bd35-dcc167a077b1
1 parent c20d90d commit c4481d1

4 files changed

Lines changed: 175 additions & 5 deletions

File tree

‎.changeset/ready-taxis-start.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
3+
---
4+
5+

‎packages/agentic/components/src/components/card/card.stories.tsx‎

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ReactNode } from 'react';
44
import { StyleSheet, Text, View } from 'react-native';
55

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

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

147148
export default meta;
148149

149-
type Story = StoryObj<typeof Card>;
150+
type Story = WdioStory<StoryObj<typeof Card>>;
150151

151152
export const Default: Story = {};
152153

154+
export const NativeStructure: Story = {
155+
render: () => (
156+
<View style={styles.story}>
157+
<Card
158+
accessibilityLabel="Unavailable report"
159+
disabled
160+
selected
161+
layout="structured"
162+
testID="card-spec-root"
163+
header={{ accessible: true, accessibilityLabel: 'Report heading', testID: 'card-spec-header', children: <Text>Report</Text> }}
164+
content={{ accessible: true, accessibilityLabel: 'Report summary', testID: 'card-spec-content', children: <Text>Summary</Text> }}
165+
content02={{ accessible: true, accessibilityLabel: 'Report details', testID: 'card-spec-details', children: <Text>Details</Text> }}
166+
footer={{ children: <Button content="Help" testID="card-spec-help" /> }}
167+
/>
168+
</View>
169+
),
170+
parameters: {
171+
docs: {
172+
description: {
173+
story:
174+
'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.',
175+
},
176+
},
177+
},
178+
wdio: {
179+
'CRD-002 renders structured sections in reading order': async ({ browser, expect }) => {
180+
expect(browser.capabilities.setWindowRect).toBe(true);
181+
const originalWindow = await browser.getWindowRect();
182+
const requestedWindow = {
183+
x: 0,
184+
y: 0,
185+
width: Math.max(originalWindow.width, 1024),
186+
height: Math.max(originalWindow.height, 768),
187+
};
188+
let returnedWindow: typeof originalWindow | undefined;
189+
let readBackWindow: typeof originalWindow | undefined;
190+
try {
191+
// The native setter positions before resizing; the old offset left too little room above the addons panel.
192+
returnedWindow = await browser.setWindowRect(requestedWindow.x, requestedWindow.y, requestedWindow.width, requestedWindow.height);
193+
readBackWindow = await browser.getWindowRect();
194+
const header = await browser.$('~card-spec-header');
195+
const content = await browser.$('~card-spec-content');
196+
const details = await browser.$('~card-spec-details');
197+
const help = await browser.$('~card-spec-help');
198+
await browser.waitUntil(async () => (await browser.getElementRect(await header.elementId)).height > 0);
199+
const headerRect = await browser.getElementRect(await header.elementId);
200+
const contentRect = await browser.getElementRect(await content.elementId);
201+
const detailsRect = await browser.getElementRect(await details.elementId);
202+
const helpRect = await browser.getElementRect(await help.elementId);
203+
expect(headerRect.height).toBeGreaterThan(0);
204+
expect(contentRect.height).toBeGreaterThan(0);
205+
expect(detailsRect.height).toBeGreaterThan(0);
206+
expect(helpRect.height).toBeGreaterThan(0);
207+
expect(contentRect.y).toBeGreaterThanOrEqual(headerRect.y + headerRect.height);
208+
expect(detailsRect.y).toBeGreaterThanOrEqual(contentRect.y + contentRect.height);
209+
expect(helpRect.y).toBeGreaterThanOrEqual(detailsRect.y + detailsRect.height);
210+
} catch (cause) {
211+
const observe = async (read: () => Promise<unknown>) => {
212+
const observedAt = new Date().toISOString();
213+
try {
214+
return { status: 'observed', observedAt, rect: await read() };
215+
} catch (error) {
216+
return {
217+
status: 'error',
218+
observedAt,
219+
message: (error instanceof Error ? error.message : String(error)).slice(0, 500),
220+
};
221+
}
222+
};
223+
const windowAtFailure = await observe(() => browser.getWindowRect());
224+
const elements: Record<string, unknown> = {};
225+
for (const [name, selector] of [
226+
['preview', '~agentic-storybook-story-root'],
227+
['header', '~card-spec-header'],
228+
['content', '~card-spec-content'],
229+
['details', '~card-spec-details'],
230+
['help', '~card-spec-help'],
231+
]) {
232+
elements[name] = await observe(async () => {
233+
const element = await browser.$(selector);
234+
return browser.getElementRect(await element.elementId);
235+
});
236+
}
237+
const diagnostics = {
238+
phase: 'before-window-restoration',
239+
originalWindow,
240+
requestedWindow,
241+
returnedWindow: returnedWindow === undefined ? { status: 'not-returned' } : returnedWindow,
242+
readBackWindow: readBackWindow === undefined ? { status: 'not-read' } : readBackWindow,
243+
windowAtFailure,
244+
elements,
245+
};
246+
throw new Error(`${cause instanceof Error ? cause.message : String(cause)}\nCRD-002 geometry: ${JSON.stringify(diagnostics)}`, {
247+
cause,
248+
});
249+
} finally {
250+
await browser.setWindowRect(originalWindow.x, originalWindow.y, originalWindow.width, originalWindow.height);
251+
}
252+
},
253+
'CRD-005 keeps a nested action enabled inside a disabled selectable surface': async ({ browser, expect }) => {
254+
const help = await browser.$('~card-spec-help');
255+
expect(await help.getTagName()).toBe('button');
256+
await expect(help).toBeEnabled();
257+
},
258+
},
259+
};
260+
153261
export const Overview: Story = {
154262
render: () => (
155263
<View style={styles.story}>
@@ -220,7 +328,7 @@ export const Interactive: Story = {
220328
),
221329
}}
222330
layout="structured"
223-
onPress={() => undefined}
331+
onPress={(): void => undefined}
224332
selected={false}
225333
/>
226334
),
@@ -234,7 +342,7 @@ export const Selected: Story = {
234342
children: <Text style={styles.body}>Quarterly summary with selected state.</Text>,
235343
}}
236344
layout="structured"
237-
onPress={() => undefined}
345+
onPress={(): void => undefined}
238346
selected
239347
header={{
240348
children: (
@@ -292,7 +400,7 @@ export const Disabled: Story = {
292400
children: <Text style={styles.body}>This card is unavailable.</Text>,
293401
}}
294402
layout="structured"
295-
onPress={() => undefined}
403+
onPress={(): void => undefined}
296404
disabled
297405
header={{
298406
children: (
@@ -331,7 +439,7 @@ export const Horizontal: Story = {
331439
footer={{ children: <Button content="Open" /> }}
332440
header={{ children: <Text style={styles.title}>Q3 report</Text> }}
333441
layout="structured"
334-
onPress={() => undefined}
442+
onPress={(): void => undefined}
335443
size="large"
336444
/>
337445
),

‎packages/agentic/components/src/components/card/card.test.tsx‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,62 @@ function getRootStyle(component: RenderResult): ViewStyle {
2525
}
2626

2727
describe('Card', () => {
28+
it.each([
29+
[undefined, false],
30+
[false, true],
31+
[true, true],
32+
] as const)('CRD-001 derives overlay presence from selected=%s without requiring onPress', async (selected, interactive) => {
33+
const component = await renderCard({ selected, testID: 'card-root', accessibilityLabel: 'Report' });
34+
expect(getRoot(component).props.accessible).toBe(false);
35+
if (interactive) {
36+
expect(component.getByRole('button').props.accessibilityState).toEqual({ disabled: false, selected });
37+
} else {
38+
expect(component.queryByRole('button')).toBeNull();
39+
expect(getRoot(component).props.focusable).toBe(false);
40+
}
41+
});
42+
43+
it.each([
44+
['default', ['content']],
45+
['nested', ['content', 'content02']],
46+
['structured', ['header', 'content', 'content02', 'footer']],
47+
] as const)('CRD-002 renders only the %s layout slots in order after the overlay', async (layout, expected) => {
48+
const component = await renderCard({
49+
accessibilityLabel: 'Report',
50+
layout,
51+
selected: false,
52+
testID: 'card-root',
53+
header: { testID: 'header' },
54+
content: { testID: 'content' },
55+
content02: { testID: 'content02' },
56+
footer: { testID: 'footer' },
57+
});
58+
const children = getRoot(component).children as { props: { testID?: string; accessibilityRole?: string } }[];
59+
expect(children[0].props.accessibilityRole).toBe('button');
60+
expect(children.slice(1).map((child) => child.props.testID)).toEqual(expected);
61+
});
62+
63+
it('CRD-004 CRD-005 disables the overlay without disabling a nested action', async () => {
64+
const onPress = jest.fn();
65+
const onNestedPress = jest.fn();
66+
const component = await renderCard({
67+
accessibilityLabel: 'Unavailable report',
68+
disabled: true,
69+
selected: true,
70+
layout: 'structured',
71+
onPress,
72+
footer: { children: <Button content="Help" onPress={onNestedPress} /> },
73+
});
74+
const overlay = component.getByRole('button', { name: 'Unavailable report' });
75+
expect(overlay).toBeDisabled();
76+
expect(overlay.props.focusable).toBe(false);
77+
expect(overlay.props.accessibilityState.selected).toBe(true);
78+
await fireEvent.press(overlay);
79+
await fireEvent.press(component.getByRole('button', { name: 'Help' }));
80+
expect(onPress).not.toHaveBeenCalled();
81+
expect(onNestedPress).toHaveBeenCalledTimes(1);
82+
});
83+
2884
afterEach(() => {
2985
jest.restoreAllMocks();
3086
});

‎packages/agentic/components/tsconfig.stories.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"include": [
1111
"src/components/avatar/avatar.stories.tsx",
1212
"src/components/button/button.stories.tsx",
13+
"src/components/card/card.stories.tsx",
1314
"src/components/checkbox/checkbox.stories.tsx",
1415
"src/components/dropdown/dropdown.stories.tsx",
1516
"src/components/input/input.stories.tsx",

0 commit comments

Comments
 (0)