diff --git a/.changeset/desktop-pipeline-logging.md b/.changeset/desktop-pipeline-logging.md new file mode 100644 index 0000000000..9d80c73842 --- /dev/null +++ b/.changeset/desktop-pipeline-logging.md @@ -0,0 +1,6 @@ +--- +"@fluentui-react-native/storybook-desktop": patch +"@fluentui-react-native/desktop-driver": patch +--- + +Capture ordered per-command stdout/stderr logs, replay full output on failure or in verbose mode, and report contextual story navigation and test failures with nested causes. diff --git a/.changeset/inline-wdio-story-tests.md b/.changeset/inline-wdio-story-tests.md new file mode 100644 index 0000000000..42ba5ee87e --- /dev/null +++ b/.changeset/inline-wdio-story-tests.md @@ -0,0 +1,7 @@ +--- +"@fluentui-react-native/storybook-desktop": minor +"@fluentui-react-native/desktop-driver": minor +"@fluentui-react-native/components": patch +--- + +Add opt-in executable WebdriverIO callbacks in React Native stories, Storybook-owned test configuration, isolated Node execution, and Button examples. diff --git a/.changeset/named-platform-wdio-tests.md b/.changeset/named-platform-wdio-tests.md new file mode 100644 index 0000000000..14c5c1f39f --- /dev/null +++ b/.changeset/named-platform-wdio-tests.md @@ -0,0 +1,7 @@ +--- +"@fluentui-react-native/storybook-desktop": minor +"@fluentui-react-native/desktop-driver": patch +"@fluentui-react-native/components": patch +--- + +Add named, individually isolated WDIO story tests with target-platform context and test-name filtering. Migrate Button's declarative desktop-driver plans to executable WebdriverIO callbacks. diff --git a/.changeset/story-grouped-desktop-tests.md b/.changeset/story-grouped-desktop-tests.md new file mode 100644 index 0000000000..aa77328e62 --- /dev/null +++ b/.changeset/story-grouped-desktop-tests.md @@ -0,0 +1,6 @@ +--- +"@fluentui-react-native/storybook-desktop": patch +"@fluentui-react-native/components": patch +--- + +Navigate to each test's story before execution, bound navigation waits, and group static plans and inline WebdriverIO callbacks by story in desktop smoke tests. diff --git a/.changeset/storybook-ci-readiness.md b/.changeset/storybook-ci-readiness.md new file mode 100644 index 0000000000..6aada88542 --- /dev/null +++ b/.changeset/storybook-ci-readiness.md @@ -0,0 +1,5 @@ +--- +'@fluentui-react-native/storybook-desktop': patch +--- + +Wait for the authenticated Storybook preview to initialize before navigating after app startup or restart. Allow the aggregate WDIO subprocess contract enough CI time without relaxing individual callback deadlines. diff --git a/.changeset/storybook-source-config-import.md b/.changeset/storybook-source-config-import.md new file mode 100644 index 0000000000..4bf44a84d8 --- /dev/null +++ b/.changeset/storybook-source-config-import.md @@ -0,0 +1,5 @@ +--- +"@fluentui-react-native/storybook-desktop": patch +--- + +Fix loading WDIO settings from Storybook's source-config fallback before compiled JavaScript exists. diff --git a/.github/skills/agentic-component-authoring/references/tests-and-stories.md b/.github/skills/agentic-component-authoring/references/tests-and-stories.md index 3c1069e2d4..5b1fff7e90 100644 --- a/.github/skills/agentic-component-authoring/references/tests-and-stories.md +++ b/.github/skills/agentic-component-authoring/references/tests-and-stories.md @@ -113,37 +113,48 @@ layout order, or native class names. Keep the initial args deterministic and add identifiers only to the small smoke set that agents and CI actively validate. -Portable desktop automation is authored inline under -`parameters.desktopDriver` and typed with `DesktopStoryTests` from -`@fluentui-react-native/desktop-driver/authoring`: +Prefer executable WebdriverIO tests under `wdio`, typed with +`WdioStory>` from +`@fluentui-react-native/storybook-desktop/testing` using `import type`. +The story is the suite (`describe`), and named functions are its cases (`it`): ```tsx export const Default: Story = { - tags: ['desktop-e2e'], - parameters: { - desktopDriver: { - version: 1, - tests: [ - { - id: 'enabled-button', - steps: [ - { action: 'wait', target: { testId: 'story-button' } }, - { expect: { state: 'enabled', target: { testId: 'story-button' }, value: true } }, - ], - }, - ], - } satisfies DesktopStoryTests, + wdio: { + 'is enabled': async ({ browser, expect }) => { + await expect(await browser.$('~story-button')).toBeEnabled(); + }, }, }; ``` -Keep the plan a static JSON literal. Storybook extracts it without importing -the React Native module, so variables, functions, spreads, computed properties, -and runtime platform branches are rejected. Express real differences with -`platforms`, `requires`, and explicit skip results. Use `testID` for actions; -use role, accessible name, state, and value assertions to verify the public -accessibility contract. Button, Checkbox, and Input defaults are the canonical -initial examples. +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']`. + +Button is the proof of concept and no longer uses custom `desktopDriver` +plans. Keep names static and callbacks self-contained. Node helpers may be +dynamically imported inside each callback, but callbacks cannot capture +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 --` and optionally `--test `. +`--list` discovers names without executing code. The `stories-and-tests` +smoke mode runs these functions alongside remaining legacy plans, grouped by +story. See the [executable test contract](../../../../packages/agentic/storybook-desktop/README.md#executable-tests-inside-stories). + +Legacy Checkbox and Input tests still use static JSON +`parameters.desktopDriver` plans typed with `DesktopStoryTests` from +`@fluentui-react-native/desktop-driver/authoring`. Keep those plans static and +use declarative `platforms` and `requires` until they are migrated; do not add +functions or dynamic values to the old format. + +The components package's `test:stories` project checks the experimental +Button stories without emitting them into the component library. Button uses focused appearance, size, shape, icon, selection, disabled, and constrained-content stories. Icon uses a source and size overview plus focused font, image, SVG, size, color, and accessibility stories. diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 831b08bbd5..71b03096ae 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -235,6 +235,7 @@ jobs: name: Storybook_macos_Dump path: | apps/storybook/artifacts/macos + apps/storybook/artifacts/storybook-commands ${{ steps.desktop-driver.outputs.doctor-path }} ios: @@ -409,6 +410,7 @@ jobs: name: Storybook_windows_Dump path: | apps/storybook/artifacts/windows + apps/storybook/artifacts/storybook-commands ${{ steps.desktop-driver.outputs.doctor-path }} ${{ steps.desktop-driver.outputs.cache-root }}/v1/artifacts/**/build.log ${{ steps.desktop-driver.outputs.cache-root }}/v1/diagnostics/** @@ -500,6 +502,7 @@ jobs: name: Storybook_win32_Dump path: | apps/storybook/artifacts/win32 + apps/storybook/artifacts/storybook-commands ${{ steps.desktop-driver.outputs.doctor-path }} ${{ steps.desktop-driver.outputs.cache-root }}/v1/artifacts/**/build.log ${{ steps.desktop-driver.outputs.cache-root }}/v1/diagnostics/** diff --git a/apps/storybook/AGENTS.md b/apps/storybook/AGENTS.md index 366136cdab..5fcb1dd14e 100644 --- a/apps/storybook/AGENTS.md +++ b/apps/storybook/AGENTS.md @@ -45,11 +45,24 @@ Read [`agent-map.yaml`](agent-map.yaml) first for the compact architecture, look commands against that listener. - macOS, Windows, and Win32 use their source-built native helpers. Keep the deterministic fake provider limited to package contract tests. -- Authored tests belong in component story `parameters.desktopDriver`, not in - this app. The app owns identity, package discovery, platform exclusions, and - generated manifests. +- Author new tests as component story `wdio` functions or named function + collections, not app-local test scripts. Button demonstrates migration from + the legacy `parameters.desktopDriver` plans, which remain supported. + The app owns identity, package discovery, platform exclusions, and manifests. +- Run executable tests with `yarn storybook test --` and optionally + `--test `. Callbacks receive the target endpoint as `platform`; + each named case gets an isolated worker/session and fresh preview. Defaults belong in + `storybook.config.mts` under `wdio`; the shared Babel config removes + callbacks from native bundles. `stories-and-tests` smoke mode runs configured + callbacks together with static plans, grouped by story. Always navigate to + the requested story and await correlated readiness before executing tests. - Keep `storybook-desktop.generated`, reports, trees, screenshots, and run manifests ignored. Never patch generated runtime identity or story manifests. +- Shared command logs merge stdout/stderr in write order under + `artifacts/storybook-commands`. Failed commands replay the complete log; + `--verbose` also replays successful commands. Preserve labeled stderr + diagnostics and nested failure causes rather than replacing them with + exit-code-only summaries. - Treat the exact-platform and portable-plan digests as contracts. A dynamic or invalid plan must fail generation rather than disappear from the manifest. - Preserve nonce-authenticated runtime hello/readiness/error messages and diff --git a/apps/storybook/README.md b/apps/storybook/README.md index 929e5af860..6daefd57ca 100644 --- a/apps/storybook/README.md +++ b/apps/storybook/README.md @@ -26,6 +26,26 @@ app-owned, while the platform-neutral UI, configuration helpers, and `storybook- from the shared package. The app exposes only the shared CLI entry points; native lifecycle scripts remain package-owned. +Button demonstrates named executable `wdio` tests instead of custom +`desktopDriver` plans. Each callback receives the target `platform` and its +own fresh preview/session/worker. Run `yarn storybook test --macos --list` to +discover the case names, or use `--test ` to filter them. With +`yarn storybook driver --macos` running and the app launched via +`yarn storybook run --macos`, run +`yarn storybook test --macos --story 'components-button--*'`. +The shared runner extracts the callbacks for Node, injects WebdriverIO and +native assertions, and leaves test dependencies out of Metro. Optional runner +defaults belong under `wdio` in `storybook.config.mts`; no separate test config +is needed. See the [authoring contract](../../packages/agentic/storybook-desktop/README.md#executable-tests-inside-stories). +`stories-and-tests` smoke mode also runs these callbacks, grouped with each +story's static plans. Both paths select the correct page before executing tests. + +Commands retain merged stdout/stderr logs under `artifacts/storybook-commands`. +Successful commands emit short summaries; failures replay their complete log +and print contextual story/test diagnostics. Use +`yarn storybook --verbose smoke --macos --mode stories-and-tests` to include +successful-command output as well. No shell redirection is required. + ## Layout ``` @@ -305,7 +325,8 @@ Run it alongside `yarn start` and `yarn storybook run --macos|--windows`. The on ## Writing stories Follow the package-level story authoring instructions in `../../packages/agentic/components/AGENTS.md`. Add a -`*.stories.tsx` file next to its component; standalone native package story globs are listed explicitly in `src/main.ts`. +`*.stories.tsx` file next to its component; package discovery belongs in `storybook.config.mts`, adapted by `src/main.ts`. See `../../packages/agentic/components/src/components/button/button.stories.tsx` for the canonical higher-order component example. -Portable tests are static `parameters.desktopDriver` data with stable `testID` -selectors; Button, Checkbox, and Input demonstrate the initial contract. +Prefer named `wdio` functions with stable `testID` selectors and injected +`platform` context, as demonstrated by Button. Checkbox and Input retain the +legacy static `parameters.desktopDriver` format during migration. diff --git a/apps/storybook/agent-map.yaml b/apps/storybook/agent-map.yaml index f9c54bc42a..27d9228d5f 100644 --- a/apps/storybook/agent-map.yaml +++ b/apps/storybook/agent-map.yaml @@ -26,6 +26,16 @@ services: instance: command: yarn storybook instance -- policy: use-reported-ports-and-target-id + executable_tests: + command: yarn storybook test -- + discovery: yarn storybook test -- --list + authoring: top-level wdio callbacks or named callback collections in CSF3 stories + config: storybook.config.mts wdio settings + lifecycle: attached-session-per-test-with-isolated-node-worker + context_platform: macos-or-windows-or-win32-target-endpoint + test_filter: --test + smoke_mode: stories-and-tests + execution: navigate-and-remount-then-run-tests-grouped-by-story agent_commands: generate_manifest: yarn storybook manifest -- @@ -81,6 +91,8 @@ validation: lifecycle: yarn storybook smoke -- --mode stories-and-tests output_hygiene: + - Commands retain merged stdout/stderr logs under artifacts/storybook-commands; failures replay grouped output. + - Use --verbose to replay successful-command logs; files update continuously while commands run. - Use structured CLI output instead of logging raw channel events. - Search large native build logs for errors and final status instead of reading them in full. - Keep warning suppression exact; console diagnostics remain available even when LogBox hides a warning. diff --git a/packages/agentic/components/AGENTS.md b/packages/agentic/components/AGENTS.md index ec8dde4746..545ba889d8 100644 --- a/packages/agentic/components/AGENTS.md +++ b/packages/agentic/components/AGENTS.md @@ -9,9 +9,17 @@ invariants; detailed authoring recipes live in the - Higher-order components live in `src/components`; read `src/components/AGENTS.md`. - Primitive components live in `src/primitives`; read `src/primitives/AGENTS.md`. - Story files are library source and follow the tests and stories reference. -- Portable desktop story tests are static `parameters.desktopDriver` plans. - Use the public authoring types, stable `testID` selectors, declarative - capability requirements, and no platform branches or executable callbacks. +- Prefer executable tests under top-level `wdio`, as one callback or a map + of named callbacks. Button is the migration proof of concept; legacy + `parameters.desktopDriver` plans remain supported for other components. + Use stable `testID` selectors and the injected target `platform` for real + platform differences, with explicit skips for unsupported capabilities. +- Executable tests are typed + with `WdioStory` from `storybook-desktop/testing` using `import type`. + Keep callbacks self-contained; dynamically import Node helpers inside + them so the shared Babel transform can exclude them from native bundles. + Each named case gets a fresh story, session, and worker; do not share native + element handles or depend on previous cases. - Storybook application, native project, Metro, bundle, or CocoaPods work follows the [Storybook instructions](../../../apps/storybook/AGENTS.md) and the `agentic-storybook-development` skill. - Native React Native Windows Fabric component work follows the @@ -42,7 +50,7 @@ invariants; detailed authoring recipes live in the - Export higher-order components and public types explicitly from `src/index.ts`; export primitives and their public types explicitly from `src/primitives/index.ts`. Never use wildcard exports. - Colocate runtime tests, type tests, and Storybook stories with the implementation. -- Keep desktop story plans inline and statically extractable; do not hide them +- Keep legacy desktop story plans inline and statically extractable; do not hide them behind variables, spreads, functions, or computed values. - Use package scripts for format, lint, build, tests, and snapshots. - Do not copy web-only APIs, CSS behavior, or DOM assumptions into React Native. diff --git a/packages/agentic/components/package.json b/packages/agentic/components/package.json index 089a38cac4..f8ef186834 100644 --- a/packages/agentic/components/package.json +++ b/packages/agentic/components/package.json @@ -35,7 +35,8 @@ "format": "fluentui-scripts format", "lint": "fluentui-scripts lint", "report:spec-source-drift": "node ./scripts/spec-contracts/report-spec-source-drift.cjs", - "test": "node ./scripts/spec-contracts/check-spec-contracts.cjs && fluentui-scripts jest", + "test": "node ./scripts/spec-contracts/check-spec-contracts.cjs && yarn test:stories && fluentui-scripts jest", + "test:stories": "tsc -p tsconfig.stories.json", "update-snapshots": "fluentui-scripts jest -u" }, "dependencies": { @@ -48,12 +49,14 @@ "@fluentui-react-native/desktop-driver": "workspace:*", "@fluentui-react-native/focus-zone": "workspace:*", "@fluentui-react-native/scripts": "workspace:*", + "@fluentui-react-native/storybook-desktop": "workspace:*", "@office-iss/react-native-win32": "^0.81.0", "@react-native-community/cli": "^20.0.0", "@react-native-community/cli-platform-android": "^20.0.0", "@react-native-community/cli-platform-ios": "^20.0.0", "@react-native/babel-preset": "^0.81.0", "@react-native/metro-config": "^0.81.0", + "@storybook/react-native": "^10.4.7", "@testing-library/react-native": "catalog:", "@types/react": "~19.1.4", "react": "19.1.4", @@ -61,6 +64,7 @@ "react-native-macos": "^0.81.0", "react-native-svg": "^15.12.1", "react-native-windows": "^0.81.0", + "storybook": "^10.4.0", "test-renderer": "catalog:" }, "peerDependencies": { diff --git a/packages/agentic/components/src/components/AGENTS.md b/packages/agentic/components/src/components/AGENTS.md index 104b6aa11d..2d9ecfba42 100644 --- a/packages/agentic/components/src/components/AGENTS.md +++ b/packages/agentic/components/src/components/AGENTS.md @@ -55,10 +55,12 @@ audit. layout. - Test both paths of a self-driving axis, and test that an externally driven `selected` does not change on press. - Keep a self-driving controlled prop out of story `args`, and keep an identity-changing axis out of story controls. -- Author desktop automation under `parameters.desktopDriver` as static data - satisfying `DesktopStoryTests`. Use one stable `testID` per interacted - element, declare capabilities, and assert public native semantics rather than - implementation structure. +- Prefer top-level `wdio` functions or named function collections demonstrated + by Button. Use the injected `platform`, explicit capability skips, stable + `testID` selectors, and public native semantics. The story supplies the suite + scope and each named case is isolated. Run with `storybook test`. +- Legacy `parameters.desktopDriver` plans remain supported. Do not put + functions in static plans or import Node helpers at story-module scope. ## Focused references diff --git a/packages/agentic/components/src/components/button/button.stories.tsx b/packages/agentic/components/src/components/button/button.stories.tsx index ea49a1937d..8eca72f44b 100644 --- a/packages/agentic/components/src/components/button/button.stories.tsx +++ b/packages/agentic/components/src/components/button/button.stories.tsx @@ -4,7 +4,7 @@ import type { ReactNode } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import type { Meta, StoryObj } from '@storybook/react-native'; -import type { DesktopStoryTests } from '@fluentui-react-native/desktop-driver/authoring'; +import type { WdioStory } from '@fluentui-react-native/storybook-desktop/testing'; import { Button } from './button'; import type { ButtonAppearance, ButtonShape, ButtonSize } from './button.types'; @@ -12,11 +12,20 @@ import type { ButtonAppearance, ButtonShape, ButtonSize } from './button.types'; type StoryGroupProps = { children: ReactNode; label: string; + labelTestID?: string; }; -const StoryGroup = ({ children, label }: StoryGroupProps) => ( +const StoryGroup = ({ children, label, labelTestID }: StoryGroupProps) => ( - {label} + {labelTestID ? ( + + + {label} + + + ) : ( + {label} + )} {children} ); @@ -74,43 +83,34 @@ const meta: Meta = { export default meta; -type Story = StoryObj; +type Story = WdioStory>; export const Default: Story = { tags: ['desktop-e2e'], - parameters: { - desktopDriver: { - version: 1, - tests: [ - { - id: 'pointer-focus', - platforms: ['windows', 'win32'], - title: 'Responds to activation and receives focus', - requires: ['element-screenshot', 'focus', 'physical-click'], - steps: [ - { action: 'wait', target: { testId: 'agentic-storybook-button' } }, - { expect: { state: 'role', target: { testId: 'agentic-storybook-button' }, value: 'button' } }, - { expect: { state: 'enabled', target: { testId: 'agentic-storybook-button' }, value: true } }, - { action: 'click', target: { testId: 'agentic-storybook-button' } }, - { expect: { state: 'focused', target: { testId: 'agentic-storybook-button' }, value: true } }, - { action: 'screenshot', name: 'button-focused', target: { testId: 'agentic-storybook-button' } }, - ], - }, - { - id: 'pointer-activation', - platforms: ['macos'], - title: 'Accepts native pointer activation', - requires: ['element-screenshot', 'physical-click'], - steps: [ - { action: 'wait', target: { testId: 'agentic-storybook-button' } }, - { expect: { state: 'role', target: { testId: 'agentic-storybook-button' }, value: 'button' } }, - { expect: { state: 'enabled', target: { testId: 'agentic-storybook-button' }, value: true } }, - { action: 'click', target: { testId: 'agentic-storybook-button' } }, - { action: 'screenshot', name: 'button-after-click', target: { testId: 'agentic-storybook-button' } }, - ], - }, - ], - } satisfies DesktopStoryTests, + wdio: { + 'exposes enabled button semantics': async ({ browser, expect }) => { + const assert: typeof import('node:assert') = (await import('node:assert')).default; + const button = await browser.$('~agentic-storybook-button'); + await expect(button).toExist(); + await expect(button).toBeEnabled(); + assert.strictEqual(await button.getTagName(), 'button'); + }, + 'supports native pointer activation': async ({ browser, expect, platform, skip }) => { + const features = browser.capabilities['furn:features']; + if (!features) throw new Error('Desktop Driver did not provide feature capabilities.'); + if (!features.physicalClick || !features.elementScreenshot || (platform !== 'macos' && !features.focus)) { + skip('This test requires physical clicks, element screenshots, and focus on Windows/Win32.'); + return; + } + const button = await browser.$('~agentic-storybook-button'); + await button.click(); + if (platform === 'windows' || platform === 'win32') { + await browser.waitUntil(async () => (await button.getProperty('focused')) === true, { + timeoutMsg: 'The activated button did not receive keyboard focus.', + }); + } + expect(await browser.takeElementScreenshot(await button.elementId)).toMatch(/^iVBORw0KGgo/); + }, }, }; @@ -243,19 +243,42 @@ export const Selected: Story = { }; export const ExternallyDrivenSelection: Story = { + wdio: { + 'starts unselected': async ({ browser, expect }) => { + await expect(await browser.$('~agentic-storybook-button-selection-state')).toHaveText('Not selected'); + }, + 'updates and resets caller-owned selection': async ({ browser, expect, skip }) => { + const features = browser.capabilities['furn:features']; + if (!features) throw new Error('Desktop Driver did not provide feature capabilities.'); + if (!features.physicalClick) { + skip('This test requires physical pointer input.'); + return; + } + const favorite = await browser.$('~agentic-storybook-button-favorite'); + const reset = await browser.$('~agentic-storybook-button-reset'); + const status = await browser.$('~agentic-storybook-button-selection-state'); + await expect(favorite).toBeEnabled(); + await expect(status).toHaveText('Not selected'); + await favorite.click(); + await expect(status).toHaveText('Selected'); + await reset.click(); + await expect(status).toHaveText('Not selected'); + }, + }, render: () => { const ToggleGroup = () => { const [selected, setSelected] = useState(false); return ( - +