From 948fd0ff145f4ac700ecf1c1392d7bea629133a5 Mon Sep 17 00:00:00 2001 From: Srividya Ramaswamy Date: Mon, 26 Oct 2026 17:57:19 -0700 Subject: [PATCH 1/3] test(progress-bar): cover bounded values text priority and naming Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b66f3ef-85ac-4405-bd35-dcc167a077b1 --- .../progress-bar/progress-bar.test.tsx | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/agentic/components/src/components/progress-bar/progress-bar.test.tsx b/packages/agentic/components/src/components/progress-bar/progress-bar.test.tsx index 10ec02b91f..d312e19053 100644 --- a/packages/agentic/components/src/components/progress-bar/progress-bar.test.tsx +++ b/packages/agentic/components/src/components/progress-bar/progress-bar.test.tsx @@ -25,6 +25,56 @@ function getIndicatorStyle(component: RenderResult) { } describe('ProgressBar', () => { + it.each(['determinate', 'static'] as const)('PGB-001/PGB-004/PGB-006 clamps %s values and recomputes width after track resize', async (type) => { + for (const [progress, resolved] of [[-20, 0], [140, 100], [NaN, 0], [25.5, 25.5]]) { + const component = await renderProgressBar({ progress, type }); + expect(getIndicatorStyle(component).width).toBe(0); + expect(getRoot(component).props.accessibilityValue).toMatchObject({ min: 0, max: 100, now: resolved }); + expect(component.getByText(`${resolved}%`)).toBeOnTheScreen(); + + for (const width of [100, 240]) { + await fireEvent(getTrack(component), 'layout', { + nativeEvent: { layout: { height: 4, width, x: 0, y: 0 } }, + }); + expect(getIndicatorStyle(component).width).toBeCloseTo((width * resolved) / 100); + } + await component.unmount(); + } + }); + + it.each(['determinate', 'indeterminate', 'static'] as const)('PGB-003 gives status and explicit text precedence over %s type text', async (type) => { + for (const [status, text] of [['error', 'Progress failed'], ['success', 'Complete']] as const) { + const component = await renderProgressBar({ progress: 42, status, type }); + expect(component.getByText(text)).toBeOnTheScreen(); + + await component.rerender(); + expect(component.getByText('Caller text')).toBeOnTheScreen(); + expect(component.queryByText(text)).toBeNull(); + if (type !== 'indeterminate') { + expect(getRoot(component).props.accessibilityValue.text).toBe('Caller text'); + } + await component.unmount(); + } + }); + + it.each([ + { accessibilityLabelledBy: 'external-label' }, + { accessibilityLabelledBy: ['external-label', 'description'] }, + ])('PGB-006 appends a stable visible-label identifier to caller references %j', async ({ accessibilityLabelledBy }) => { + const original = Array.isArray(accessibilityLabelledBy) ? [...accessibilityLabelledBy] : accessibilityLabelledBy; + const component = await renderProgressBar({ accessibilityLabelledBy, label: 'Upload', progress: 10 }); + const labelId = component.getByText('Upload').props.nativeID; + const expected = [...(Array.isArray(original) ? original : [original]), labelId]; + + expect(typeof labelId).toBe('string'); + expect(labelId.length).toBeGreaterThan(0); + expect(getRoot(component).props.accessibilityLabelledBy).toEqual(expected); + expect(accessibilityLabelledBy).toEqual(original); + await component.rerender(); + expect(component.getByText('Upload').props.nativeID).toBe(labelId); + expect(getRoot(component).props.accessibilityLabelledBy).toEqual(expected); + }); + it('renders the default label, value text, and determinate accessibility state', async () => { const component = await renderProgressBar({}); const root = getRoot(component); From 93cefde0f1ca0d9f6fe337c2aa892f261cce6178 Mon Sep 17 00:00:00 2001 From: Srividya Ramaswamy Date: Mon, 26 Oct 2026 18:13:04 -0700 Subject: [PATCH 2/3] test(progress-bar): format coverage and record test-only changeset Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b66f3ef-85ac-4405-bd35-dcc167a077b1 --- .changeset/wide-camels-act.md | 5 + .../progress-bar/progress-bar.test.tsx | 108 ++++++++++-------- 2 files changed, 66 insertions(+), 47 deletions(-) create mode 100644 .changeset/wide-camels-act.md diff --git a/.changeset/wide-camels-act.md b/.changeset/wide-camels-act.md new file mode 100644 index 0000000000..064435dc68 --- /dev/null +++ b/.changeset/wide-camels-act.md @@ -0,0 +1,5 @@ +--- + +--- + + diff --git a/packages/agentic/components/src/components/progress-bar/progress-bar.test.tsx b/packages/agentic/components/src/components/progress-bar/progress-bar.test.tsx index d312e19053..5359b6771f 100644 --- a/packages/agentic/components/src/components/progress-bar/progress-bar.test.tsx +++ b/packages/agentic/components/src/components/progress-bar/progress-bar.test.tsx @@ -25,55 +25,69 @@ function getIndicatorStyle(component: RenderResult) { } describe('ProgressBar', () => { - it.each(['determinate', 'static'] as const)('PGB-001/PGB-004/PGB-006 clamps %s values and recomputes width after track resize', async (type) => { - for (const [progress, resolved] of [[-20, 0], [140, 100], [NaN, 0], [25.5, 25.5]]) { - const component = await renderProgressBar({ progress, type }); - expect(getIndicatorStyle(component).width).toBe(0); - expect(getRoot(component).props.accessibilityValue).toMatchObject({ min: 0, max: 100, now: resolved }); - expect(component.getByText(`${resolved}%`)).toBeOnTheScreen(); - - for (const width of [100, 240]) { - await fireEvent(getTrack(component), 'layout', { - nativeEvent: { layout: { height: 4, width, x: 0, y: 0 } }, - }); - expect(getIndicatorStyle(component).width).toBeCloseTo((width * resolved) / 100); + it.each(['determinate', 'static'] as const)( + 'PGB-001/PGB-004/PGB-006 clamps %s values and recomputes width after track resize', + async (type) => { + for (const [progress, resolved] of [ + [-20, 0], + [140, 100], + [NaN, 0], + [25.5, 25.5], + ]) { + const component = await renderProgressBar({ progress, type }); + expect(getIndicatorStyle(component).width).toBe(0); + expect(getRoot(component).props.accessibilityValue).toMatchObject({ min: 0, max: 100, now: resolved }); + expect(component.getByText(`${resolved}%`)).toBeOnTheScreen(); + + for (const width of [100, 240]) { + await fireEvent(getTrack(component), 'layout', { + nativeEvent: { layout: { height: 4, width, x: 0, y: 0 } }, + }); + expect(getIndicatorStyle(component).width).toBeCloseTo((width * resolved) / 100); + } + await component.unmount(); } - await component.unmount(); - } - }); - - it.each(['determinate', 'indeterminate', 'static'] as const)('PGB-003 gives status and explicit text precedence over %s type text', async (type) => { - for (const [status, text] of [['error', 'Progress failed'], ['success', 'Complete']] as const) { - const component = await renderProgressBar({ progress: 42, status, type }); - expect(component.getByText(text)).toBeOnTheScreen(); - - await component.rerender(); - expect(component.getByText('Caller text')).toBeOnTheScreen(); - expect(component.queryByText(text)).toBeNull(); - if (type !== 'indeterminate') { - expect(getRoot(component).props.accessibilityValue.text).toBe('Caller text'); + }, + ); + + it.each(['determinate', 'indeterminate', 'static'] as const)( + 'PGB-003 gives status and explicit text precedence over %s type text', + async (type) => { + for (const [status, text] of [ + ['error', 'Progress failed'], + ['success', 'Complete'], + ] as const) { + const component = await renderProgressBar({ progress: 42, status, type }); + expect(component.getByText(text)).toBeOnTheScreen(); + + await component.rerender(); + expect(component.getByText('Caller text')).toBeOnTheScreen(); + expect(component.queryByText(text)).toBeNull(); + if (type !== 'indeterminate') { + expect(getRoot(component).props.accessibilityValue.text).toBe('Caller text'); + } + await component.unmount(); } - await component.unmount(); - } - }); - - it.each([ - { accessibilityLabelledBy: 'external-label' }, - { accessibilityLabelledBy: ['external-label', 'description'] }, - ])('PGB-006 appends a stable visible-label identifier to caller references %j', async ({ accessibilityLabelledBy }) => { - const original = Array.isArray(accessibilityLabelledBy) ? [...accessibilityLabelledBy] : accessibilityLabelledBy; - const component = await renderProgressBar({ accessibilityLabelledBy, label: 'Upload', progress: 10 }); - const labelId = component.getByText('Upload').props.nativeID; - const expected = [...(Array.isArray(original) ? original : [original]), labelId]; - - expect(typeof labelId).toBe('string'); - expect(labelId.length).toBeGreaterThan(0); - expect(getRoot(component).props.accessibilityLabelledBy).toEqual(expected); - expect(accessibilityLabelledBy).toEqual(original); - await component.rerender(); - expect(component.getByText('Upload').props.nativeID).toBe(labelId); - expect(getRoot(component).props.accessibilityLabelledBy).toEqual(expected); - }); + }, + ); + + it.each([{ accessibilityLabelledBy: 'external-label' }, { accessibilityLabelledBy: ['external-label', 'description'] }])( + 'PGB-006 appends a stable visible-label identifier to caller references %j', + async ({ accessibilityLabelledBy }) => { + const original = Array.isArray(accessibilityLabelledBy) ? [...accessibilityLabelledBy] : accessibilityLabelledBy; + const component = await renderProgressBar({ accessibilityLabelledBy, label: 'Upload', progress: 10 }); + const labelId = component.getByText('Upload').props.nativeID; + const expected = [...(Array.isArray(original) ? original : [original]), labelId]; + + expect(typeof labelId).toBe('string'); + expect(labelId.length).toBeGreaterThan(0); + expect(getRoot(component).props.accessibilityLabelledBy).toEqual(expected); + expect(accessibilityLabelledBy).toEqual(original); + await component.rerender(); + expect(component.getByText('Upload').props.nativeID).toBe(labelId); + expect(getRoot(component).props.accessibilityLabelledBy).toEqual(expected); + }, + ); it('renders the default label, value text, and determinate accessibility state', async () => { const component = await renderProgressBar({}); From 822fd67733f59b42b0d1b99ba632748ddaa6bf0e Mon Sep 17 00:00:00 2001 From: Srividya Ramaswamy Date: Mon, 26 Oct 2026 18:34:21 -0700 Subject: [PATCH 3/3] test(progress-bar): cover sweep lifecycle and shared status colors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b66f3ef-85ac-4405-bd35-dcc167a077b1 --- .../progress-bar/progress-bar.test.tsx | 79 ++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/packages/agentic/components/src/components/progress-bar/progress-bar.test.tsx b/packages/agentic/components/src/components/progress-bar/progress-bar.test.tsx index 5359b6771f..35fef0f146 100644 --- a/packages/agentic/components/src/components/progress-bar/progress-bar.test.tsx +++ b/packages/agentic/components/src/components/progress-bar/progress-bar.test.tsx @@ -1,11 +1,20 @@ /** @jsxImportSource @fluentui-react-native/framework-base */ -import { StyleSheet } from 'react-native'; +import { Animated, Easing, StyleSheet } from 'react-native'; import { fireEvent } from '@testing-library/react-native'; import { render } from '../../common/renderWithTheme'; +import { useReducedMotion } from '@fluentui-react-native/framework-base'; +import { defaultFlexTokens } from '@fluentui-react-native/design/testing'; import { ProgressBar } from './progress-bar'; +jest.mock('@fluentui-react-native/framework-base', () => ({ + ...jest.requireActual('@fluentui-react-native/framework-base'), + useReducedMotion: jest.fn(), +})); + +const mockUseReducedMotion = jest.mocked(useReducedMotion); + async function renderProgressBar(props: React.ComponentProps) { return render(); } @@ -25,6 +34,74 @@ function getIndicatorStyle(component: RenderResult) { } describe('ProgressBar', () => { + beforeEach(() => { + mockUseReducedMotion.mockReturnValue(false); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it.each([ + [200, 70, 130], + [20, 16, 4], + ])( + 'PGB-004/PGB-005 gates the measured %s track sweep and stops it for reduced motion and static type', + async (width, segment, travel) => { + const animation = { start: jest.fn(), stop: jest.fn(), reset: jest.fn() }; + const loop = jest.spyOn(Animated, 'loop').mockReturnValue(animation); + const timing = jest.spyOn(Animated, 'timing'); + const setValue = jest.spyOn(Animated.Value.prototype, 'setValue'); + const component = await renderProgressBar({ type: 'indeterminate' }); + + expect(loop).not.toHaveBeenCalled(); + expect(getIndicatorStyle(component).width).toBe(0); + await fireEvent(getTrack(component), 'layout', { + nativeEvent: { layout: { height: 4, width, x: 0, y: 0 } }, + }); + expect(getIndicatorStyle(component).width).toBe(segment); + expect(animation.start).toHaveBeenCalledTimes(1); + expect(timing).toHaveBeenCalledWith( + expect.any(Animated.Value), + expect.objectContaining({ duration: 2000, easing: Easing.linear, toValue: travel, useNativeDriver: false }), + ); + + mockUseReducedMotion.mockReturnValue(true); + await component.rerender(); + expect(animation.stop).toHaveBeenCalled(); + expect(setValue).toHaveBeenLastCalledWith(0); + expect(getIndicatorStyle(component).transform).toBeUndefined(); + expect(getIndicatorStyle(component).width).toBe(segment); + expect(getRoot(component).props.accessibilityState.busy).toBe(true); + + mockUseReducedMotion.mockReturnValue(false); + await component.rerender(); + expect(loop).toHaveBeenCalledTimes(1); + expect(getIndicatorStyle(component).transform).toBeUndefined(); + expect(getIndicatorStyle(component).width).toBeCloseTo(width * 0.4); + }, + ); + + it.each([ + ['neutral', 'foregroundBrandPrimary'], + ['error', 'foregroundDangerPrimary'], + ['success', 'foregroundSuccessPrimary'], + ] as const)('PGB-007 shares the %s status color between indicator and decorative validation icon', async (status, token) => { + const component = await renderProgressBar({ + status, + showValidationIcon: true, + validationIcon: { fontSource: { codepoint: 0x2605, fontFamily: 'Arial' }, testID: 'validation-icon' }, + }); + const icon = component.getByTestId('validation-icon', { includeHiddenElements: true }); + + expect(getIndicatorStyle(component).backgroundColor).toBe(defaultFlexTokens.color[token]); + expect(StyleSheet.flatten(component.getByText('★', { includeHiddenElements: true }).props.style).color).toBe( + defaultFlexTokens.color[token], + ); + expect(icon.props.accessible).toBe(false); + expect(StyleSheet.flatten(icon.props.style)).toMatchObject({ height: 16, width: 16 }); + }); + it.each(['determinate', 'static'] as const)( 'PGB-001/PGB-004/PGB-006 clamps %s values and recomputes width after track resize', async (type) => {