Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
dc4bad0
minimum implementation for '--disable-hud' app arg flag needed for vi…
pravusjif May 5, 2026
23c32ea
Merge branch 'dev' into chore/visual-test-app-args
pravusjif May 6, 2026
8481dce
Merge branch 'dev' into chore/visual-test-app-args
pravusjif May 7, 2026
d9c29e7
Merge branch 'dev' into chore/visual-test-app-args
pravusjif May 7, 2026
bfd8373
refactored --disable-hud to avoid SDK UI
pravusjif May 8, 2026
9821bee
Merge branch 'chore/visual-test-app-args' of github.com:decentraland/…
pravusjif May 8, 2026
48f701c
implemented --graphics
pravusjif May 8, 2026
3b47ddb
added visual tests determinism section in docs
pravusjif May 8, 2026
9121464
Merge branch 'dev' into chore/visual-test-app-args
pravusjif May 8, 2026
5f57c3c
fixed skybox time values not bound to --skybox-time-enabled flag
pravusjif May 8, 2026
62c066f
added --skip-minimum-specs-screen
pravusjif May 9, 2026
1d0a9d9
Merge branch 'dev' into chore/visual-test-app-args
pravusjif May 9, 2026
60a7bd4
updated docs
pravusjif May 9, 2026
1f59799
Merge branch 'dev' into chore/visual-test-app-args
pravusjif May 12, 2026
fe26266
added scene readiness probe for visual tests dynamic check
pravusjif May 12, 2026
cae4c98
Merge branch 'dev' into chore/visual-test-app-args
pravusjif May 13, 2026
e63a757
added github workflow for triggering Alttester Visual Tests with a Gi…
pravusjif May 13, 2026
0bb373d
Merge branch 'dev' into chore/visual-test-app-args
pravusjif May 14, 2026
7ee1d3d
Merge branch 'dev' of github.com:decentraland/unity-explorer into cho…
pravusjif May 15, 2026
65eb643
Merge branch 'dev' into chore/visual-test-app-args
pravusjif May 15, 2026
882f0d1
Merge branch 'dev' into chore/visual-test-app-args
pravusjif May 18, 2026
3a55381
Merge branch 'dev' of github.com:decentraland/unity-explorer into cho…
pravusjif May 18, 2026
31c4136
Merge branch 'dev' into chore/visual-test-app-args
pravusjif May 18, 2026
6fe73ac
failing async tests refactor
pravusjif May 18, 2026
84cd99c
async tests refactor again
pravusjif May 19, 2026
2000a40
final tests correction
pravusjif May 19, 2026
8c407e8
fix(native-window): use ExclusiveFullScreen when --resolution is pro…
popuz May 19, 2026
4490a2a
Merge remote-tracking branch 'origin/chore/visual-test-app-args' into…
popuz May 19, 2026
097f875
updated docs with alttester visual tests triggering info
pravusjif May 19, 2026
7a2cde8
updated app arguments doc
pravusjif May 20, 2026
f4b7deb
Merge branch 'dev' into chore/visual-test-app-args
pravusjif May 20, 2026
632242d
mini refactor of Fullscreen and Resolution override logic
pravusjif May 20, 2026
38c7590
inherit fullscreen setting if no paramter or playerpref value is exis…
pravusjif May 20, 2026
2d2d841
claude review feedback
pravusjif May 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions .github/workflows/visual-regression.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
name: Visual Regression

# Slash-command-driven visual regression runner. The actual mechanics live in
# decentraland/explorer-automation's `run-visual-suite.yml` reusable workflow;
# this file only handles trigger validation, branch matching, and dispatching
# to that workflow.
#
# Trigger:
# Comment "/visual-tests" on a PR. Restricted to OWNER / MEMBER /
# COLLABORATOR author associations — anyone else's comment is silently
# ignored to keep this from becoming a free-CPU buffet for drive-by PRs.
#
# Branch matching:
# We look up the PR's head branch in explorer-automation. If that branch
# exists, we pass it as `tests_ref` so visual baselines and test fixtures
# from the matching feature branch are used. Otherwise we fall back to
# explorer-automation's default branch (metaforge handles the empty case
# internally).
#
# Required secrets / vars (configure once on the repo):
#
# Used directly by this dispatcher:
# - vars.DEV_EXPLORER_TEAM_S3_BUCKET_PUBLIC_URL Constructs the Explorer build download URL
#
# Inherited by the reusable workflow via `secrets: inherit` — must exist
# on this repo even though the dispatcher itself doesn't reference them:
# - secrets.ALTTESTER_LICENSE
# - secrets.REPOS_READ_ONLY_TOKEN
# - secrets.DEV_EXPLORER_TEAM_S3_BUCKET
# - secrets.DEV_EXPLORER_TEAM_AWS_DEFAULT_REGION
# - secrets.DEV_EXPLORER_TEAM_AWS_ACCESS_KEY_ID
# - secrets.DEV_EXPLORER_TEAM_AWS_SECRET_ACCESS_KEY

on:
issue_comment:
types: [created]

permissions:
contents: read
pull-requests: write

concurrency:
group: visual-regression-${{ github.event.issue.number }}
cancel-in-progress: true

jobs:
resolve:
name: Resolve trigger
if: |
github.event.issue.pull_request &&
startsWith(github.event.comment.body, '/visual-tests')
runs-on: ubuntu-latest
outputs:
authorized: ${{ steps.gate.outputs.authorized }}
pr_number: ${{ steps.pr.outputs.number }}
build_url: ${{ steps.pr.outputs.build_url }}
tests_ref: ${{ steps.match.outputs.tests_ref }}
head_sha: ${{ steps.pr.outputs.head_sha }}
head_short_sha: ${{ steps.pr.outputs.head_short_sha }}
head_ref: ${{ steps.pr.outputs.head_ref }}

steps:
- name: Gate by author association
id: gate
env:
ASSOC: ${{ github.event.comment.author_association }}
run: |
set -euo pipefail
case "$ASSOC" in
OWNER|MEMBER|COLLABORATOR)
echo "authorized=true" >> "$GITHUB_OUTPUT"
;;
*)
echo "::notice::Ignoring /visual-tests from $ASSOC ${{ github.event.comment.user.login }} — write access required."
echo "authorized=false" >> "$GITHUB_OUTPUT"
;;
esac

- name: React to the trigger comment
if: steps.gate.outputs.authorized == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh api -X POST \
"repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" \
-f content=eyes >/dev/null

- name: Resolve PR head + build URL
id: pr
if: steps.gate.outputs.authorized == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.issue.number }}
PUBLIC_URL_PREFIX: ${{ vars.DEV_EXPLORER_TEAM_S3_BUCKET_PUBLIC_URL }}
run: |
set -euo pipefail

# 1. Fetch PR head — issue_comment events don't carry it in the payload.
PR_JSON=$(gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}")
HEAD_SHA=$(echo "$PR_JSON" | jq -r '.head.sha')
HEAD_REF=$(echo "$PR_JSON" | jq -r '.head.ref')
SHORT_SHA="${HEAD_SHA:0:7}"

# 2. Find the most recent successful Unity Cloud Build run for this SHA.
RUN_JSON=$(gh api "repos/${{ github.repository }}/actions/runs?head_sha=${HEAD_SHA}&status=success&per_page=20" \
--jq '[.workflow_runs[] | select(.name == "Unity Cloud Build")] | .[0]')
if [ -z "$RUN_JSON" ] || [ "$RUN_JSON" = "null" ]; then
echo "::error::No successful Unity Cloud Build run found for SHA ${SHORT_SHA}. Is the build still in progress, or did it fail?"
exit 1
fi
RUN_NUMBER=$(echo "$RUN_JSON" | jq -r '.run_number')
ORIGINAL_EVENT=$(echo "$RUN_JSON" | jq -r '.event')

# /visual-tests is only meaningful against a PR build. If the
# latest successful Unity Cloud Build for this SHA was triggered
# by something else (push to dev, scheduled run, manual dispatch),
# the artifact path uses a different prefix and our URL would
# 404. Fail loudly so the user knows to wait for a PR build
# rather than chasing a phantom 404 inside metaforge.
if [ "$ORIGINAL_EVENT" != "pull_request" ]; then
echo "::error::Latest successful Unity Cloud Build for ${SHORT_SHA} was triggered by '${ORIGINAL_EVENT}', not 'pull_request'. Visual tests can only run against PR builds."
exit 1
fi

ARTIFACT_PATH="@dcl/${{ github.event.repository.name }}/branch/${HEAD_REF}/pr-${RUN_NUMBER}-${SHORT_SHA}"
BUILD_URL="${PUBLIC_URL_PREFIX}/${ARTIFACT_PATH}/Decentraland_macos.zip"

{
echo "number=${PR_NUMBER}"
echo "head_sha=${HEAD_SHA}"
echo "head_short_sha=${SHORT_SHA}"
echo "head_ref=${HEAD_REF}"
echo "build_url=${BUILD_URL}"
} >> "$GITHUB_OUTPUT"

- name: Look up matching explorer-automation branch
id: match
if: steps.gate.outputs.authorized == 'true'
env:
GH_TOKEN: ${{ secrets.REPOS_READ_ONLY_TOKEN }}
HEAD_REF: ${{ steps.pr.outputs.head_ref }}
run: |
set -euo pipefail
# 200 = branch exists on explorer-automation, use it as tests_ref.
# 404 = no matching branch, fall back to default (empty string =>
# metaforge resolves to main).
if gh api "repos/decentraland/explorer-automation/branches/${HEAD_REF}" --silent >/dev/null 2>&1; then
echo "tests_ref=${HEAD_REF}" >> "$GITHUB_OUTPUT"
echo "::notice::Using matching explorer-automation branch '${HEAD_REF}'."
else
echo "tests_ref=" >> "$GITHUB_OUTPUT"
echo "::notice::No matching explorer-automation branch '${HEAD_REF}' — using default."
fi

run-suite:
name: Run visual suite
needs: resolve
if: needs.resolve.outputs.authorized == 'true'
# @main pins us to the merged version of the reusable workflow so PRs to
# explorer-automation that touch run-visual-suite.yml don't accidentally
# affect every unity-explorer PR's visual run.
uses: decentraland/explorer-automation/.github/workflows/run-visual-suite.yml@main
with:
mode: test
pr_number: ${{ needs.resolve.outputs.pr_number }}
build_url: ${{ needs.resolve.outputs.build_url }}
tests_ref: ${{ needs.resolve.outputs.tests_ref }}
commit_sha: ${{ needs.resolve.outputs.head_short_sha }}
branch_label: ${{ needs.resolve.outputs.head_ref }}
secrets: inherit
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public static class AppArgsFlags
public const string SKIP_VERSION_CHECK = "skip-version-check";
public const string SIMULATE_VERSION = "simulateVersion";
public const string FORCE_MINIMUM_SPECS_SCREEN = "forceMinimumSpecsScreen";
public const string SKIP_MINIMUM_SPECS_SCREEN = "skip-minimum-specs-screen";

public const string SCENE_CONSOLE = "scene-console";

Expand Down Expand Up @@ -57,9 +58,11 @@ public static class AppArgsFlags
public const string CREATOR_HUB_BIN_PATH = "creator-hub-bin-path";

public const string USE_LOG_MATRIX = "use-log-matrix";
public const string GRAPHICS = "graphics";
public const string WINDOWED_MODE = "windowed-mode";
public const string RESOLUTION = "resolution";
public const string DISABLE_WINDOW_RESTRICTIONS = "disable-window-restrictions";
public const string DISABLE_HUD = "disable-hud";

public const string BANNED_USERS_FROM_SCENE = "include-banned-users-from-scene";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,10 +359,29 @@ await dynamicWorldContainer.UserInAppInAppInitializationFlow.ExecuteAsync(
splashScreen.Hide();
}

private static void OpenDefaultUI(IMVCManager mvcManager, CancellationToken ct)
private void OpenDefaultUI(IMVCManager mvcManager, CancellationToken ct)
{
mvcManager.ShowAsync(NewNotificationController.IssueCommand(), ct).Forget();
mvcManager.ShowAsync(MainUIController.IssueCommand(), ct).Forget();

if (appArgs.HasFlag(AppArgsFlags.DISABLE_HUD))
DisableHudOnStartupAsync(mvcManager, ct).Forget();
}

internal static async UniTask DisableHudOnStartupAsync(IMVCManager mvcManager, CancellationToken ct)
{
try
{
// Wait a frame so lazily-mounted MVC views exist before toggling.
await UniTask.NextFrame(ct).SuppressCancellationThrow();

if (ct.IsCancellationRequested)
return;

// Bypass ToggleUIRequest (used by U key) so scene SDK UIDocuments stay visible.
mvcManager.SetAllViewsCanvasActive(false);
}
catch (Exception e) { ReportHub.LogException(e, ReportCategory.STARTUP); }
}

private void InitializeDebugPanel(IDebugContainerBuilder debugContainerBuilder, UIDocument debugUiRoot)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -421,9 +421,10 @@ private async UniTask<IReadOnlyList<SpecResult>> VerifyMinimumHardwareRequiremen
new PlatformDriveInfoProvider());

bool forceShow = applicationParametersParser.HasFlag(AppArgsFlags.FORCE_MINIMUM_SPECS_SCREEN);
bool skipScreen = applicationParametersParser.HasFlag(AppArgsFlags.SKIP_MINIMUM_SPECS_SCREEN) && !forceShow;
bool hasMinimumSpecs = minimumSpecsGuard.HasMinimumSpecs() && !forceShow;

if (!hasMinimumSpecs)
if (!hasMinimumSpecs && !skipScreen)
SavedQualitySettingsApplier.EnforceLowPreset();

bool userWantsToSkip = DCLPlayerPrefs.GetBool(DCLPrefKeys.DONT_SHOW_MIN_SPECS_SCREEN);
Expand All @@ -450,7 +451,7 @@ private async UniTask<IReadOnlyList<SpecResult>> VerifyMinimumHardwareRequiremen

analytics.Track(AnalyticsEvents.General.MEETS_MINIMUM_REQUIREMENTS, specsProperties);

bool shouldShowScreen = forceShow || (!userWantsToSkip && !hasMinimumSpecs);
bool shouldShowScreen = forceShow || (!skipScreen && !userWantsToSkip && !hasMinimumSpecs);

if (!shouldShowScreen)
return minimumSpecsGuard.Results;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Cysharp.Threading.Tasks;
using Global.Dynamic;
using MVC;
using NSubstitute;
using NUnit.Framework;
using System.Threading;
using System.Threading.Tasks;

namespace Global.Tests.PlayMode
{
public class BootstraperShould
{
private IMVCManager mvcManager;

[SetUp]
public async void Setup()
{
Comment thread
pravusjif marked this conversation as resolved.
mvcManager = Substitute.For<IMVCManager>();
await UniTask.Yield();
}

[Test]
public async Task DisableHudOnStartup_OnlyTouchesMVCCanvases_NotSceneUIDocuments()
{
// Workaround for Unity bug not awaiting async Setup correctly
await UniTask.WaitUntil(() => mvcManager != null);

await Bootstrap.DisableHudOnStartupAsync(mvcManager, CancellationToken.None);

mvcManager.Received(1).SetAllViewsCanvasActive(false);
mvcManager.DidNotReceiveWithAnyArgs().SetAllViewsCanvasActive(default(IController), default);
}

[Test]
public async Task DisableHudOnStartup_DoesNothing_WhenCancelledBeforeFrameAdvances()
{
// Workaround for Unity bug not awaiting async Setup correctly
await UniTask.WaitUntil(() => mvcManager != null);

var cts = new CancellationTokenSource();
cts.Cancel();

await Bootstrap.DisableHudOnStartupAsync(mvcManager, cts.Token);

mvcManager.DidNotReceiveWithAnyArgs().SetAllViewsCanvasActive(default);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading