Skip to content

chore: mobile visreg#517

Merged
cb-ekuersch merged 24 commits into
masterfrom
mobile-visreg
Mar 27, 2026
Merged

chore: mobile visreg#517
cb-ekuersch merged 24 commits into
masterfrom
mobile-visreg

Conversation

@cb-ekuersch
Copy link
Copy Markdown
Contributor

@cb-ekuersch cb-ekuersch commented Mar 18, 2026

What changed? Why?

Replaces the legacy Detox-based packages/ui-mobile-visreg package with a new, simpler approach: Maestro for screenshot capture and BrowserStack App Percy for visual comparison.

The old Detox setup required specialized native builds with injected gray-box test code, making the build matrix complex (8 variations) and CI slow. Maestro navigates the app via deep-links and calls takeScreenshot — no special native build required. The standard release prebuild is used directly, and CI patches the JS bundle rather than doing a full native rebuild on every run.

Percy project: https://percy.io/d424b72f/web/cds-mobile-visreg-03c97426

Key Changes

New packages/mobile-visreg

  • Orchestrates Maestro flows that deep-link to each component route (:///Debug), wait for animations, and capture a PNG
  • Explicit route opt-in list (config/enabled-routes.mjs) — 44 routes currently enabled
  • Nx targets: setup (install Maestro), ios, android (capture), upload (Percy)
  • Percy integration for visual diffing across branches

Removed legacy packages/ui-mobile-visreg

  • Deleted the old Detox-based package (~1,500 lines removed), its Docker setup, and the apps/mobile-app/e2e/ test suite

Deep-link navigation fix (apps/mobile-app/src/App.tsx)

  • Added getStateFromPath + getActionFromState using CommonActions.reset so that each deep-link fully resets the navigation stack — prevents previously-open modals from persisting into the next route's screenshot

CI workflow (.github/workflows/visreg-mobile.yml)

  • Runs on pushes to master and on PRs labeled visreg-mobile
  • Patches the JS bundle into the committed iOS prebuild (avoids 8+ min native rebuild in CI)
  • Uploads screenshots to Percy; posts the Percy build URL as a PR comment

Documentation

  • Rewrote packages/mobile-visreg/README.md with full workflow, Nx target reference, and Percy setup guide
  • Updated apps/mobile-app/docs/prebuilds.md to remove Detox references and document the new visreg approach

Screenshots

Percy job comparing master and v9 GHA actions
Screenshot 2026-03-21 at 2 30 54 PM Screenshot 2026-03-18 at 2 58 26 PM

Testing

How has it been tested?

In addition to running the test suite locally with my simulator I performed and full end to end test run by uploading screenshots to percy from my machine from both master and v9 visreg branches.

Sample comparison build: https://percy.io/d424b72f/web/cds-mobile-visreg-03c97426/builds/47997661/changed/2548746021?browser=chrome&browser_ids=63%2C69%2C70%2C71&group_snapshots_by=similar_diff&subcategories=approved&test_case_ids=&viewLayout=side-by-side&viewMode=new&width=1206&widths=1206

  • Unit tests
  • Interaction tests
  • Pseudo State tests
  • Manual - Web
  • Manual - Android (Emulator / Device)
  • Manual - iOS (Emulator / Device)

Testing instructions

If you plan to test uploading screenshots from your machine, the the percy token from our project.

  1. yarn nx run mobile-visreg:setup (install Maestro, one-time)
  2. yarn nx run mobile-app:build:ios-release && yarn nx run mobile-app:launch:ios-release
  3. yarn nx run mobile-visreg:ios — verify screenshots appear in packages/mobile-visreg/visreg-screenshots/
  4. PERCY_TOKEN=app_xxx yarn nx run mobile-visreg:upload — verify build appears in Percy dashboard

Illustrations/Icons Checklist

Required if this PR changes files under packages/illustrations/** or packages/icons/**

  • verified visreg changes with Terran (include link to visreg run/approval)
  • all illustration/icons names have been reviewed by Dom and/or Terran

Change management

type=routine
risk=low
impact=sev5

automerge=false

@cb-heimdall
Copy link
Copy Markdown
Collaborator

cb-heimdall commented Mar 18, 2026

✅ Heimdall Review Status

Requirement Status More Info
Reviews 1/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 1
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1
CODEOWNERS ✅ See below

CODEOWNERS

Code Owner Status Calculation
ui-systems-eng-team 1/1
Denominator calculation
Additional CODEOWNERS Requirement
Show calculation
Sum 0
0
From CODEOWNERS 1
Sum 1

const cmd = ['maestro', 'test', flowPath, ...envFlags.map((e) => `--env ${e}`)].join(' ');

console.log(`\nRunning: ${cmd}\n`);
execSync(cmd, { stdio: 'inherit', cwd: outputDir });

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium

This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.

Copilot Autofix

AI 3 months ago

In general, the fix is to avoid constructing a single shell command string and instead call execFileSync (or spawnSync) with the command and its arguments as separate array elements. This way, Node passes arguments directly to the process without an intervening shell, so special characters in paths or environment values cannot change command structure. For this code, the best fix is to change the Maestro invocation so that maestro is the executable and the rest of the tokens (test, flowPath, --env, KEY=VALUE) are elements in an array, then use execFileSync instead of execSync. This preserves functionality but removes shell interpretation.

Concretely in packages/mobile-visreg/src/run.mjs:

  • Add execFileSync to the import from child_process.
  • Replace the string-building cmd line with an array of arguments: ['test', flowPath, ...envFlags.flatMap(e => ['--env', e])].
  • Replace the execSync(cmd, ...) call with execFileSync('maestro', args, { stdio: 'inherit', cwd: outputDir });.
  • Keep the existing console log of the human-readable command string (or reconstruct it from the args) for visibility if desired, but this log should not be executed by the shell.

The earlier execSync that runs node src/generate-flows.mjs ${platform} is technically similar, but it does not involve the tainted flowPath/absolute path and only injects platform (which is limited to 'ios' by default or whatever the caller supplies). Since CodeQL’s specific alerts here are about the Maestro command, we will leave that first execSync unchanged to avoid unrequested behavioral changes.

Suggested changeset 1
packages/mobile-visreg/src/run.mjs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/mobile-visreg/src/run.mjs b/packages/mobile-visreg/src/run.mjs
--- a/packages/mobile-visreg/src/run.mjs
+++ b/packages/mobile-visreg/src/run.mjs
@@ -1,4 +1,4 @@
-import { execSync } from 'child_process';
+import { execSync, execFileSync } from 'child_process';
 import { mkdirSync, readdirSync } from 'fs';
 import { resolve, dirname } from 'path';
 import { fileURLToPath } from 'url';
@@ -50,10 +50,11 @@
   envFlags.push(`ROUTE_NAME=${route}`);
 }
 
-const cmd = ['maestro', 'test', flowPath, ...envFlags.map((e) => `--env ${e}`)].join(' ');
+const maestroArgs = ['test', flowPath, ...envFlags.flatMap((e) => ['--env', e])];
+const cmd = ['maestro', ...maestroArgs].join(' ');
 
 console.log(`\nRunning: ${cmd}\n`);
-execSync(cmd, { stdio: 'inherit', cwd: outputDir });
+execFileSync('maestro', maestroArgs, { stdio: 'inherit', cwd: outputDir });
 
 const screenshots = readdirSync(outputDir).filter((f) => f.endsWith('.png'));
 console.log(`\nCapture complete: ${screenshots.length} screenshots in ${outputDir}`);
EOF
@@ -1,4 +1,4 @@
import { execSync } from 'child_process';
import { execSync, execFileSync } from 'child_process';
import { mkdirSync, readdirSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
@@ -50,10 +50,11 @@
envFlags.push(`ROUTE_NAME=${route}`);
}

const cmd = ['maestro', 'test', flowPath, ...envFlags.map((e) => `--env ${e}`)].join(' ');
const maestroArgs = ['test', flowPath, ...envFlags.flatMap((e) => ['--env', e])];
const cmd = ['maestro', ...maestroArgs].join(' ');

console.log(`\nRunning: ${cmd}\n`);
execSync(cmd, { stdio: 'inherit', cwd: outputDir });
execFileSync('maestro', maestroArgs, { stdio: 'inherit', cwd: outputDir });

const screenshots = readdirSync(outputDir).filter((f) => f.endsWith('.png'));
console.log(`\nCapture complete: ${screenshots.length} screenshots in ${outputDir}`);
Copilot is powered by AI and may make mistakes. Always verify output.
const screenshotDir = resolve(dir);
console.log(`Uploading screenshots from ${screenshotDir} to Percy...`);

execSync(`npx percy upload ${screenshotDir}`, { stdio: 'inherit' });

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium

This shell command depends on an uncontrolled
absolute path
.

Copilot Autofix

AI 3 months ago

In general, to fix shell command injection or misinterpretation issues, avoid constructing a single shell command string that includes untrusted values. Instead, invoke the underlying program directly (bypassing the shell) and pass untrusted values as separate arguments in an array, using child_process.execFile / execFileSync or spawn / spawnSync. This prevents the shell from interpreting special characters in the arguments.

For this specific case in packages/mobile-visreg/src/upload.mjs, replace execSync with execFileSync from child_process, and change the invocation from a template string to a command plus arguments array:

  • Import execFileSync instead of or in addition to execSync at the top of the file.
  • Replace execSync(\npx percy upload ${screenshotDir}`, { stdio: 'inherit' });withexecFileSync('npx', ['percy', 'upload', screenshotDir], { stdio: 'inherit' });`.

This keeps all existing behavior (still runs npx percy upload <dir> and inherits stdio) but avoids the shell entirely, so screenshotDir is treated as a literal argument. All changes are confined to packages/mobile-visreg/src/upload.mjs within the shown snippet, and no additional methods are needed beyond the new import.

Suggested changeset 1
packages/mobile-visreg/src/upload.mjs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/mobile-visreg/src/upload.mjs b/packages/mobile-visreg/src/upload.mjs
--- a/packages/mobile-visreg/src/upload.mjs
+++ b/packages/mobile-visreg/src/upload.mjs
@@ -1,4 +1,4 @@
-import { execSync } from 'child_process';
+import { execFileSync } from 'child_process';
 import { resolve } from 'path';
 
 function parseArgs() {
@@ -24,6 +24,6 @@
 const screenshotDir = resolve(dir);
 console.log(`Uploading screenshots from ${screenshotDir} to Percy...`);
 
-execSync(`npx percy upload ${screenshotDir}`, { stdio: 'inherit' });
+execFileSync('npx', ['percy', 'upload', screenshotDir], { stdio: 'inherit' });
 
 console.log('\nUpload complete. Visit percy.io to review the build.');
EOF
@@ -1,4 +1,4 @@
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
import { resolve } from 'path';

function parseArgs() {
@@ -24,6 +24,6 @@
const screenshotDir = resolve(dir);
console.log(`Uploading screenshots from ${screenshotDir} to Percy...`);

execSync(`npx percy upload ${screenshotDir}`, { stdio: 'inherit' });
execFileSync('npx', ['percy', 'upload', screenshotDir], { stdio: 'inherit' });

console.log('\nUpload complete. Visit percy.io to review the build.');
Copilot is powered by AI and may make mistakes. Always verify output.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 23, 2026

Comment thread .nxignore
Comment thread apps/mobile-app/docs/prebuilds.md
Comment thread packages/mobile-visreg/flows/capture-route-steps.yaml
Comment thread packages/mobile-visreg/README.md
Comment on lines +25 to +32
runs-on: [small, default-config]
steps:
- uses: actions/checkout@v4

# Test the published action
# - name: New CDS Action
# uses: [fill this in on new branch]
# with: [fill this in on new branch]

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 2 months ago

In general, the fix is to explicitly declare a permissions: block that restricts the GITHUB_TOKEN to the minimal scopes required. For this stub workflow, the only visible requirement is to read repository contents (for actions/checkout), so contents: read is sufficient. Adding this at the workflow root applies to all jobs unless they override it.

The best fix without changing functionality is to add a root-level permissions: block right after the name: line, before the on: block, in .github/workflows/guard-debug-workflow.yml. This will document and enforce that the workflow’s GITHUB_TOKEN has only read access to repository contents, which is compatible with the existing steps. No imports, methods, or additional definitions are needed, as this is purely a YAML configuration change inside the workflow.

Suggested changeset 1
.github/workflows/guard-debug-workflow.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/guard-debug-workflow.yml b/.github/workflows/guard-debug-workflow.yml
--- a/.github/workflows/guard-debug-workflow.yml
+++ b/.github/workflows/guard-debug-workflow.yml
@@ -16,6 +16,9 @@
 
 name: Debug Workflow (replace me in your branch)
 
+permissions:
+  contents: read
+
 on:
   # Manual trigger for testing
   workflow_dispatch:
EOF
@@ -16,6 +16,9 @@

name: Debug Workflow (replace me in your branch)

permissions:
contents: read

on:
# Manual trigger for testing
workflow_dispatch:
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

4 participants