From b96a30f9520d5cdc58ffe557b61db75ee6059326 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 23 Mar 2026 01:22:16 +0000
Subject: [PATCH 1/3] Initial plan
From 4e97a53ce08ff51dfd9f945b5d1fc191efda8627 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 23 Mar 2026 01:34:26 +0000
Subject: [PATCH 2/3] feat: add PR preview screenshot workflow and play-mode
test
- Add .github/workflows/pr-preview.yml: triggers on PRs to main, runs a
dedicated Unity play-mode test, uploads the PNG as a GitHub Actions
artifact (14-day retention), and posts a PR comment with a direct
download link via actions/github-script
- Add Assets/Tests/PlayMode/SceneScreenshotTests.cs: loads the bundled
ProofOfConcept scene, bypasses the startup menu by advancing the
GameManager state, waits up to 4 minutes for the map build pipeline to
reach GameState.Racing, then captures a 1920x1080 PNG using
Camera.Render() + RenderTexture (works in headless/batch mode)
- Add Assets/Tests/PlayMode/SceneScreenshotTests.cs.meta: Unity metadata
Co-authored-by: adam133 <20442729+adam133@users.noreply.github.com>
Agent-Logs-Url: https://github.com/adam133/vectorroad/sessions/15e71ff2-beeb-4983-9c4b-dca9a8b4341a
---
.github/workflows/pr-preview.yml | 82 ++++++++++++++
Assets/Tests/PlayMode/SceneScreenshotTests.cs | 107 ++++++++++++++++++
.../PlayMode/SceneScreenshotTests.cs.meta | 11 ++
3 files changed, 200 insertions(+)
create mode 100644 .github/workflows/pr-preview.yml
create mode 100644 Assets/Tests/PlayMode/SceneScreenshotTests.cs
create mode 100644 Assets/Tests/PlayMode/SceneScreenshotTests.cs.meta
diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml
new file mode 100644
index 0000000..af83b59
--- /dev/null
+++ b/.github/workflows/pr-preview.yml
@@ -0,0 +1,82 @@
+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
+ 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,
+ });
diff --git a/Assets/Tests/PlayMode/SceneScreenshotTests.cs b/Assets/Tests/PlayMode/SceneScreenshotTests.cs
new file mode 100644
index 0000000..33dda60
--- /dev/null
+++ b/Assets/Tests/PlayMode/SceneScreenshotTests.cs
@@ -0,0 +1,107 @@
+using System.Collections;
+using System.IO;
+using NUnit.Framework;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using UnityEngine.TestTools;
+using VectorRoad.Core;
+
+namespace VectorRoad.Tests.PlayMode
+{
+ ///
+ /// Play-mode test that loads the default ProofOfConcept scene, waits
+ /// for the map-build pipeline to reach the
+ /// state, then renders a PNG screenshot to Screenshots/pr-preview.png
+ /// at the project root.
+ ///
+ ///
+ /// Designed to run in GitHub Actions via the pr-preview.yml workflow.
+ /// The screenshot artifact is uploaded and linked in a PR comment so
+ /// reviewers can see the rendered result at a glance.
+ ///
+ ///
+ ///
+ /// The startup menu is bypassed automatically by advancing the
+ /// state to
+ /// immediately after the scene loads.
+ ///
+ ///
+ public class SceneScreenshotTests
+ {
+ private const string SceneName = "ProofOfConcept";
+ private const int ScreenshotWidth = 1920;
+ private const int ScreenshotHeight = 1080;
+
+ ///
+ /// Loads the default location, waits for level generation to complete,
+ /// and saves a screenshot to Screenshots/pr-preview.png.
+ ///
+ [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;
+ }
+
+ // 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();
+ 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 /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}");
+ }
+ }
+}
diff --git a/Assets/Tests/PlayMode/SceneScreenshotTests.cs.meta b/Assets/Tests/PlayMode/SceneScreenshotTests.cs.meta
new file mode 100644
index 0000000..6dad483
--- /dev/null
+++ b/Assets/Tests/PlayMode/SceneScreenshotTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d053d836326f403683ba056925896fd7
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
From c9f3cc16a8ae9ce1a1908190c7e5ac71e258770b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 23 Mar 2026 01:58:44 +0000
Subject: [PATCH 3/3] fix: add issues:write permission and wait for camera to
settle before screenshot
- Add `issues: write` permission to pr-preview.yml so that
actions/github-script can post PR comments via the Issues API
(github.rest.issues.createComment requires this permission)
- Add `yield return new WaitForSeconds(3f)` after GameState.Racing so
that physics has time to settle the vehicle onto the road surface and
ChaseCam's SmoothDamp has time to move from its initial position to
behind the vehicle before the screenshot is captured
Co-authored-by: adam133 <20442729+adam133@users.noreply.github.com>
Agent-Logs-Url: https://github.com/adam133/vectorroad/sessions/5bceafe7-9ab6-4c42-977f-a4d9b35085dc
---
.github/workflows/pr-preview.yml | 1 +
Assets/Tests/PlayMode/SceneScreenshotTests.cs | 6 ++++++
2 files changed, 7 insertions(+)
diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml
index af83b59..d2514e0 100644
--- a/.github/workflows/pr-preview.yml
+++ b/.github/workflows/pr-preview.yml
@@ -19,6 +19,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
+ issues: write
pull-requests: write
steps:
diff --git a/Assets/Tests/PlayMode/SceneScreenshotTests.cs b/Assets/Tests/PlayMode/SceneScreenshotTests.cs
index 33dda60..a8db0cf 100644
--- a/Assets/Tests/PlayMode/SceneScreenshotTests.cs
+++ b/Assets/Tests/PlayMode/SceneScreenshotTests.cs
@@ -66,6 +66,12 @@ public IEnumerator DefaultLocation_RendersScene()
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