diff --git a/.changeset/desktop-package-guidance.md b/.changeset/desktop-package-guidance.md new file mode 100644 index 00000000000..eab0559a6b6 --- /dev/null +++ b/.changeset/desktop-package-guidance.md @@ -0,0 +1,9 @@ +--- +"@fluentui-react-native/desktop-driver": patch +"@fluentui-react-native/desktop-test": patch +"@fluentui-react-native/storybook-desktop": patch +"@fluentui-react-native/storybook-desktop-runtime": patch +--- + +Add package-owned agent guidance and user entry points, and restrict published +contents to the package files required at runtime and for documentation. diff --git a/.changeset/desktop-test-macos-fixes.md b/.changeset/desktop-test-macos-fixes.md new file mode 100644 index 00000000000..0bd09f309d9 --- /dev/null +++ b/.changeset/desktop-test-macos-fixes.md @@ -0,0 +1,14 @@ +--- +'@fluentui-react-native/desktop-test': patch +'@fluentui-react-native/storybook-desktop': patch +--- + +Fix macOS desktop runs: canonicalize the reporting root so artifacts under +symlinked temporary directories stay confined, skip stop requests to producers +whose Unix-domain sockets have already half-closed, and request the canonical +desktop target that Storybook's embedded driver registers for Fabric and Paper +smoke tests. macOS RN Test App sessions without explicit identity now launch +the project's exact built product instead of any Launch Services copy sharing +its bundle identifier, and fail closed when its bundle identifier, renderer, or +embedded JavaScript bundle does not match. Console test labels number retried +attempts by ordinal rather than by WDIO's sparse attempt keys. diff --git a/.changeset/neutral-desktop-harness.md b/.changeset/neutral-desktop-harness.md new file mode 100644 index 00000000000..b047894a161 --- /dev/null +++ b/.changeset/neutral-desktop-harness.md @@ -0,0 +1,26 @@ +--- +'@fluentui-react-native/desktop-test': minor +'@fluentui-react-native/desktop-driver': minor +'@fluentui-react-native/storybook-desktop': minor +'@fluentui-react-native/tester': patch +--- + +Add an application-neutral desktop-test harness with canonical app/suite +configuration, native lifecycle ownership, standalone WDIO/Jasmine execution, +and shared event, JSON, JUnit, console, and GitHub Actions reporting. + +Make Storybook an optional integration while preserving isolated callbacks, +authenticated readiness, platform exclusions, and owned PowerShell services. +Bind native visibility/focus to existing WDIO browsers, retain delayed-render +waits, strengthen Windows attach identity, and forward trusted launch timeouts +and unpackaged working directories. + +Expose authoritative UI Automation required-form state, preserving unsupported +results on other providers. Adapt native checkbox/switch reads to checked state +without changing selected semantics or legacy Appium behavior. + +Expose the FluentTester application's canonical desktop targets for referenced +standalone suites without importing test or Storybook tooling into the app. + +Legacy desktop Appium and mobile lanes remain until native semantic parity and +multi-day reliability qualification are complete. diff --git a/.github/skills/agentic-component-authoring/SKILL.md b/.github/skills/agentic-component-authoring/SKILL.md index 69cfc808b5b..523995961a1 100644 --- a/.github/skills/agentic-component-authoring/SKILL.md +++ b/.github/skills/agentic-component-authoring/SKILL.md @@ -66,9 +66,11 @@ references used, the implementation order, and the validation plan. 7. Add focused tests and stories that exercise the public API and the resolved native output. 8. Reconcile and ratify the local contract against the realized public types, tests, stories, and platform evidence. Upstream changes require explicit re-review and never overwrite the local contract. -9. Run the smallest declared validation command while iterating. Finish with package format, lint, build, and tests; - run the Storybook bundle for story changes and the root build when public types, manifests, or project references - change. Finish with `yarn clean-lage-cache` and then `yarn lage buildci` (R-VALID-003) and report results truthfully (R-VALID-002). +9. Iterate with focused validation (R-VALID-004): `yarn test ` from the package, `yarn build` after type + changes, and `yarn test --findRelatedTests ` after editing shared code. Before handover, run the package + format, lint, build, and whole test suite, the Storybook bundle for story changes, and the root build when public + types, manifests, or project references change. Then run `yarn clean-lage-cache` and `yarn lage buildci` once as + final acceptance (R-VALID-003) and report results truthfully (R-VALID-002). Do not divide one component implementation into separate sub-agent or sub-skill phases. Its types, state, styling, and rendering form one contract and should remain in one implementation context. diff --git a/.github/skills/agentic-storybook-development/SKILL.md b/.github/skills/agentic-storybook-development/SKILL.md index de68596067a..de3e4a060aa 100644 --- a/.github/skills/agentic-storybook-development/SKILL.md +++ b/.github/skills/agentic-storybook-development/SKILL.md @@ -1,31 +1,31 @@ --- name: agentic-storybook-development -description: Develop, run, bundle, or troubleshoot the agentic-components Storybook app on macOS or Windows, including CocoaPods, Xcode, Metro, Fabric, and offline builds. +description: Develop, bundle, build, or troubleshoot the agentic-components Storybook app in apps/storybook on macOS, Windows, or Win32, including app catalog configuration, Metro, CocoaPods, native projects, and renderer builds. Not for configuring or running desktop tests or changing desktop package source. license: MIT --- -# Agentic Storybook development +# Agentic Storybook app development -Work on the native Storybook application in `apps/storybook`. +This workflow covers the `apps/storybook` consumer and its native toolchain. +For the shared Storybook integration API and CLI use +[`storybook-desktop` references](../../../packages/agentic/storybook-desktop/references/README.md); +for app identity, discovery, and diagnostics use the app's +[`agent-map.yaml`](../../../apps/storybook/agent-map.yaml) and +[`AGENTS.md`](../../../apps/storybook/AGENTS.md). -## Workflow +1. Inspect `desktop.config.mts` and the app's declared scripts; do not invent + aliases or direct native runner commands. +2. Use the package's platform recipes for prep, bundle, build, smoke, and + diagnostics. Treat Pods, workspaces, generated solutions, build directories, + and DerivedData as disposable. Fix the owning hand-authored source. +3. Reinstall/regenerate affected native dependency sources and use a clean + build after dependency or workaround changes. +4. Validate the JavaScript bundle and affected native build. Run the app's + format/lint and root validation when shared manifests or references change. +5. Finish code/configuration changes with `yarn clean-lage-cache` then + `yarn lage buildci` (R-VALID-003). -1. Read the repository `AGENTS.md`, then the compact - [agent map](../../../apps/storybook/agent-map.yaml). Read the - [Storybook instructions](../../../apps/storybook/AGENTS.md), including its native diagnostics commands, and - [README](../../../apps/storybook/README.md) only as needed for the task. -2. Inspect the Storybook `package.json` and run its declared workspace scripts; do not invent direct runner commands. -3. Use `storybook-agent` to query runtime story IDs, select stories, and sweep renderability. Reproduce the first - failure from the Storybook workspace so pnpm-linked React Native tooling resolves from the app. -4. Treat native workspaces, Pods, lockfiles, generated solutions, build directories, and DerivedData as disposable - outputs. Fix the owning manifest, Podfile, configuration, or script instead. -5. After dependency or workaround changes, regenerate the affected native dependency source and use a clean build so - stale output cannot produce a false success. - When adding a codegen-enabled workspace dependency to existing macOS Pods, the first pod install can generate its - spec without refreshing ReactCodegen's source list. If the clean build then reports a missing generated spec header, - run `pods:macos` once more before changing native configuration. -6. Validate the JavaScript bundle and the affected native build. Run Storybook format/lint and the root build when - manifests or shared package references changed. Finish with `yarn clean-lage-cache` and then `yarn lage buildci` (R-VALID-003). - -Keep component API, slot, token, and rendering work in the `agentic-component-authoring` skill; use this skill for the -Storybook application and its native toolchains. +For test configuration, helper provisioning, test runs, smoke reports, or CI +lanes, use `desktop-testing`. For package source changes, use +`maintain-desktop-testing`. Component API and assertion work stays with +`agentic-component-authoring`. diff --git a/.github/skills/desktop-testing/SKILL.md b/.github/skills/desktop-testing/SKILL.md new file mode 100644 index 00000000000..7592ad6b5eb --- /dev/null +++ b/.github/skills/desktop-testing/SKILL.md @@ -0,0 +1,37 @@ +--- +name: desktop-testing +description: Configure, run, or troubleshoot native desktop tests using desktop-test, desktop-driver, and storybook-desktop, including app targets and suites, helpers, permissions, WDIO suites, Storybook story tests and smoke runs, reports, and CI. Not for package source changes or the Storybook app build toolchain. +license: MIT +--- + +# Desktop testing + +Package references own commands, schemas, and troubleshooting. Read only the +matching topic; do not copy its content here. + +| Task | Read | +| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Add an app target or suite | [`desktop-test` configuration](../../../packages/agentic/desktop-test/references/configuration.md) | +| Run stages, suites, or inspect results | [`desktop-test` commands](../../../packages/agentic/desktop-test/references/commands.md) and [reporting](../../../packages/agentic/desktop-test/references/reporting.md) | +| Prepare a platform or provision a helper | [`desktop-test` platforms](../../../packages/agentic/desktop-test/references/platforms.md) and [`desktop-driver` helpers](../../../packages/agentic/desktop-driver/references/native-helpers.md) | +| Integrate Storybook or run callbacks | [`storybook-desktop` integration](../../../packages/agentic/storybook-desktop/references/integration.md), [commands](../../../packages/agentic/storybook-desktop/references/commands.md), and [tests](../../../packages/agentic/storybook-desktop/references/story-tests.md) | +| Fix E2E adapters or diagnose failures | [`desktop-test` WDIO suites](../../../packages/agentic/desktop-test/references/wdio-suites.md) and [lifecycle](../../../packages/agentic/desktop-test/references/lifecycle.md) | + +## Workflow + +1. Identify the consumer workspace; read its config and declared scripts. For + `apps/storybook`, also read `AGENTS.md` and `agent-map.yaml`. +2. Reproduce with the narrowest declared stage, suite, story, or filter. + Provision the verified helper explicitly; never guess flags or call runner + internals. +3. Do not change privacy settings without authorization or overlap interactive + desktop workloads. +4. Report failures, skips, unsupported capabilities, and incomplete work + distinctly. Skips are not passes. +5. If a published package changed, add its changeset. Finish code/config + changes with `yarn clean-lage-cache` then `yarn lage buildci`. + +For package source changes or reviews, use `maintain-desktop-testing`. For the +`apps/storybook` build/toolchain, use `agentic-storybook-development`. +Component story assertions stay with `agentic-component-authoring`; v0/v1 +FluentTester assertions stay with `maintain-v0-v1-component`. diff --git a/.github/skills/implement-native-component/SKILL.md b/.github/skills/implement-native-component/SKILL.md index 3e9beaa6941..0fe06089204 100644 --- a/.github/skills/implement-native-component/SKILL.md +++ b/.github/skills/implement-native-component/SKILL.md @@ -37,7 +37,8 @@ ownership and qualification rules (R-NATIVE-001 through R-NATIVE-003). 4. Implement with the TypeScript specification, codegen configuration, native props, events, commands, and registration kept consistent. Preserve public APIs and registration names (R-NATIVE-002) and keep fork imports in platform files (R-REPO-002). -5. Validate the affected boundary with declared scripts (R-VALID-001): package build, test, and lint; native-core +5. Validate the affected boundary with declared scripts (R-VALID-001), running focused package tests while iterating + (R-VALID-004): package build, test, and lint; native-core `codegen:windows` when Windows specs change; native builds through the owning app, such as `yarn storybook build --macos` or `--windows` from `apps/storybook`; and runtime evidence through Storybook smoke or `wdio` story tests. macOS Storybook commands default to Fabric: add `--paper` to `prep`, `build`, `run`, or `smoke` diff --git a/.github/skills/maintain-desktop-testing/SKILL.md b/.github/skills/maintain-desktop-testing/SKILL.md new file mode 100644 index 00000000000..5c16d62d393 --- /dev/null +++ b/.github/skills/maintain-desktop-testing/SKILL.md @@ -0,0 +1,45 @@ +--- +name: maintain-desktop-testing +description: Change, review, or plan source changes to desktop-driver, desktop-test, storybook-desktop, or storybook-desktop-runtime, including native helpers. Applies package invariants, documentation, native and consumer validation, and changesets. +license: MIT +--- + +# Maintain desktop testing packages + +| Owner | Entry | +| --------------------------------------------- | ---------------------------------------------------------------------------- | +| WebDriver service, clients, helpers | [`desktop-driver`](../../../packages/agentic/desktop-driver/AGENTS.md) | +| App/suite config, lifecycle, WDIO, reporting | [`desktop-test`](../../../packages/agentic/desktop-test/AGENTS.md) | +| Storybook integration, CLI, extraction, tests | [`storybook-desktop`](../../../packages/agentic/storybook-desktop/AGENTS.md) | +| On-device bridge, Metro, theme, chrome | [`runtime`](../../../packages/agentic/storybook-desktop-runtime/AGENTS.md) | + +Read the matching contributing reference from the package index. + +## Mode first + +- **Review/plan:** read-only. Do not implement, run write-mode formatting, + build, create changesets, launch apps, or run native contracts. Report + validation not run. +- **Implementation:** continue below. + +## Implementation + +1. Change the lowest package that owns the behavior; update dependent + references when needed. Preserve exact identity, fail-closed behavior, + cancellation, ownership-safe cleanup, and explicit unsupported/skip status. + Contract fixtures that spawn Node or generate specs pass values through + arguments, not spliced source (R-SCAN-002). +2. For standalone Swift/C++ helpers, read the provider README and + `native/PROTOCOL.md`; keep protocol schema and docs in sync. Apply + `implement-native-component` as a native overlay. React-module and Office + bridging-header steps do not apply to these standalone helpers. +3. Update owning references and affected README/AGENTS files in the same + change. +4. Iterate with `yarn test ` for the files you change (R-VALID-004), then run package format, lint, build, and + test. Native changes also need the matching-OS helper contract and affected + consumer lane; JS tests do not qualify native behavior. +5. Add a patch changeset for each changed published package. Finish with + `yarn clean-lage-cache` then `yarn lage buildci`. + +When delegating, pass the package AGENTS.md and selected reference paths; +subagents do not inherit them automatically. diff --git a/.github/skills/maintain-v0-v1-component/SKILL.md b/.github/skills/maintain-v0-v1-component/SKILL.md index 07ea03fcc01..67127a3f609 100644 --- a/.github/skills/maintain-v0-v1-component/SKILL.md +++ b/.github/skills/maintain-v0-v1-component/SKILL.md @@ -30,9 +30,10 @@ v0 and v1 are maintenance-only (R-V0-001 and R-V1-001). A request for a new comp editing. 5. Make the smallest change that fixes the problem. Preserve public props, types, exports and aliases, display names, `componentRef`, token contracts, and native registration names (R-V0-002, R-V0-003, R-V1-002, R-V1-003). -6. Validate with the owning package's declared scripts (R-VALID-001): build, test, and lint, plus the framework - packages named in the recipe when framework semantics change. Review snapshot diffs before accepting them. Report - each check's outcome (R-VALID-002), and finish with `yarn clean-lage-cache` and then `yarn lage buildci` (R-VALID-003). +6. Validate with the owning package's declared scripts (R-VALID-001), narrowest first (R-VALID-004): `yarn test ` + while iterating, then the package's build, test, and lint, plus the framework packages named in the recipe when + framework semantics change. Review snapshot diffs before accepting them. Report each check's outcome (R-VALID-002), + and finish with one `yarn clean-lage-cache` and then `yarn lage buildci` (R-VALID-003). 7. Add a changeset for published packages (R-REPO-003). Stop and confirm scope before migrating between frameworks, creating a component, or changing a public API. Those diff --git a/.github/skills/review-component/SKILL.md b/.github/skills/review-component/SKILL.md index 40c6a91044a..8c13d635970 100644 --- a/.github/skills/review-component/SKILL.md +++ b/.github/skills/review-component/SKILL.md @@ -32,9 +32,11 @@ license: MIT | Framework-base pattern | [Framework-base instructions](../../../packages/framework-base/AGENTS.md) | 4. Check repository-wide rules: explicit exports (R-REPO-001), platform fork imports (R-REPO-002), changesets - (R-REPO-003), and no copied private source (R-REPO-004). -5. Inspect validation claims. Run non-mutating checks only when they add evidence; never run snapshot-update or fix - modes. Record each check as passed, failed, skipped, not run, or unsupported (R-VALID-002). + (R-REPO-003), no copied private source (R-REPO-004), and the code-scanning patterns: linear-time regular expressions + and no values spliced into executed code (R-SCAN-001, R-SCAN-002). +5. Inspect validation claims. Run non-mutating checks only when they add evidence, preferring focused tests over the + whole suite (R-VALID-004); never run snapshot-update or fix modes. Record each check as passed, failed, skipped, not + run, or unsupported (R-VALID-002). ## Output diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index f479f0396f6..e3b37a0eab5 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -238,10 +238,10 @@ jobs: if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: Storybook_macos_Dump + name: Storybook_macos_fabric_${{ github.run_id }}_${{ github.run_attempt }} path: | apps/storybook/artifacts/macos - apps/storybook/artifacts/storybook-commands + apps/storybook/artifacts/desktop ${{ steps.desktop-driver.outputs.doctor-path }} macos-paper-storybook: @@ -289,10 +289,10 @@ jobs: if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: Storybook_macos_Paper_Dump + name: Storybook_macos_paper_${{ github.run_id }}_${{ github.run_attempt }} path: | apps/storybook/artifacts/macos - apps/storybook/artifacts/storybook-commands + apps/storybook/artifacts/desktop ${{ steps.desktop-driver.outputs.doctor-path }} ios: @@ -464,10 +464,10 @@ jobs: if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: Storybook_windows_Dump + name: Storybook_windows_fabric_${{ github.run_id }}_${{ github.run_attempt }} path: | apps/storybook/artifacts/windows - apps/storybook/artifacts/storybook-commands + 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/** @@ -556,10 +556,10 @@ jobs: if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: Storybook_win32_Dump + name: Storybook_win32_paper_${{ github.run_id }}_${{ github.run_attempt }} path: | apps/storybook/artifacts/win32 - apps/storybook/artifacts/storybook-commands + 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/** diff --git a/.gitignore b/.gitignore index d0ff6b78de4..48a56a740a7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ yarn-error.log* **/errorShots/ **/allure-results/ **/allure-report/ +**/artifacts/desktop/ # Runtime data pids diff --git a/.yarn/patches/react-native-macos-npm-0.81.9-aa583a0648.patch b/.yarn/patches/react-native-macos-npm-0.81.9-aa583a0648.patch index 19175c68900..09efd729693 100644 --- a/.yarn/patches/react-native-macos-npm-0.81.9-aa583a0648.patch +++ b/.yarn/patches/react-native-macos-npm-0.81.9-aa583a0648.patch @@ -53,6 +53,26 @@ index 09822246f92767208a7d2ad561d4b2954c0fef33..6e5fce575e507ed0c3e3f6794115dd76 @"timer": NSAccessibilityStaticTextRole, // no exact match on macOS @"toolbar": NSAccessibilityToolbarRole, // Roles/traits that are macOS specific and are used by some of the core components (Lists): +diff --git a/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm b/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +index 804eec8079a01f7580d0d6e7b4c2ede4a4a61c7f..a6d967119c4847be709c5a73b518862f781f7634 100644 +--- a/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm ++++ b/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +@@ -344,10 +344,15 @@ static void RCTCancelTouchesForView(RCTPlatformView *view) + + - (BOOL)isAccessibilityElement + { ++#if TARGET_OS_OSX // [macOS ++ // macOS has no `accessibilityElements` provider, so an accessible paragraph exposes itself as static text. ++ return static_cast(*_props).accessible; ++#else // macOS] + // All accessibility functionality of the component is implemented in `accessibilityElements` method below. + // Hence to avoid calling all other methods from `UIAccessibilityContainer` protocol (most of them have default + // implementations), we return here `NO`. + return NO; ++#endif // [macOS] + } + + #if !TARGET_OS_OSX // [macOS] diff --git a/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm index e7254907271b70783e886d378c7824df46e04e93..fbe1d5d7e834afb4553adb03253e34f6b9ce69d7 100644 --- a/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm diff --git a/AGENTS.md b/AGENTS.md index 2a4d2268713..2b2aaa202c1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,8 +6,9 @@ routes each task to the workflow and references it needs; load only what the tas ## How guidance is organized -- **`AGENTS.md` files** hold safeguards and local invariants. Copilot CLI loads this file plus the `AGENTS.md` in the - working directory and each ancestor. From the repository root, read the scoped files below when a task touches them. +- **`AGENTS.md` files** hold safeguards and local invariants. Copilot CLI loads applicable files from the repository, + working directory, ancestors, and directories in the path of files being worked on. Read the scoped entries below + when a task touches them. - **Skills** in [`.github/skills`](.github/skills) define task workflows. - **[Shared references](references/agents/README.md)** in `references/agents/` hold reusable recipes and repository knowledge, indexed by task. @@ -26,11 +27,13 @@ These apply to every task. authorization (R-PERM-007). Read the [permissions reference](references/agents/repository/permissions.md) before using `gh api`, Pod, process, loopback `curl`, or capture operations, or changing the policy. - **Validation:** run the owning workspace's declared scripts (R-VALID-001). Report each check as passed, failed, - skipped, not run, or unsupported; JavaScript tests and bundles are not native qualification (R-VALID-002). Finish - code or configuration changes with `yarn clean-lage-cache` and then `yarn lage buildci` (R-VALID-003). + skipped, not run, or unsupported; JavaScript tests and bundles are not native qualification (R-VALID-002). Iterate + with focused tests, such as `yarn test ` in the package, and finish code or configuration changes with one + `yarn clean-lage-cache` and then `yarn lage buildci` (R-VALID-003, R-VALID-004). - **Code:** no barrel exports (R-REPO-001); keep React Native fork imports in platform files (R-REPO-002); add a changeset for published packages (R-REPO-003); never copy private source bodies into this public repository - (R-REPO-004). + (R-REPO-004). Keep regular expressions linear-time and never splice values into code that runs, tests included + (R-SCAN-001, R-SCAN-002; see [code scanning](references/agents/repository/code-scanning.md)). - **Component generations:** v0 and v1 are maintenance-only; new components use the agentic workflow (R-V0-001, R-V1-001). Classify code by imports and structure, not by path (R-CLASSIFY-001). - **Native boundary:** before changing Swift, Objective-C, C++, headers, podspecs, Windows projects, codegen specs, @@ -51,32 +54,40 @@ Classify the change with the [classification guide](references/agents/components | Review a component change of any generation | [review-component](.github/skills/review-component/SKILL.md) | | Native component, TurboModule, service, codegen, or a native-core `/legacy` wrapper | [implement-native-component](.github/skills/implement-native-component/SKILL.md) | | Cross-component agentic extraction or dependency direction | [agentic-component-optimization](.github/skills/agentic-component-optimization/SKILL.md) | -| Storybook app, Metro, CocoaPods, or desktop native toolchain | [agentic-storybook-development](.github/skills/agentic-storybook-development/SKILL.md) | +| Desktop test configuration, execution, reporting, helpers, or CI | [desktop-testing](.github/skills/desktop-testing/SKILL.md) | +| Change or review the desktop testing package source | [maintain-desktop-testing](.github/skills/maintain-desktop-testing/SKILL.md) | +| Storybook app catalog, Metro, CocoaPods, or native builds | [agentic-storybook-development](.github/skills/agentic-storybook-development/SKILL.md) | | Dependency vulnerability remediation | [fix-security-issues](.github/skills/fix-security-issues/SKILL.md) | Other repository work starts from the [reference index](references/agents/README.md): architecture, commands and validation, TypeScript, release, design tokens, focus, and native guidance. +For package-owned knowledge, follow the +[package guidance standard](references/agents/repository/package-guidance.md). ## Scoped instructions -| Path | Scope | -| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | -| [packages/agentic/components](packages/agentic/components/AGENTS.md) | Agentic package invariants, contracts, and story tests | -| [packages/agentic/components/src](packages/agentic/components/src/AGENTS.md) | Source boundaries and extraction | -| [.../src/components](packages/agentic/components/src/components/AGENTS.md) | Higher-order components and the focus visual policy | -| [.../src/primitives](packages/agentic/components/src/primitives/AGENTS.md) | Public primitives | -| [packages/agentic/desktop-driver](packages/agentic/desktop-driver/AGENTS.md) | Desktop WebDriver driver | -| [packages/framework-base](packages/framework-base/AGENTS.md) | Shared component patterns | -| [packages/framework-base/src/accessibility](packages/framework-base/src/accessibility/AGENTS.md) | Accessibility helpers and native action qualification | -| [packages/framework/framework](packages/framework/framework/AGENTS.md) | v1 framework package | -| [packages/deprecated/foundation-compose](packages/deprecated/foundation-compose/AGENTS.md) | v0 framework package | -| [apps/storybook](apps/storybook/AGENTS.md) | Storybook app, native workflows, and diagnostics | +| Path | Scope | +| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | +| [packages/agentic/components](packages/agentic/components/AGENTS.md) | Agentic package invariants, contracts, and story tests | +| [packages/agentic/components/src](packages/agentic/components/src/AGENTS.md) | Source boundaries and extraction | +| [.../src/components](packages/agentic/components/src/components/AGENTS.md) | Higher-order components and the focus visual policy | +| [.../src/primitives](packages/agentic/components/src/primitives/AGENTS.md) | Public primitives | +| [packages/agentic/desktop-driver](packages/agentic/desktop-driver/AGENTS.md) | Desktop WebDriver driver | +| [packages/agentic/desktop-test](packages/agentic/desktop-test/AGENTS.md) | Application-neutral desktop test harness | +| [packages/agentic/storybook-desktop](packages/agentic/storybook-desktop/AGENTS.md) | Storybook desktop integration | +| [packages/agentic/storybook-desktop-runtime](packages/agentic/storybook-desktop-runtime/AGENTS.md) | On-device Storybook runtime | +| [packages/framework-base](packages/framework-base/AGENTS.md) | Shared component patterns | +| [packages/framework-base/src/accessibility](packages/framework-base/src/accessibility/AGENTS.md) | Accessibility helpers and native action qualification | +| [packages/framework/framework](packages/framework/framework/AGENTS.md) | v1 framework package | +| [packages/deprecated/foundation-compose](packages/deprecated/foundation-compose/AGENTS.md) | v0 framework package | +| [apps/storybook](apps/storybook/AGENTS.md) | Storybook app, native workflows, and diagnostics | ## Quick facts - Yarn 4 in pnpm mode; TypeScript 7.0.2 compiled as one `tsc -b` project-references graph; Lage task orchestration. New packages must be added to the root `tsconfig.json` `references`. -- `yarn build` builds the repository; `yarn lage test` and `yarn lage lint` test and lint packages; `yarn format` formats. +- `yarn build` builds the repository; `yarn lage test` and `yarn lage lint` test and lint packages, and `yarn test ` + in a package runs only the matching test files; `yarn format` formats. `yarn lage buildci` runs the CI aggregate, including `yarn check-agent-instructions`. `yarn clean-lage-cache` clears the Lage cache before final validation. - Details: [commands and validation](references/agents/repository/development.md), diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ac4c660f1d6..8112f311799 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,6 +13,11 @@ This guide assumes you: There are some specific quirks to this repository that one should be familiar with. +When configuring or maintaining a package, start with its `AGENTS.md` and +`README.md`; package-owned recipes and shared agent guidance live in its +indexed `references/` directory. Follow the +[package-owned guidance standard](./references/agents/repository/package-guidance.md). + ### Component generations The repository contains three generations of components. Identify the generation from a component's imports and @@ -84,7 +89,8 @@ types explicitly from `src/index.ts`, or from `src/primitives/index.ts` for a pr 1. Add Storybook stories next to the component. Desktop behavior tests are `wdio` functions in the stories and run on macOS, Windows, and Win32 through the [agentic Storybook app](./apps/storybook/README.md). 1. Run the package's declared scripts, such as `yarn workspace @fluentui-react-native/components test`, plus `build`, - `lint`, and `format`. + `lint`, and `format`. While iterating, run only your component's tests from its package, for example + `yarn test button`. 1. Before opening a pull request, run `yarn clean-lage-cache` and then `yarn lage buildci` from the repository root. ### Maintaining v0 and v1 components diff --git a/apps/E2E/README.md b/apps/E2E/README.md index fde98703f86..1703182121c 100644 --- a/apps/E2E/README.md +++ b/apps/E2E/README.md @@ -1,5 +1,77 @@ # E2E Testing Overview +## Native desktop migration + +`desktop.config.mts` adds the application-neutral +[`desktop-test`](../../packages/agentic/desktop-test/README.md) harness alongside +the existing Appium runners. It references canonical app targets in +`apps/fluent-tester` and `apps/win32`; applications do not import E2E suites or +install Storybook. + +From the repository root, after the normal repository build: + +```sh +yarn workspace @fluentui-react-native/tester-win32 desktop-test --target win32 build-driver +yarn workspace @fluentui-react-native/tester-win32 desktop-test --target win32 bundle +yarn workspace @fluentui-react-native/e2e-testing desktop-test --target win32 test --suite pilot --list +yarn workspace @fluentui-react-native/e2e-testing desktop-test --target win32 test --suite pilot +``` + +The pilot deliberately starts small: Pressable navigation on Windows/Win32 and +TextLegacy on macOS. It is not the behavioral parity gate. `legacy` retains the +exact grouped selection shared with the old configs in `desktop-specs.mts`: +31 macOS files, 40 Win32 files, and the existing Windows allowlist. That allowlist +matches ten files and still contains two unmatched patterns +(`TabsLegacy/specs/*.windows.ts` and `Tokens/specs/*.win.ts`). Native discovery +fails on those patterns rather than silently omitting them. + +`--suite behavioral-pilot` selects the unchanged Win32 Button, Checkbox, +Switch, directional FocusZone, and Callout specs in one group. An interactive +Win32 run on October 22, 2026 (UTC) passes all 37 original assertions with no +skips, retries, or infrastructure failures, including keyboard callbacks, +checked/required state, focus navigation, scrolling-dependent page navigation, +and popup opening. This probe is not the complete legacy baseline or an +old/new comparative run. It does not enable restricted input or waive +unsupported assertions. + +On macOS, bundle before `prep`, because CocoaPods embeds only resources that exist +during `pod install`. Add `--paper` to `prep` and `test` for Paper: + +```sh +yarn workspace @fluentui-react-native/tester desktop-test --target macos build-driver +yarn workspace @fluentui-react-native/tester desktop-test --target macos bundle +yarn workspace @fluentui-react-native/tester desktop-test --target macos prep +yarn workspace @fluentui-react-native/tester desktop-test --target macos build +yarn workspace @fluentui-react-native/e2e-testing desktop-test --target macos test --suite legacy +``` + +On October 22, 2026 (UTC), the macOS pilot and all 62 `legacy` tests pass on +both Fabric and Paper, with no skips or retries. Fabric depends on the +repository's RNmacOS Text accessibility patch, described in the +[native-core README](../../packages/agentic/native-core/README.md#repository-local-rnmacos-prerequisite-patch). +Selected tester buttons are scrolled until their center is inside the list +before clicking, because the native click targets that point. + +Jasmine globals, hooks, matchers, retries, and grouped specs remain WDIO-owned. +The shared page adapter maps only established native attributes, uses checked +state for toggles, explicit key actions for chords, and native wheel input for +desktop scrolling. Required state maps to UI Automation's authoritative +`IsRequiredForForm` property; providers without it fail explicitly. `HelpText` +and toggle-pattern availability remain unmapped. Native setup retains initial +window bounds: it does **not** emulate the old maximize hook. Popup lookup +remains scoped to the selected window. + +Reports, original failures, and target-scoped evidence live under +`artifacts/desktop/`. Native helpers must be built/provisioned explicitly. +Windows additionally requires a built, registered RN Test App with a verified +generated renderer; macOS requires its own built application and permissions. + +Keep all legacy desktop and mobile lanes until per-test assertion mapping, +equivalent-build comparisons, and at least 100 representative runs per endpoint +over 14 days demonstrate at least 99% infrastructure success and zero process +or input leaks. Hosted input-disabled checks do not qualify physical input. +The remaining sections describe the retained Appium workflow. + **There is currently an Appium issue with NPM v8.19.2. Please use an older version of NPM** ## Win32/UWP Prerequisites diff --git a/apps/E2E/desktop-adapters.test.cjs b/apps/E2E/desktop-adapters.test.cjs new file mode 100644 index 00000000000..b5785534b7a --- /dev/null +++ b/apps/E2E/desktop-adapters.test.cjs @@ -0,0 +1,86 @@ +const assert = require('node:assert/strict'); +const { afterEach, test } = require('node:test'); +const { BasePage } = require('./lib/common/BasePage.js'); +const { Keys } = require('./lib/common/consts.js'); +const { installLegacyDesktopAttributes, sendNativeDesktopKeys } = require('./lib/common/nativeDesktop.js'); + +afterEach(() => { + delete globalThis.browser; +}); + +test('native checkbox and switch state never falls back to selected', async () => { + const page = new BasePage(); + const element = { elementId: 'toggle', isSelected: () => assert.fail('checked is not selected') }; + for (const value of [false, true, 'mixed', null, undefined]) { + globalThis.browser = { + capabilities: { browserName: 'furn-native-desktop' }, + async getElementProperty(id, property) { + assert.equal(id, 'toggle'); + assert.equal(property, 'checked'); + return value; + }, + }; + if (typeof value === 'boolean') assert.equal(await page.isChecked(element), value); + else await assert.rejects(page.isChecked(element), /native boolean checked state/); + } +}); + +test('legacy runners retain their selected-based toggle behavior', async () => { + globalThis.browser = { capabilities: { browserName: 'legacy-appium' } }; + const page = new BasePage(); + for (const value of [false, true]) assert.equal(await page.isChecked({ isSelected: async () => value }), value); +}); + +test('legacy required-form attributes use the authoritative native required property', async () => { + let attribute; + globalThis.browser = { + overwriteCommand(name, callback, element) { + assert.equal(name, 'getAttribute'); + assert.equal(element, true); + attribute = callback; + }, + }; + installLegacyDesktopAttributes(); + for (const value of [false, true, null, undefined, 'False']) { + const element = { + elementId: 'field', + async getElementProperty(id, property) { + assert.equal(id, 'field'); + assert.equal(property, 'required'); + return value; + }, + }; + const result = attribute.call(element, undefined, 'IsRequiredForForm'); + if (typeof value === 'boolean') assert.equal(await result, value ? 'True' : 'False'); + else await assert.rejects(result, /required-form state is unavailable/); + } +}); + +test('native key chords release held input even when dispatch fails', async () => { + const actions = [ + { type: 'keyDown', value: Keys.CONTROL }, + { type: 'keyDown', value: Keys.ENTER }, + { type: 'keyUp', value: Keys.ENTER }, + { type: 'keyUp', value: Keys.CONTROL }, + ]; + for (const fail of [false, true]) { + let released = false; + globalThis.browser = { + async elementSendKeys(id, value) { + assert.equal(id, 'button'); + assert.equal(value, ''); + }, + async performActions(sources) { + assert.deepEqual(sources, [{ type: 'key', id: 'tester-keyboard', actions }]); + if (fail) throw new Error('input dispatch failed'); + }, + async releaseActions() { + released = true; + }, + }; + const dispatch = sendNativeDesktopKeys({ elementId: 'button' }, [Keys.CONTROL, Keys.ENTER, Keys.NULL]); + if (fail) await assert.rejects(dispatch, /input dispatch failed/); + else await dispatch; + assert.equal(released, true); + } +}); diff --git a/apps/E2E/desktop-specs.mts b/apps/E2E/desktop-specs.mts new file mode 100644 index 00000000000..b23c4efdb66 --- /dev/null +++ b/apps/E2E/desktop-specs.mts @@ -0,0 +1,20 @@ +export const legacyDesktopSpecs = { + macos: [['src/**/specs/*.macos.ts']], + windows: [ + [ + 'src/ActivityIndicator/specs/*.win.ts', + 'src/ButtonLegacy/specs/*.win.ts', + 'src/CheckboxLegacy/specs/*.win.ts', + 'src/LinkLegacy/specs/*.win.ts', + 'src/PersonaCoin/specs/*.win.ts', + 'src/Pressable/specs/*.win.ts', + 'src/Separator/specs/*.win.ts', + 'src/TabsLegacy/specs/*.windows.ts', + 'src/TextLegacy/specs/*.win.ts', + 'src/TextV1/specs/*.win.ts', + 'src/Theme/specs/*.win.ts', + 'src/Tokens/specs/*.win.ts', + ], + ], + win32: [['../E2E/src/**/specs/*.win.ts']], +} as const; diff --git a/apps/E2E/desktop.config.mts b/apps/E2E/desktop.config.mts new file mode 100644 index 00000000000..d98b5dd4414 --- /dev/null +++ b/apps/E2E/desktop.config.mts @@ -0,0 +1,51 @@ +import { defineDesktopConfig } from '@fluentui-react-native/desktop-test/config'; +import { BASE_TESTPAGE } from './src/common/consts.ts'; +import { legacyDesktopSpecs } from './desktop-specs.mts'; + +const runner = { + kind: 'wdio', + framework: 'jasmine', + readiness: { kind: 'native', testId: BASE_TESTPAGE, timeoutMs: 180_000 }, + hooks: './src/common/desktopHooks.ts', + maxInstances: 1, + specFileRetries: 1, + bail: 1, +} as const; + +export default defineDesktopConfig({ + schemaVersion: 1, + projectRoot: new URL('.', import.meta.url), + targets: { + macos: { use: { config: '../fluent-tester/desktop.config.mts', target: 'macos' } }, + windows: { use: { config: '../fluent-tester/desktop.config.mts', target: 'windows' } }, + win32: { use: { config: '../win32/desktop.config.mts', target: 'win32' } }, + }, + suites: { + legacy: { ...runner, specs: legacyDesktopSpecs }, + pilot: { + ...runner, + specFileRetries: 0, + specs: { + macos: [['src/TextLegacy/specs/*.macos.ts']], + windows: [['src/Pressable/specs/*.win.ts']], + win32: [['src/Pressable/specs/*.win.ts']], + }, + }, + 'behavioral-pilot': { + ...runner, + specFileRetries: 0, + specs: { + win32: [ + [ + 'src/ButtonV1/specs/*.win.ts', + 'src/CheckboxV1/specs/*.win.ts', + 'src/Switch/specs/*.win.ts', + 'src/FocusZone/specs/FocusZone_Directional.spec.win.ts', + 'src/Callout/specs/*.win.ts', + ], + ], + }, + }, + }, + reporting: { artifactsRoot: './artifacts/desktop' }, +}); diff --git a/apps/E2E/package.json b/apps/E2E/package.json index 76847f93225..5f960e6cab0 100644 --- a/apps/E2E/package.json +++ b/apps/E2E/package.json @@ -26,6 +26,7 @@ }, "scripts": { "build": "tsc -b", + "desktop-test": "desktop-test", "e2eprep:android": "cross-env APPIUM_HOME=.appium yarn exec appium driver install uiautomator2", "e2eprep:ios": "cross-env APPIUM_HOME=.appium yarn exec appium driver install xcuitest", "e2eprep:macos": "cross-env APPIUM_HOME=.appium yarn exec appium driver install mac2", @@ -37,6 +38,7 @@ "e2etest:win32": "cross-env APPIUM_HOME=. wdio run wdio.conf.win32.js", "e2etest:windows": "rimraf errorShots reports && cross-env APPIUM_HOME=. wdio run wdio.conf.windows.js", "lint": "fluentui-scripts lint", + "test": "node --test desktop-adapters.test.cjs", "test:appium": "cross-env APPIUM_HOME=. node --test appium.test.cjs" }, "dependencies": { @@ -45,6 +47,8 @@ "devDependencies": { "@babel/core": "catalog:", "@babel/runtime": "catalog:", + "@fluentui-react-native/desktop-driver": "workspace:*", + "@fluentui-react-native/desktop-test": "workspace:*", "@fluentui-react-native/focus-zone": "workspace:*", "@fluentui-react-native/scripts": "workspace:*", "@office-iss/react-native-win32": "^0.81.0", diff --git a/apps/E2E/src/Callout/pages/CalloutPageObject.win.ts b/apps/E2E/src/Callout/pages/CalloutPageObject.win.ts index 862eb665e95..1a6f78ea4b0 100644 --- a/apps/E2E/src/Callout/pages/CalloutPageObject.win.ts +++ b/apps/E2E/src/Callout/pages/CalloutPageObject.win.ts @@ -23,7 +23,7 @@ class CalloutPageObject extends BasePage { async closeCallout(): Promise { // all we have to do is click outside the callout - this._testPage.click(); + await this._testPage.click(); } /*****************************************/ diff --git a/apps/E2E/src/CheckboxLegacy/pages/CheckboxLegacyPageObject.ts b/apps/E2E/src/CheckboxLegacy/pages/CheckboxLegacyPageObject.ts index 416b2ad14e8..175ce9b18e3 100644 --- a/apps/E2E/src/CheckboxLegacy/pages/CheckboxLegacyPageObject.ts +++ b/apps/E2E/src/CheckboxLegacy/pages/CheckboxLegacyPageObject.ts @@ -18,7 +18,7 @@ class CheckboxLegacyPageObject extends BasePage { // for native windows, .isSelected() always returns false. this is a workaround return (await checkbox.getAttribute(Attribute.ToggleState)) === AttributeValue.on; } else { - return await checkbox.isSelected(); + return this.isChecked(checkbox); } } diff --git a/apps/E2E/src/CheckboxV1/pages/CheckboxV1PageObject.ts b/apps/E2E/src/CheckboxV1/pages/CheckboxV1PageObject.ts index 3228d27dffd..3d2e1f97408 100644 --- a/apps/E2E/src/CheckboxV1/pages/CheckboxV1PageObject.ts +++ b/apps/E2E/src/CheckboxV1/pages/CheckboxV1PageObject.ts @@ -12,7 +12,7 @@ class CheckboxV1PageObject extends BasePage { /**************** UI Element Interaction Methods ******************/ /******************************************************************/ async isCheckboxChecked(): Promise { - return await (await this._primaryComponent).isSelected(); + return this.isChecked(this._primaryComponent); } /* Waits for the checkbox to be checked or unchecked if the new state is true or false. Returns true if the diff --git a/apps/E2E/src/FocusZone/pages/FocusZonePageObject.ts b/apps/E2E/src/FocusZone/pages/FocusZonePageObject.ts index 20be39d4365..27954bcce01 100644 --- a/apps/E2E/src/FocusZone/pages/FocusZonePageObject.ts +++ b/apps/E2E/src/FocusZone/pages/FocusZonePageObject.ts @@ -65,15 +65,15 @@ class FocusZonePageObject extends BasePage { break; default: } - const switchValue = await switchElement.isSelected(); + const switchValue = await this.isChecked(switchElement); if (switchValue !== arg) { await switchElement.click(); } // Wait until the switch correctly changes states - await browser.waitUntil(async () => (await switchElement.isSelected()) == arg, { + await browser.waitUntil(async () => (await this.isChecked(switchElement)) == arg, { timeout: 15000, - timeoutMsg: `Attempted to switch the ${option} to ${arg}, but it remained at ${await switchElement.isSelected()}`, + timeoutMsg: `Attempted to switch the ${option} to ${arg}, but it remained at ${await this.isChecked(switchElement)}`, }); } diff --git a/apps/E2E/src/Switch/pages/SwitchPageObject.ts b/apps/E2E/src/Switch/pages/SwitchPageObject.ts index 2b03898b35b..82c4727ea29 100644 --- a/apps/E2E/src/Switch/pages/SwitchPageObject.ts +++ b/apps/E2E/src/Switch/pages/SwitchPageObject.ts @@ -17,7 +17,7 @@ class SwitchPageObject extends BasePage { } async isSwitchChecked(): Promise { - return await (await this._primaryComponent).isSelected(); + return this.isChecked(this._primaryComponent); } async waitForSwitchStateChange(newState: boolean, errorMsg: string): Promise { diff --git a/apps/E2E/src/common/BasePage.ts b/apps/E2E/src/common/BasePage.ts index 5c9507d378f..4de0e8a35c2 100644 --- a/apps/E2E/src/common/BasePage.ts +++ b/apps/E2E/src/common/BasePage.ts @@ -11,7 +11,8 @@ import { ROOT_VIEW, TESTPAGE_BUTTONS_SCROLLVIEWER, TESTPAGE_CONTENT_SCROLLVIEWER, -} from './consts'; +} from './consts.js'; +import { isNativeDesktop, scrollNativeDesktopElement, sendNativeDesktopKeys } from './nativeDesktop.js'; const DUMMY_CHAR = ''; // The E2ETEST_PLATFORM environment variable should be set in the beforeSession hook in the wdio.conf file for the respective platform @@ -78,15 +79,18 @@ export abstract class BasePage { */ async navigateToPageAndLoadTests(): Promise { // Desktop platforms automatically scroll to a page's navigation button - this extra step is purely for mobile platforms. - if (this.platform === 'android' || this.platform === 'ios' || this.platform === 'windows') { + if (isNativeDesktop()) { + await scrollNativeDesktopElement(this._pageButton, this._testPageButtonScrollViewer); + } else if (this.platform === 'android' || this.platform === 'ios' || this.platform === 'windows') { await this.mobileScrollToComponentButton(); } - if (this.platform === 'windows') { + if (this.platform === 'windows' && !isNativeDesktop()) { const pageButton = await this._pageButton; // sendKeys will move focus to the button, so we dont have to worry about the button being offscreen in the scrollable area. await this.sendKeys(pageButton, [Keys.ENTER]); - } else { + } else if (!isNativeDesktop() || (await this._pageButton.isEnabled())) { + // The tester disables the selected page's button; specs that share a page (e.g. IconLegacy and IconV1) find it already open. await this._pageButton.click(); } @@ -104,25 +108,32 @@ export abstract class BasePage { await e2eSwitch.waitForDisplayed({ timeoutMsg: 'E2E Test Switch should be visible to enable it' }); await e2eSwitch.waitForEnabled({ timeoutMsg: 'The E2E Switch should be enabled before we interact with it' }); - switch (this.platform) { - // Usually, we use .isSelected() to see if a control (our switch) is checked true or false, but the process is - // different on android because .isSelected() doesn't function as expected on the platform. - case 'android': - if ((await e2eSwitch.getAttribute(AndroidAttribute.Checked)) === 'false') { - await e2eSwitch.click(); - await e2eSwitch.waitUntil(async () => (await e2eSwitch.getAttribute(AndroidAttribute.Checked)) === 'true', { - timeoutMsg: 'Clicked the E2E Mode Switch, but it failed to toggle.', - }); - } - break; - default: - if (!(await e2eSwitch.isSelected())) { - await e2eSwitch.click(); - await e2eSwitch.waitUntil(async () => await e2eSwitch.isSelected(), { - timeoutMsg: 'Clicked the E2E Mode Switch, but it failed to toggle.', - }); - } - } + if (isNativeDesktop()) { + const checked = () => this.isChecked(e2eSwitch); + if (!(await checked())) { + await e2eSwitch.click(); + await browser.waitUntil(checked, { timeoutMsg: 'Clicked the native E2E switch, but checked state did not become true.' }); + } + } else + switch (this.platform) { + // Usually, we use .isSelected() to see if a control (our switch) is checked true or false, but the process is + // different on android because .isSelected() doesn't function as expected on the platform. + case 'android': + if ((await e2eSwitch.getAttribute(AndroidAttribute.Checked)) === 'false') { + await e2eSwitch.click(); + await e2eSwitch.waitUntil(async () => (await e2eSwitch.getAttribute(AndroidAttribute.Checked)) === 'true', { + timeoutMsg: 'Clicked the E2E Mode Switch, but it failed to toggle.', + }); + } + break; + default: + if (!(await e2eSwitch.isSelected())) { + await e2eSwitch.click(); + await e2eSwitch.waitUntil(async () => await e2eSwitch.isSelected(), { + timeoutMsg: 'Clicked the E2E Mode Switch, but it failed to toggle.', + }); + } + } return await this._e2eSection.waitForDisplayed({ timeoutMsg: 'Pressed E2E Mode Switch, but E2E Test Sections failed to display before the timeout.', @@ -190,6 +201,13 @@ export abstract class BasePage { return element.click(); } + async isChecked(element: ChainablePromiseElement): Promise { + if (!isNativeDesktop()) return element.isSelected(); + const value: unknown = await browser.getElementProperty(await element.elementId, 'checked'); + if (typeof value !== 'boolean') throw new Error('The control did not expose a native boolean checked state.'); + return value; + } + /** Given a WebdriverIO element promise, send the passed in list of keys as keyboard inputs. Use this across all PageObject methods and test specs. * * Common examples: @@ -198,6 +216,7 @@ export abstract class BasePage { * - Escape out of a menu: MenuPageObject.sendKeys(MenuPageObject.item1, [KEY_ESCAPE]) */ async sendKeys(element: ChainablePromiseElement, keys: Keys[]) { + if (isNativeDesktop()) return sendNativeDesktopKeys(element, keys); return element.addValue(keys.join()); } @@ -209,6 +228,7 @@ export abstract class BasePage { /* Scrolls until the desired test page's button is displayed. We use the scroll viewer UI element as the point to start scrolling. * We use a negative number as the Y-coordinate because that enables us to scroll downwards */ async mobileScrollToComponentButton() { + if (isNativeDesktop()) return scrollNativeDesktopElement(this._pageButton, this._testPageButtonScrollViewer); if (this.platform !== 'android' && (await this._pageButton.isDisplayed())) { return; } @@ -302,6 +322,7 @@ export abstract class BasePage { /* Scrolls to the specified or primary UI test element until it is displayed. */ async scrollToTestElement(component?: ChainablePromiseElement) { const ComponentToScrollTo = component ? component : this._primaryComponent; + if (isNativeDesktop()) return scrollNativeDesktopElement(ComponentToScrollTo, By(TESTPAGE_CONTENT_SCROLLVIEWER)); if (await ComponentToScrollTo.isDisplayed()) { return; } @@ -331,6 +352,7 @@ export abstract class BasePage { /* Scrolls to the specified or primary UI test element until it is displayed. */ async mobileScrollToTestElement(componentIdentifier?: string): Promise { const componentToScrollTo = componentIdentifier ?? this._primaryComponentName; + if (isNativeDesktop()) return scrollNativeDesktopElement(By(componentToScrollTo), By(TESTPAGE_CONTENT_SCROLLVIEWER)); if (await By(componentToScrollTo).isDisplayed()) { return; } diff --git a/apps/E2E/src/common/desktopHooks.ts b/apps/E2E/src/common/desktopHooks.ts new file mode 100644 index 00000000000..055bd094426 --- /dev/null +++ b/apps/E2E/src/common/desktopHooks.ts @@ -0,0 +1,44 @@ +import path from 'node:path'; +import { createHash } from 'node:crypto'; +import { createDesktopDriverClient, DesktopSessionClient } from '@fluentui-react-native/desktop-driver'; +import { ArtifactManager, captureDesktopFailureEvidence } from '@fluentui-react-native/desktop-driver/artifacts'; +import { createDesktopReportSession } from '@fluentui-react-native/desktop-test/reporting'; +import type { DesktopReportSession } from '@fluentui-react-native/desktop-test/reporting'; +import { installLegacyDesktopAttributes } from './nativeDesktop'; + +let reporting: DesktopReportSession | undefined; + +export default { + async before() { + reporting = await createDesktopReportSession({ artifactsRoot: path.resolve('artifacts', 'desktop'), kind: 'lifecycle' }); + installLegacyDesktopAttributes(); + // Native cohorts use the app's initial bounds and native scrolling, not an emulated maximize command. + const { width, height } = await browser.getWindowRect(); + if (width <= 0 || height <= 0) throw new Error('The native tester window has no usable bounds.'); + }, + async afterTest(test: { fullTitle?: string; title: string }, _context: unknown, outcome: { passed: boolean }) { + if (outcome.passed || !reporting) return; + const reporter = reporting.reporter; + const name = createHash('sha256') + .update(JSON.stringify([test.fullTitle ?? test.title, Date.now()])) + .digest('hex') + .slice(0, 24); + const root = reporter.artifactPath(path.join('evidence', name)); + const url = `${browser.options.protocol}://${browser.options.hostname}:${browser.options.port}${browser.options.path}`; + const session = new DesktopSessionClient( + createDesktopDriverClient({ url }), + browser.sessionId, + Object.fromEntries(Object.entries(browser.capabilities)), + ); + const evidence = await captureDesktopFailureEvidence(session, new ArtifactManager(root), 'failure'); + for (const artifact of evidence.artifacts) + reporter.emit({ + kind: 'artifact', + payload: { path: path.relative(reporter.root, path.join(root, artifact.path)), description: artifact.kind }, + }); + for (const error of evidence.errors) reporter.fail(new Error(error), 'evidence'); + }, + async afterSession() { + await reporting?.close(); + }, +}; diff --git a/apps/E2E/src/common/nativeDesktop.ts b/apps/E2E/src/common/nativeDesktop.ts new file mode 100644 index 00000000000..8d7f8146cd6 --- /dev/null +++ b/apps/E2E/src/common/nativeDesktop.ts @@ -0,0 +1,123 @@ +import { Keys } from './consts.js'; + +export function isNativeDesktop(): boolean { + return browser.capabilities.browserName === 'furn-native-desktop'; +} + +export async function sendNativeDesktopKeys(element: ChainablePromiseElement, keys: readonly Keys[]): Promise { + const target = await element; + await browser.elementSendKeys(await target.elementId, ''); + const actions: { type: 'keyDown' | 'keyUp'; value: string }[] = []; + const held = new Set(); + for (const value of keys) { + if (value === Keys.NULL) { + for (const modifier of held) actions.push({ type: 'keyUp', value: modifier }); + held.clear(); + } else if ([Keys.SHIFT, Keys.CONTROL, Keys.ALT].some((modifier) => modifier === value)) { + if (!held.has(value)) actions.push({ type: 'keyDown', value }); + held.add(value); + } else actions.push({ type: 'keyDown', value }, { type: 'keyUp', value }); + } + for (const value of held) actions.push({ type: 'keyUp', value }); + let failure: unknown; + try { + await browser.performActions([{ type: 'key', id: 'tester-keyboard', actions }]); + } catch (error) { + failure = error; + } + try { + await browser.releaseActions(); + } catch (error) { + throw new AggregateError(failure === undefined ? [error] : [failure, error], 'Desktop keyboard input or release failed.'); + } + if (failure !== undefined) throw failure; +} + +async function isCenterWithinScroller(element: ChainablePromiseElement, scroller: ChainablePromiseElement): Promise { + if (!(await element.isDisplayed())) return false; + const target = await browser.getElementRect(await element.elementId); + const viewport = await browser.getElementRect(await scroller.elementId); + const x = target.x + target.width / 2; + const y = target.y + target.height / 2; + return x > viewport.x && y > viewport.y && x < viewport.x + viewport.width && y < viewport.y + viewport.height; +} + +export async function scrollNativeDesktopElement(element: ChainablePromiseElement, scroller: ChainablePromiseElement): Promise { + // A partly clipped control still reports displayed, but the driver clicks its center point, which may be covered. + if (await isCenterWithinScroller(element, scroller)) return; + if (!browser.capabilities['furn:features']?.wheel) + throw new Error('This desktop does not support native wheel input; navigation cannot be qualified.'); + // Native wheel input goes to the topmost window under the pointer, so the tester window must be in front first. + await browser.switchToWindow(await browser.getWindowHandle()); + const origin = { 'element-6066-11e4-a52e-4f735466cecf': await scroller.elementId }; + await browser.waitUntil( + async () => { + await browser.performActions([ + { type: 'wheel', id: 'tester-scroll', actions: [{ type: 'scroll', origin, x: 0, y: 0, deltaX: 0, deltaY: 300, duration: 100 }] }, + ]); + return isCenterWithinScroller(element, scroller); + }, + { timeout: 30_000, timeoutMsg: 'The native scroll container did not bring the selected tester control into view.' }, + ); +} + +export function installLegacyDesktopAttributes(): void { + const properties: Readonly> = { + AutomationId: 'automationId', + Name: 'name', + ControlType: 'role', + IsEnabled: 'enabled', + HasKeyboardFocus: 'focused', + IsRequiredForForm: 'required', + 'Toggle.ToggleState': 'checked', + 'ExpandCollapse.ExpandCollapseState': 'expanded', + }; + const roles: Readonly> = { + button: 'Button', + group: 'Group', + checkbox: 'CheckBox', + image: 'Image', + link: 'HyperLink', + menu: 'Menu', + menuitem: 'MenuItem', + radio: 'RadioButton', + list: 'List', + tablist: 'Tab', + tab: 'TabItem', + text: 'Text', + }; + browser.overwriteCommand( + 'getAttribute', + async function (original, name) { + const property = properties[name]; + if (!property) { + if (['HelpText', 'IsTogglePatternAvailable'].includes(name)) { + throw new Error( + `Legacy attribute "${name}" has no qualified native mapping. It must not be reported as false or silently skipped.`, + ); + } + return original(name); + } + const value: unknown = await this.getElementProperty(this.elementId, property); + if (name === 'ControlType') { + if (typeof value !== 'string' || !roles[value]) + throw new Error(`Native role "${String(value)}" has no legacy ControlType mapping.`); + return `ControlType.${roles[value]}`; + } + if (name === 'Toggle.ToggleState') { + if (value === 'mixed') return '2'; + if (typeof value !== 'boolean') throw new Error('Native checked state is unavailable.'); + return value ? '1' : '0'; + } + if (name === 'ExpandCollapse.ExpandCollapseState') { + if (typeof value !== 'boolean') throw new Error('Native expanded state is unavailable.'); + return value ? 'Expanded' : 'Collapsed'; + } + if (name === 'IsRequiredForForm' && typeof value !== 'boolean') throw new Error('Native required-form state is unavailable.'); + if (typeof value === 'boolean') return value ? 'True' : 'False'; + if (value === null || typeof value === 'string') return value; + throw new Error(`Native property "${property}" returned an invalid value.`); + }, + true, + ); +} diff --git a/apps/E2E/tsconfig.json b/apps/E2E/tsconfig.json index 55864b6fdef..7bd00ba3fb7 100644 --- a/apps/E2E/tsconfig.json +++ b/apps/E2E/tsconfig.json @@ -9,6 +9,12 @@ }, "include": ["src"], "references": [ + { + "path": "../../packages/agentic/desktop-test/tsconfig.json" + }, + { + "path": "../../packages/agentic/desktop-driver/tsconfig.json" + }, { "path": "../../packages/shim/focus-zone/tsconfig.json" }, diff --git a/apps/E2E/wdio.conf.macos.js b/apps/E2E/wdio.conf.macos.js index 67e61c5a8bb..943720e48e8 100644 --- a/apps/E2E/wdio.conf.macos.js +++ b/apps/E2E/wdio.conf.macos.js @@ -1,4 +1,5 @@ const fs = require('fs'); +const { legacyDesktopSpecs } = require('./desktop-specs.mts'); const defaultWaitForTimeout = 20000; const defaultConnectionRetryTimeout = 60000; @@ -6,7 +7,7 @@ const jasmineDefaultTimeout = 60000; // 60 seconds for Jasmine test timeout exports.config = { runner: 'local', // Where should your test be launched - specs: [['src/**/specs/*.macos.ts']], + specs: legacyDesktopSpecs.macos, exclude: [/* 'path/to/excluded/files' */], maxInstances: 30, diff --git a/apps/E2E/wdio.conf.win32.js b/apps/E2E/wdio.conf.win32.js index c2f180a4046..b8a27e81f29 100644 --- a/apps/E2E/wdio.conf.win32.js +++ b/apps/E2E/wdio.conf.win32.js @@ -1,5 +1,6 @@ const path = require('path'); const fs = require('fs'); +const { legacyDesktopSpecs } = require('./desktop-specs.mts'); const appPath = path.resolve(path.dirname(require.resolve('@office-iss/rex-win32/rex-win32.js')), 'ReactTest.exe'); const appArgs = @@ -14,7 +15,7 @@ const jasmineDefaultTimeout = 60000; // 60 seconds for Jasmine test timeout exports.config = { runner: 'local', // Where should your test be launched - specs: [['../E2E/src/**/specs/*.win.ts']], + specs: legacyDesktopSpecs.win32, exclude: [], capabilities: [ diff --git a/apps/E2E/wdio.conf.windows.js b/apps/E2E/wdio.conf.windows.js index 997c2a24d66..57de27f1244 100644 --- a/apps/E2E/wdio.conf.windows.js +++ b/apps/E2E/wdio.conf.windows.js @@ -1,4 +1,5 @@ const fs = require('fs'); +const { legacyDesktopSpecs } = require('./desktop-specs.mts'); const defaultWaitForTimeout = 20000; const defaultConnectionRetryTimeout = 60000; @@ -6,24 +7,8 @@ const jasmineDefaultTimeout = 60000; // 60 seconds for Jasmine test timeout exports.config = { runner: 'local', - /* UWP controls are a subset of the Win32 controls. Only some work on our UWP test app, - so we must specify which ones we want to test here. */ - specs: [ - [ - 'src/ActivityIndicator/specs/*.win.ts', - 'src/ButtonLegacy/specs/*.win.ts', - 'src/CheckboxLegacy/specs/*.win.ts', // See spec file for more information - 'src/LinkLegacy/specs/*.win.ts', - 'src/PersonaCoin/specs/*.win.ts', - 'src/Pressable/specs/*.win.ts', - 'src/Separator/specs/*.win.ts', - 'src/TabsLegacy/specs/*.windows.ts', // See spec file for more information - 'src/TextLegacy/specs/*.win.ts', - 'src/TextV1/specs/*.win.ts', - 'src/Theme/specs/*.win.ts', - 'src/Tokens/specs/*.win.ts', - ], - ], + // Preserve the existing allowlist, including unmatched patterns that block native parity. + specs: legacyDesktopSpecs.windows, exclude: [/* 'path/to/excluded/files' */], maxInstances: 1, diff --git a/apps/fluent-tester/README.md b/apps/fluent-tester/README.md index e0ea234389d..51a4d3d80a3 100644 --- a/apps/fluent-tester/README.md +++ b/apps/fluent-tester/README.md @@ -7,3 +7,7 @@ - [macOS](./docs/macos.md) - [Windows](./docs/windows.md) - [Win32](./../win32/README.md) + +For native desktop E2E configuration and suite execution, see the +`@fluentui-react-native/desktop-test` package guide at +`packages/agentic/desktop-test/README.md`. diff --git a/apps/fluent-tester/desktop.config.mts b/apps/fluent-tester/desktop.config.mts new file mode 100644 index 00000000000..a8b7c59dd40 --- /dev/null +++ b/apps/fluent-tester/desktop.config.mts @@ -0,0 +1,10 @@ +import { defineDesktopConfig } from '@fluentui-react-native/desktop-test/config'; + +export default defineDesktopConfig({ + schemaVersion: 1, + projectRoot: new URL('.', import.meta.url), + targets: { + macos: { host: { kind: 'react-native-test-app', renderer: 'fabric' }, launchTimeoutMs: 180_000 }, + windows: { host: { kind: 'react-native-test-app', renderer: 'fabric' }, launchTimeoutMs: 180_000 }, + }, +}); diff --git a/apps/fluent-tester/package.json b/apps/fluent-tester/package.json index fc4bf900dab..ed277ea12a3 100644 --- a/apps/fluent-tester/package.json +++ b/apps/fluent-tester/package.json @@ -30,6 +30,7 @@ "bundle:macos": "rnx-cli bundle --dev false --platform macos", "bundle:windows": "rnx-cli bundle --dev false --platform windows", "clean": "fluentui-scripts clean", + "desktop-test": "desktop-test", "format": "fluentui-scripts format", "ios": "rnx-cli run --platform ios", "lint": "fluentui-scripts lint", @@ -51,6 +52,7 @@ "@babel/core": "catalog:", "@babel/runtime": "catalog:", "@fluentui-react-native/callout": "workspace:*", + "@fluentui-react-native/desktop-test": "workspace:*", "@fluentui-react-native/experimental-appearance-additions": "workspace:*", "@fluentui-react-native/experimental-avatar": "workspace:*", "@fluentui-react-native/experimental-checkbox": "workspace:*", diff --git a/apps/fluent-tester/tsconfig.json b/apps/fluent-tester/tsconfig.json index ae103d04278..a66e1911e52 100644 --- a/apps/fluent-tester/tsconfig.json +++ b/apps/fluent-tester/tsconfig.json @@ -13,6 +13,9 @@ { "path": "../../packages/shim/callout/tsconfig.json" }, + { + "path": "../../packages/agentic/desktop-test/tsconfig.json" + }, { "path": "../../packages/experimental/AppearanceAdditions/tsconfig.json" }, diff --git a/apps/storybook/AGENTS.md b/apps/storybook/AGENTS.md index 483172f1462..ee59fea47e7 100644 --- a/apps/storybook/AGENTS.md +++ b/apps/storybook/AGENTS.md @@ -1,190 +1,56 @@ -# Agentic Components Storybook development - -These instructions apply to `apps/storybook` and its descendants. - -Read [`agent-map.yaml`](agent-map.yaml) first for the compact architecture, lookup, and interaction map. Read this file, -`README.md`, `package.json`, and `storybook.config.mts` before changing the Storybook application or its native projects. - -## Agent-efficient discovery and interaction - -- Generate the platform manifest with `yarn storybook manifest --` instead of guessing IDs or searching the - sidebar. -- Query and run stories through `yarn desktop-driver` against the listener started by - `yarn storybook driver --`. -- Use screenshots only for visual evidence. Use WebSocket events and accessibility bounds for lookup and interaction. -- Treat `src/storybook.requires.ts` as generated output; use `storybook.config.mts` for story package discovery and - `src/main.ts` only as the shared config adapter. -- Keep `agent-map.yaml` synchronized when stable paths, scripts, services, or interaction contracts change. Do not list - individual stories there; the runtime index is authoritative. - -## Command and dependency discipline - -- From the repository root, invoke scripts with - `yarn workspace @fluentui-react-native/agentic-components-storybook