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/components-desktop-it.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fluentui-react-native/components': patch
---

Declare every component and primitive story test with `desktopIt`, so each case states the input it needs, the endpoints it runs on, and its known gaps instead of skipping at run time, and remove the internal `requireDesktopFocus` story helper.
5 changes: 5 additions & 0 deletions .changeset/desktop-driver-ci-input-lanes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fluentui-react-native/desktop-driver': patch
---

Describe the Storybook CI input lanes: the hosted Windows and Win32 jobs run the accessible lane, and an advisory Win32 job runs the physical lane.
5 changes: 5 additions & 0 deletions .changeset/storybook-desktop-channel-http-close.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fluentui-react-native/storybook-desktop': patch
---

Close the upstream Storybook HTTP listener and its open connections when the legacy channel service stops, so lingering keep-alive or reconnect sockets cannot keep the channel process alive past its shutdown budget.
5 changes: 5 additions & 0 deletions .changeset/storybook-runtime-win32-preview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fluentui-react-native/storybook-desktop-runtime': patch
---

Scroll the Win32 story preview inside its viewport and remount the preview for each navigation generation, so story tests start from a fresh, reachable preview.
52 changes: 48 additions & 4 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ jobs:
}

- name: Smoke test Windows Storybook
run: yarn storybook smoke --windows --mode stories-and-tests
run: yarn storybook smoke --windows --mode stories-and-tests --input accessible
working-directory: apps/storybook

- name: Upload Windows Storybook artifacts
Expand Down Expand Up @@ -510,7 +510,7 @@ jobs:
if-no-files-found: warn

win32-storybook:
name: Win32 Storybook PR
name: Win32 Storybook PR (accessible)
runs-on: windows-latest
timeout-minutes: 60
steps:
Expand All @@ -537,14 +537,58 @@ jobs:
disabled-input-features: keyboard,physicalClick,wheel

- name: Smoke test Win32 Storybook
run: yarn storybook smoke --win32 --mode stories-and-tests
run: yarn storybook smoke --win32 --mode stories-and-tests --input accessible
working-directory: apps/storybook

- name: Upload Win32 Storybook artifacts
if: ${{ always() }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: Storybook_win32_paper_${{ github.run_id }}_${{ github.run_attempt }}
name: Storybook_win32_paper_accessible_${{ github.run_id }}_${{ github.run_attempt }}
path: |
apps/storybook/artifacts/win32
apps/storybook/artifacts/desktop
${{ steps.desktop-driver.outputs.doctor-path }}
${{ steps.desktop-driver.outputs.cache-root }}/v1/artifacts/**/build.log
${{ steps.desktop-driver.outputs.cache-root }}/v1/diagnostics/**
if-no-files-found: warn

# An advisory pilot outside the aggregate PR check: hosted physical input is not yet qualified (#4349).
win32-storybook-physical:
name: Win32 Storybook PR (physical)
runs-on: windows-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up toolchain
uses: microsoft/react-native-test-app/.github/actions/setup-toolchain@1c99dd2947a77097bf937701e3dcc90aa109d17e # 5.4.15
with:
platform: windows
node-version: 22

- name: Install dependencies
run: yarn

- name: Build packages
run: yarn build

- name: Set up Win32 Desktop Driver
id: desktop-driver
uses: ./.github/actions/setup-desktop-driver
with:
platform: win32

- name: Smoke test Win32 Storybook
run: yarn storybook smoke --win32 --mode stories-and-tests --input physical
working-directory: apps/storybook

- name: Upload Win32 Storybook artifacts
if: ${{ always() }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: Storybook_win32_paper_physical_${{ github.run_id }}_${{ github.run_attempt }}
path: |
apps/storybook/artifacts/win32
apps/storybook/artifacts/desktop
Expand Down
6 changes: 5 additions & 1 deletion apps/storybook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ yarn storybook smoke --macos --mode stories-and-tests
```

Replace `macos` with `windows` or `win32`. Use `yarn storybook test
--<platform> --list` to discover executable story-test names. Runtime IDs and
--<platform> --list` to discover executable story-test names. Add
`--input accessible` to `test` or `smoke --mode stories-and-tests` to exclude
cases that need physical input, or `--input physical` on an unattended
machine. Accessible runs avoid pointer and keyboard injection, but may still
activate windows or change focus; keep desktop workloads exclusive. Runtime IDs and
listener ports are available through the app's `desktop-driver` command.
See `agent-map.yaml` for the current command contract and paths.

Expand Down
3 changes: 2 additions & 1 deletion apps/storybook/agent-map.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ services:
executable_tests:
command: yarn storybook test --<platform>
discovery: yarn storybook test --<platform> --list
authoring: top-level wdio callbacks or named callback collections in CSF3 stories
authoring: desktopIt cases with input, platform, and gap descriptors in CSF3 story wdio arrays
config: desktop.config.mts Storybook integration wdio settings
lifecycle: attached-session-per-test-with-isolated-node-worker
context_platform: macos-or-windows-or-win32-target-endpoint
test_filter: --test <name-glob>
input_lane: --input accessible|physical
smoke_mode: stories-and-tests
execution: navigate-and-remount-then-run-tests-grouped-by-story

Expand Down
59 changes: 42 additions & 17 deletions packages/agentic/components/references/tests-and-stories.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,23 +137,41 @@ validate.
Prefer executable WebdriverIO tests under `wdio`, typed with
`WdioStory<StoryObj<typeof Component>>` from
`@fluentui-react-native/storybook-desktop/testing` using `import type`.
The story is the suite (`describe`), and named functions are its cases (`it`):
The story is the suite (`describe`). Declare each case (`it`) with `desktopIt`
from the same entry point, adding a descriptor when the case needs host input
or applies only to some endpoints:

```tsx
export const Default: Story = {
wdio: {
'is enabled': async ({ browser, expect }) => {
wdio: [
desktopIt('is enabled', async ({ browser, expect }) => {
await expect(await browser.$('~story-button')).toBeEnabled();
},
},
}),
desktopIt('moves focus with Tab', { requires: ['physical-click', 'keyboard', 'focus'] }, async ({ browser, expect }) => {
// ...
}),
],
};
```

Each callback receives `browser`, `expect`, `platform`, `desktop`, `signal`,
and `skip`. The typed target `platform` is `macos`, `windows`, or `win32`;
Win32 is distinct from its WebDriver `platformName` of `windows`. Branch
inside callbacks for genuine platform differences and use explicit skip
reasons for unsupported `browser.capabilities['furn:features']`.
inside callbacks for genuine platform differences in one assertion. Do not
call `skip` or feature-check `browser.capabilities['furn:features']` for a
condition the descriptor can state:

- `requires` lists the host capabilities the case uses, and its physical
entries put the case in the physical input lane. Focus and keyboard cases
declare `['physical-click', 'keyboard', 'focus']`. A case that reads only
native state declares nothing and runs in both lanes. Use `click` when either
click transport proves the behavior, `physical-click` for pointer-specific
behavior, and `accessibility-click` for UIA or AX action dispatch.
- `platforms` scopes the case to endpoints, such as `['win32']` or `['!macos']`.
Split a case when endpoints need different transports rather than branching
on the click mode.
- `unsupported` records a known product gap on named endpoints with a reason,
and `quarantine` suspends a flaky case with an issue and an `expires` date.

All catalog tests use WDIO; custom `desktopDriver` plans remain only in runner
compatibility fixtures. Keep names static and callbacks self-contained. Node helpers may be
Expand All @@ -162,11 +180,13 @@ story-module bindings. The shared Babel config strips all test functions
before native dependency collection. Named cases get independent previews,
sessions, workers, deadlines, results, and failure evidence.

The single-function `wdio` form remains supported. Run with
`yarn storybook test --<platform>` and optionally `--test <name-glob>`.
`--list` discovers names without executing code. The `stories-and-tests`
Run with `yarn storybook test --<platform>`, optionally with `--test <name-glob>`
and `--input accessible|physical` to select one input lane. `--list` discovers
cases and their resolved input without executing code. The `stories-and-tests`
smoke mode runs these functions alongside remaining legacy plans, grouped by
story. See the executable test contract (`@fluentui-react-native/storybook-desktop/references/story-tests.md`).
story. See the executable test contract
(`@fluentui-react-native/storybook-desktop/references/story-tests.md`) for the
full descriptor, lane, and reporting rules.

The components package's `test:stories` project checks every test-bearing story
without emitting it into the component library. Add new test-bearing files to
Expand All @@ -191,9 +211,12 @@ timing.
### Desktop focus test scope

Keep shared requirements executable on Windows Fabric, Office Win32, and macOS.
`requireDesktopFocus` checks native capabilities, not an operating-system
allowlist. A missing capability skips the case with a reason; a platform
difference usually changes one assertion rather than skipping the entire case.
Focus cases declare `requires: ['physical-click', 'keyboard', 'focus']`, which
describes native capabilities, not an operating-system allowlist. The physical
lane runs them, the accessible lane excludes them, and a run without a lane
reports a case whose capability the host lacks as unsupported, with its reason,
instead of executing it. A platform difference
usually changes one assertion rather than excluding the entire case.

| Behavior | Scope and expected result |
| -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
Expand Down Expand Up @@ -225,13 +248,15 @@ resolved-style coverage without incidental scene rerenders, plus native visual
evidence for renderer appearance. Likewise, a checked/selected state read after
a physical click does not exercise UIA/AX action dispatch.

Report passed, failed, and skipped counts together. A zero-exit smoke traversal
with platform-skipped focus cases is not cross-platform focus qualification.
Report passed, failed, skipped, unsupported, and excluded counts together. A
zero-exit accessible-lane run excludes every focus case and is not
cross-platform focus qualification.

The macOS Fabric View does not project disabled or selected accessibility state
to AXEnabled/AXSelected (C-A11Y-005 in the
repository evidence reference `accessibility.action-transport-evidence`). Those native
announcement assertions are isolated in explicitly skipped cases; shared
announcement assertions are isolated in cases declaring
`unsupported: { platforms: ['macos'], reason }`; shared
focus, activation, and selection-callback cases still run. Do not substitute
rendered state for an AX result or describe these skips as intended macOS
semantics. Track the native projection gap and revisit the cases when the
Expand Down
12 changes: 0 additions & 12 deletions packages/agentic/components/src/common/desktopFocus.wdio.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
import type { WdioStoryContext } from '@fluentui-react-native/storybook-desktop/testing';

export function requireDesktopFocus({ browser, skip }: Pick<WdioStoryContext, 'browser' | 'skip'>): boolean {
const features = browser.capabilities['furn:features'];
if (!features) {
throw new Error('Desktop Driver did not provide feature capabilities.');
}
if (!features.physicalClick || !features.keyboard || !features.focus) {
skip('Native focus qualification requires physical pointer input, keyboard input, and focus observation.');
return false;
}
return true;
}

/** Click-to-focus is Windows-specific; ordinary AppKit controls need not take focus on click. */
export async function expectWindowsPointerFocus(
{ browser, platform }: Pick<WdioStoryContext, 'browser' | 'platform'>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ReactNode } from 'react';
import { StyleSheet, Text, View } from 'react-native';

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

import { Avatar } from './avatar';
Expand Down Expand Up @@ -108,32 +109,35 @@ export const ActivityRing: Story = {
},
},
},
wdio: {
'AVT-004 exposes the informative identity as a named native image': async ({ browser, expect }) => {
wdio: [
desktopIt('AVT-004 exposes the informative identity as a named native image', async ({ browser, expect }) => {
const avatar = await browser.$('~agentic-storybook-avatar-activity-ring');
await expect(avatar).toExist();
expect(await avatar.getTagName()).toBe('image');
expect(await avatar.getProperty('name')).toBe('Lydia Mitchelson');
},
'toggles the activity ring at every size without crashing or changing avatar bounds': async ({ browser, desktop, expect }) => {
const assert: typeof import('node:assert') = (await import('node:assert')).default;
for (const size of [16, 20, 24, 28, 32, 40, 56, 120]) {
for (const activityRing of [false, true]) {
await desktop.session.updateStoryArgs('components-avatar--activity-ring', { activityRing, size });
await expect(await browser.$('~agentic-storybook-avatar-ring-state')).toHaveText(
`Activity ring ${activityRing ? 'on' : 'off'}, size ${size}`,
);
const avatar = await browser.$('~agentic-storybook-avatar-activity-ring');
await expect(avatar).toExist();
const bounds = await browser.getElementRect(await avatar.elementId);
assert(
Math.abs(bounds.width - size) <= 1 && Math.abs(bounds.height - size) <= 1,
'The activity ring must not resize the avatar.',
);
}),
desktopIt(
'toggles the activity ring at every size without crashing or changing avatar bounds',
async ({ browser, desktop, expect }) => {
const assert: typeof import('node:assert') = (await import('node:assert')).default;
for (const size of [16, 20, 24, 28, 32, 40, 56, 120]) {
for (const activityRing of [false, true]) {
await desktop.session.updateStoryArgs('components-avatar--activity-ring', { activityRing, size });
await expect(await browser.$('~agentic-storybook-avatar-ring-state')).toHaveText(
`Activity ring ${activityRing ? 'on' : 'off'}, size ${size}`,
);
const avatar = await browser.$('~agentic-storybook-avatar-activity-ring');
await expect(avatar).toExist();
const bounds = await browser.getElementRect(await avatar.elementId);
assert(
Math.abs(bounds.width - size) <= 1 && Math.abs(bounds.height - size) <= 1,
'The activity ring must not resize the avatar.',
);
}
}
}
},
},
},
),
],
};

const styles = StyleSheet.create({
Expand Down
Loading
Loading