Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .github/workflows/pr-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: PR Preview Screenshot

# Loads the default location in Unity play mode, renders a screenshot, and
# posts a direct download link as a comment on the pull request.
#
# Required repository secrets
# ────────────────────────────
# UNITY_LICENSE – contents of a valid Unity .ulf license file
# UNITY_EMAIL – Unity account e-mail
# UNITY_PASSWORD – Unity account password

on:
pull_request:
branches: [ main ]

jobs:
screenshot:
name: Capture play-mode screenshot
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: true

- name: Cache Unity Library
uses: actions/cache@v4
with:
path: Library
key: Library-screenshot-${{ hashFiles('Assets/**', 'Packages/**', 'ProjectSettings/**') }}
restore-keys: |
Library-screenshot-
Library-

- name: Run play-mode screenshot test
uses: game-ci/unity-test-runner@v4
id: screenshot-test
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
testMode: playMode
testFilter: VectorRoad.Tests.PlayMode.SceneScreenshotTests
artifactsPath: TestResults/playMode
githubToken: ${{ secrets.GITHUB_TOKEN }}
checkName: PR Preview Screenshot

- name: Upload screenshot artifact
id: upload-screenshot
uses: actions/upload-artifact@v4
if: always()
with:
name: pr-preview-screenshot
path: Screenshots/pr-preview.png
if-no-files-found: warn
retention-days: 14

- name: Post PR comment with download link
uses: actions/github-script@v7
if: always()
env:
ARTIFACT_URL: ${{ steps.upload-screenshot.outputs.artifact-url }}
with:
script: |
const artifactUrl = process.env.ARTIFACT_URL;
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;

const body = artifactUrl
? `## 📸 PR Preview Screenshot\n\nA screenshot of the default location in play mode was captured for this PR.\n\n**[⬇️ Download Screenshot](${artifactUrl})**\n\n> Rendered at [workflow run](${runUrl})`
: `## 📸 PR Preview Screenshot\n\n⚠️ The screenshot could not be captured for this PR.\n\nSee the [workflow run](${runUrl}) for details.`;

github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body,
});
113 changes: 113 additions & 0 deletions Assets/Tests/PlayMode/SceneScreenshotTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System.Collections;
using System.IO;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
using VectorRoad.Core;

namespace VectorRoad.Tests.PlayMode
{
/// <summary>
/// Play-mode test that loads the default <c>ProofOfConcept</c> scene, waits
/// for the map-build pipeline to reach the <see cref="GameState.Racing"/>
/// state, then renders a PNG screenshot to <c>Screenshots/pr-preview.png</c>
/// at the project root.
///
/// <para>
/// Designed to run in GitHub Actions via the <c>pr-preview.yml</c> workflow.
/// The screenshot artifact is uploaded and linked in a PR comment so
/// reviewers can see the rendered result at a glance.
/// </para>
///
/// <para>
/// The startup menu is bypassed automatically by advancing the
/// <see cref="GameManager"/> state to <see cref="GameState.LoadingMap"/>
/// immediately after the scene loads.
/// </para>
/// </summary>
public class SceneScreenshotTests
{
private const string SceneName = "ProofOfConcept";
private const int ScreenshotWidth = 1920;
private const int ScreenshotHeight = 1080;

/// <summary>
/// Loads the default location, waits for level generation to complete,
/// and saves a screenshot to <c>Screenshots/pr-preview.png</c>.
/// </summary>
[UnityTest]
[Timeout(300000)] // 5 minutes – map build can take a while in CI
public IEnumerator DefaultLocation_RendersScene()
{
yield return SceneManager.LoadSceneAsync(SceneName);

// Allow Awake/Start to run on all objects in the loaded scene.
yield return null;

// The MapSceneBuilder waits for the GameManager to leave MainMenu
// before it starts loading map data. Advance the state here to
// skip the interactive startup menu in automated runs.
var gm = GameManager.Instance;
if (gm != null && gm.CurrentState == GameState.MainMenu)
gm.SetState(GameState.LoadingMap);

// Wait until the map build pipeline signals that the level is ready.
float elapsed = 0f;
const float mapLoadTimeout = 240f; // seconds
while (elapsed < mapLoadTimeout)
{
var instance = GameManager.Instance;
if (instance == null)
Assert.Fail("GameManager.Instance became null while waiting for map load.");
if (instance.CurrentState == GameState.Racing)
break;
elapsed += Time.deltaTime;
yield return null;
}

// Give the physics engine and ChaseCam a few seconds to settle.
// The vehicle is spawned 2 m above the road surface and needs time to
// drop onto it; the ChaseCam uses SmoothDamp so it also needs several
// frames to move from its initial position to behind the vehicle.
yield return new WaitForSeconds(3f);

// Find any active camera to render from. Camera.main returns the
// camera tagged "MainCamera", which is the expected render camera in
// the ProofOfConcept scene. FindFirstObjectByType is a safe fallback
// for scenes where the main camera tag has not been set.
var camera = Camera.main ?? Object.FindFirstObjectByType<Camera>();
Assert.IsNotNull(camera, "No Camera was found in the scene.");

// Render the scene to a RenderTexture so the capture works reliably
// in headless / batch mode (no display required).
var rt = new RenderTexture(ScreenshotWidth, ScreenshotHeight, 24);
var prevTarget = camera.targetTexture;
camera.targetTexture = rt;
camera.Render();

var tex = new Texture2D(ScreenshotWidth, ScreenshotHeight,
TextureFormat.RGB24, false);
RenderTexture.active = rt;
tex.ReadPixels(new Rect(0, 0, ScreenshotWidth, ScreenshotHeight), 0, 0);
tex.Apply();

// Save to <project root>/Screenshots/pr-preview.png so the workflow
// can locate and upload the file as an artifact.
string screenshotDir = Path.GetFullPath(
Path.Combine(Application.dataPath, "..", "Screenshots"));
Directory.CreateDirectory(screenshotDir);
string screenshotPath = Path.Combine(screenshotDir, "pr-preview.png");
File.WriteAllBytes(screenshotPath, tex.EncodeToPNG());

// Restore state and release GPU resources.
camera.targetTexture = prevTarget;
RenderTexture.active = null;
Object.Destroy(rt);
Object.Destroy(tex);

Assert.IsTrue(File.Exists(screenshotPath),
$"Screenshot was not saved to {screenshotPath}");
}
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/PlayMode/SceneScreenshotTests.cs.meta

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

Loading