From 6660dcc037bc1da7ca4651a2dc5f655f3069543b Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:40:53 -0600 Subject: [PATCH 01/37] Add Udon Test Kit as a docs project Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/doc_issue.yml | 2 +- AGENTS.md | 8 ++++--- README.md | 7 +++--- docs/udon-test-kit/intro.md | 22 ++++++++++++++++++ i18n/es/code.json | 8 ++++++- .../current/udon-test-kit/intro.md | 23 +++++++++++++++++++ sidebars.ts | 12 +++++++--- src/components/homepage/Projects.tsx | 15 ++++++++++-- 8 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 docs/udon-test-kit/intro.md create mode 100644 i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md diff --git a/.github/ISSUE_TEMPLATE/doc_issue.yml b/.github/ISSUE_TEMPLATE/doc_issue.yml index b02aaad..1e3ec08 100644 --- a/.github/ISSUE_TEMPLATE/doc_issue.yml +++ b/.github/ISSUE_TEMPLATE/doc_issue.yml @@ -13,7 +13,7 @@ body: id: project attributes: label: Project - description: Which project's docs this is about (e.g. tsvrc). + description: Which project's docs this is about (e.g. tsvrc or udon-test-kit). validations: required: true - type: dropdown diff --git a/AGENTS.md b/AGENTS.md index 81906c4..af71255 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,9 +28,11 @@ linking instead of duplicating, avoiding AI-sounding prose. `dirName` pointed straight at a leaf folder returns an unlabeled flat list, so a category's `label` has to come from the wrapping `{type: 'category', ...}` node, not a `_category_.json`. -- `tsvrc-core` (code) and this repo (docs) aren't wired together, no submodule, no - build-time fetch, and can drift. Changed public API or behavior in `tsvrc-core`, or - something under `docs/tsvrc/`? Check the other side still matches. +- Each project's code repo (`tsvrc-core` for `docs/tsvrc/`, `udon-test-kit` for + `docs/udon-test-kit/`) and this repo aren't wired together, no submodule, no build-time + fetch, and can drift. Changed public API or behavior in a project, or something under its + `docs//`? Check the other side still matches. TsVRC's testing pages describe + helpers built on Udon Test Kit, so a kit change can affect `docs/tsvrc/` too. ## Commit / PR rules diff --git a/README.md b/README.md index 1f6cc91..09b7ba8 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,10 @@ [![License: CC BY 4.0](https://img.shields.io/badge/license-CC%20BY%204.0-58a6ff?labelColor=161b22)](LICENSE) The [Docusaurus](https://docusaurus.io/) site behind [tsvrc.com](https://tsvrc.com), -documenting every project under the `tsvrc` GitHub org. Framework code lives in -[`tsvrc/tsvrc-core`](https://github.com/tsvrc/tsvrc-core); this repo is docs and content -only. +documenting every project under the `tsvrc` GitHub org. Each project's code lives in its own +repository, such as [`tsvrc/tsvrc-core`](https://github.com/tsvrc/tsvrc-core) for the +framework and [`tsvrc/udon-test-kit`](https://github.com/tsvrc/udon-test-kit) for Udon Test +Kit; this repo is docs and content only. ## Development diff --git a/docs/udon-test-kit/intro.md b/docs/udon-test-kit/intro.md new file mode 100644 index 0000000..0a0858e --- /dev/null +++ b/docs/udon-test-kit/intro.md @@ -0,0 +1,22 @@ +--- +id: intro +title: Udon Test Kit +sidebar_position: 1 +--- + +# Udon Test Kit + +Udon Test Kit lets you test a VRChat world with Unity Test Framework. Play Mode tests run a real +ClientSim session with real `VRCPlayerApi` players, and the kit works around the VRChat SDK and +ClientSim problems that otherwise break those tests. + +A test extends `ClientSimTestBase` and starts its `Session`. From there it can spawn and remove +remote players, choose who is master, give any GameObject an owner, and check what your behaviours +did with all of it. Each test can run in a scene of its own, including your world's own scene. +Spies record the calls your code makes by method name, such as `SendCustomEvent`, and a menu item +creates the test assemblies a world needs in one step. + +The kit compiles only when Unity Test Framework includes tests, so nothing from it reaches a world +build. It works in any world built with UdonSharp, with or without TsVRC. TsVRC's own testing +helpers build on it: if your world uses the framework, see also +[Testing your world](/docs/tsvrc/testing/testing-your-world). diff --git a/i18n/es/code.json b/i18n/es/code.json index 70748de..bbf370d 100644 --- a/i18n/es/code.json +++ b/i18n/es/code.json @@ -35,6 +35,12 @@ "homepage.projects.learnMore": { "message": "Más información" }, + "homepage.projects.udonTestKit.description": { + "message": "Tests automatizados para mundos de VRChat: los tests de modo Play ejecutan una sesión real de ClientSim con jugadores reales, sin los problemas del SDK que de otro modo los rompen." + }, + "homepage.projects.readTheDocs": { + "message": "Leer la documentación" + }, "homepage.projects.status.available": { "message": "Disponible" }, @@ -45,7 +51,7 @@ "message": "Proyectos" }, "homepage.projects.description": { - "message": "Cada proyecto de TsVRC vive en este mismo sitio. Por ahora solo hay uno; más llegarán aquí a medida que se publiquen." + "message": "Cada proyecto de TsVRC vive en este mismo sitio, y los nuevos llegarán aquí a medida que se publiquen." }, "homepage.meta.title": { "message": "TsVRC" diff --git a/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md b/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md new file mode 100644 index 0000000..5e068be --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md @@ -0,0 +1,23 @@ +--- +id: intro +title: Udon Test Kit +sidebar_position: 1 +--- + +# Udon Test Kit + +Udon Test Kit te permite probar un mundo de VRChat con Unity Test Framework. Los tests de modo Play +ejecutan una sesión real de ClientSim con jugadores `VRCPlayerApi` reales, y el kit resuelve los +problemas del VRChat SDK y de ClientSim que de otro modo rompen esos tests. + +Un test extiende `ClientSimTestBase` e inicia su `Session`. A partir de ahí puede crear y quitar +jugadores remotos, elegir quién es el master, darle un dueño a cualquier GameObject y comprobar qué +hicieron tus behaviours con todo eso. Cada test puede ejecutarse en una escena propia, incluida la +escena de tu propio mundo. Los spies registran las llamadas que tu código hace por nombre de +método, como `SendCustomEvent`, y un elemento de menú crea en un solo paso los assemblies de test +que necesita un mundo. + +El kit solo se compila cuando Unity Test Framework incluye los tests, así que nada de él llega a la +build de un mundo. Funciona en cualquier mundo hecho con UdonSharp, con o sin TsVRC. Las +herramientas de testing de TsVRC se apoyan en él: si tu mundo usa el framework, consulta también +[Probar tu mundo](/docs/tsvrc/testing/testing-your-world). diff --git a/sidebars.ts b/sidebars.ts index 4aba22e..ea7342c 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -5,9 +5,9 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; /** * Each project under docs// gets one top-level category here, built from * autogenerated subtrees so individual pages never need a sidebar entry of their own. - * Adding a second project means adding its own top-level category the same way tsvrc's - * is built below, not relying on blanket autogeneration (see CLAUDE.md's Structure - * section for why). + * Adding another project means adding its own top-level category the same way tsvrc's + * and udon-test-kit's are built below, not relying on blanket autogeneration (see + * AGENTS.md's Gotchas section for why). * * Each labeled category below wraps a single-folder `autogenerated` subtree: pointing * `dirName` straight at a leaf folder returns its files as a flat list with no label of @@ -121,6 +121,12 @@ const sidebars: SidebarsConfig = { }, ], }, + { + type: 'category', + label: 'Udon Test Kit', + link: {type: 'doc', id: 'udon-test-kit/intro'}, + items: [], + }, ], }; diff --git a/src/components/homepage/Projects.tsx b/src/components/homepage/Projects.tsx index bac17aa..a2dd74e 100644 --- a/src/components/homepage/Projects.tsx +++ b/src/components/homepage/Projects.tsx @@ -15,6 +15,18 @@ function useProjects(): Project[] { href: '/framework', cta: translate({ id: 'homepage.projects.learnMore', message: 'Learn more' }), }, + { + name: 'Udon Test Kit', + status: 'available', + description: translate({ + id: 'homepage.projects.udonTestKit.description', + message: + 'Automated tests for VRChat worlds: Play Mode tests run a real ClientSim session with ' + + 'real players, without the SDK problems that otherwise break them.', + }), + href: '/docs/udon-test-kit/intro', + cta: translate({ id: 'homepage.projects.readTheDocs', message: 'Read the docs' }), + }, ]; } @@ -27,8 +39,7 @@ export default function Projects() {

- Every TsVRC project lives on this one site. Just one so far; more will land here as - they ship. + Every TsVRC project lives on this one site, and new ones will land here as they ship.

From 3dca161b574ce4fa0a9c418826459a07ad567980 Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:42:50 -0600 Subject: [PATCH 02/37] Keep Udon Test Kit's intro independent of TsVRC Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- docs/udon-test-kit/intro.md | 4 +--- .../current/udon-test-kit/intro.md | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/udon-test-kit/intro.md b/docs/udon-test-kit/intro.md index 0a0858e..f712227 100644 --- a/docs/udon-test-kit/intro.md +++ b/docs/udon-test-kit/intro.md @@ -17,6 +17,4 @@ Spies record the calls your code makes by method name, such as `SendCustomEvent` creates the test assemblies a world needs in one step. The kit compiles only when Unity Test Framework includes tests, so nothing from it reaches a world -build. It works in any world built with UdonSharp, with or without TsVRC. TsVRC's own testing -helpers build on it: if your world uses the framework, see also -[Testing your world](/docs/tsvrc/testing/testing-your-world). +build. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md b/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md index 5e068be..602d95c 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md +++ b/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md @@ -18,6 +18,4 @@ método, como `SendCustomEvent`, y un elemento de menú crea en un solo paso los que necesita un mundo. El kit solo se compila cuando Unity Test Framework incluye los tests, así que nada de él llega a la -build de un mundo. Funciona en cualquier mundo hecho con UdonSharp, con o sin TsVRC. Las -herramientas de testing de TsVRC se apoyan en él: si tu mundo usa el framework, consulta también -[Probar tu mundo](/docs/tsvrc/testing/testing-your-world). +build de un mundo. From 5eb8537ecc876ffe88aeefc08896449de1d05e3f Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:47:00 -0600 Subject: [PATCH 03/37] Add Udon Test Kit reference and explanation pages Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- AGENTS.md | 3 + .../a-fresh-session-for-every-test.md | 47 +++++++++ .../explanations/what-the-kit-works-around.md | 77 +++++++++++++++ .../reference/callback-spy-and-call-log.md | 77 +++++++++++++++ .../reference/clientsim-project-settings.md | 48 +++++++++ .../reference/clientsim-session.md | 99 +++++++++++++++++++ .../reference/clientsim-test-base.md | 70 +++++++++++++ .../reference/invoke-unity-tests.md | 82 +++++++++++++++ .../reference/private-field-access.md | 49 +++++++++ .../reference/test-assembly-creator.md | 56 +++++++++++ docs/udon-test-kit/reference/test-run.md | 44 +++++++++ docs/udon-test-kit/reference/test-scene.md | 81 +++++++++++++++ sidebars.ts | 15 ++- 13 files changed, 747 insertions(+), 1 deletion(-) create mode 100644 docs/udon-test-kit/explanations/a-fresh-session-for-every-test.md create mode 100644 docs/udon-test-kit/explanations/what-the-kit-works-around.md create mode 100644 docs/udon-test-kit/reference/callback-spy-and-call-log.md create mode 100644 docs/udon-test-kit/reference/clientsim-project-settings.md create mode 100644 docs/udon-test-kit/reference/clientsim-session.md create mode 100644 docs/udon-test-kit/reference/clientsim-test-base.md create mode 100644 docs/udon-test-kit/reference/invoke-unity-tests.md create mode 100644 docs/udon-test-kit/reference/private-field-access.md create mode 100644 docs/udon-test-kit/reference/test-assembly-creator.md create mode 100644 docs/udon-test-kit/reference/test-run.md create mode 100644 docs/udon-test-kit/reference/test-scene.md diff --git a/AGENTS.md b/AGENTS.md index af71255..223bd0a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,9 @@ linking instead of duplicating, avoiding AI-sounding prose. `dirName` pointed straight at a leaf folder returns an unlabeled flat list, so a category's `label` has to come from the wrapping `{type: 'category', ...}` node, not a `_category_.json`. +- Every project shares one sidebar, so two categories with the same label (each project's + "Reference", for example) need a unique `key`, or the `es` build fails with "Multiple docs + sidebar items produce the same translation key". - Each project's code repo (`tsvrc-core` for `docs/tsvrc/`, `udon-test-kit` for `docs/udon-test-kit/`) and this repo aren't wired together, no submodule, no build-time fetch, and can drift. Changed public API or behavior in a project, or something under its diff --git a/docs/udon-test-kit/explanations/a-fresh-session-for-every-test.md b/docs/udon-test-kit/explanations/a-fresh-session-for-every-test.md new file mode 100644 index 0000000..e8f9430 --- /dev/null +++ b/docs/udon-test-kit/explanations/a-fresh-session-for-every-test.md @@ -0,0 +1,47 @@ +--- +id: a-fresh-session-for-every-test +title: "Decision note: a fresh session for every test" +sidebar_position: 2 +--- + +# Decision note: a fresh session for every test + +[`ClientSimTestBase`](../reference/clientsim-test-base) ends its ClientSim session after every +test, and the next test starts its own. A session ends completely: `End` leaves nothing of +ClientSim running. The one thing that carries over is saved data, and the kit never deletes it +unless a test asks. + +## Why tests don't share a session + +A test that starts from a fresh session can't depend on what an earlier test did, so each test +passes or fails on its own, in any order. NUnit doesn't promise an order between tests, so a test +that only passes after another one fails at random. In xUnit Test Patterns' terms, this is a +fresh fixture. + +Ending a session completely also lets one test use more than one session. A test can end a session +and start another in the same scene to check that data survives a rejoin, without relying on the +order of tests. + +## Why saved data carries over + +Carrying over is what saved data is for, and ClientSim keeps it in files, not in the session. The +kit never deletes those files on its own. In your world's own scene, they're your ClientSim data, +and some worlds want them to carry over between tests. `ClientSimSession.DeleteSavedData` is there +for a test that needs none. + +## Alternatives considered + +- **One session for a whole test class** — faster, since ClientSim starts once, but every test + then shares whatever the earlier ones changed: players, owners, saved data and every object in + the scene. That's xUnit Test Patterns' shared fixture, and its known cost is tests that depend on + each other. It stays possible for tests that only read, by starting the session in a + `[UnitySetUp]` when it isn't running yet and ending it in `[OneTimeTearDown]`. +- **Deleting saved data after every test** — every test would start clean, but tests run in the + world's own scene would delete the developer's ClientSim data, and a test of data carrying over + couldn't be written. + +## Scenes follow the same rule + +Every test in a Play Mode run shares Unity Test Framework's test scene by default. A +[`TestScene`](../reference/test-scene) with the default `PerTest` lifetime gives each test a fresh +copy of its own scene, and `PerFixture` is the shared option, for tests that don't change it. diff --git a/docs/udon-test-kit/explanations/what-the-kit-works-around.md b/docs/udon-test-kit/explanations/what-the-kit-works-around.md new file mode 100644 index 0000000..2706bab --- /dev/null +++ b/docs/udon-test-kit/explanations/what-the-kit-works-around.md @@ -0,0 +1,77 @@ +--- +id: what-the-kit-works-around +title: What the kit works around +sidebar_position: 1 +--- + +# What the kit works around + +Unity Test Framework, the VRChat SDK and ClientSim each work on their own. Put together in a Play +Mode test, a few of their behaviors break the test, or make it pass or fail for the wrong reason. +This page lists each one and what the kit does about it. + +## Play Mode runs that never finish + +When Play Mode starts, the VRChat SDK's `UnityEventFilter` strips every persistent UnityEvent +listener whose type it doesn't allow, and a stripped listener is never called. Unity Test +Framework's Play Mode runner reports results through listeners of four such types. Without them, a +Play Mode run never reports its results, and a batch-mode run never exits. + +The kit adds those four types to the SDK's allowlist whenever the Editor loads it, before any test +runs. Nothing needs turning on. The allowlist is internal to the SDK, so if a future SDK moves it, +the kit logs a warning starting with `[UdonTestKit] UnityEventFilterAllowlist:` instead of failing. + +## Tests that fail without saying why + +ClientSim checks a handful of project settings as it starts. When the check fails, it opens its +settings window, which in batch mode logs errors, and Unity Test Framework fails a test on any +error log it didn't expect. The test fails, and nothing in the result names a setting. + +`ClientSimSession.Start` runs the same check first and fails the test with the names of the +failing settings and the menu item that fixes them. See +[ClientSimProjectSettings](../reference/clientsim-project-settings). + +## Saved data a finished session overwrites + +ClientSim keeps each player's saved data on a hidden object that its own teardown never destroys. +After ClientSim stops, that object keeps rewriting the player's save file. The next session in the +same scene reads the same file, and within a second the leftover object overwrites what the new +session saved, so a test of saved data passes or fails depending on timing. + +`ClientSimSession.End` destroys that object. + +## Saved data restored after ClientSim is ready + +ClientSim reports that it's ready before it restores the local player's saved data, and a +PlayerData change made before the restore is never saved. `ClientSimSession.Start` returns only +after the restore. + +## Owners that never change + +In VRChat, every networked GameObject has an owner that can change. ClientSim tracks an owner only +for GameObjects with `VRCObjectSync` or `VRCObjectPool`. The master owns every other one, and +`Networking.SetOwner` can't change that, so code that behaves differently for a player who isn't +the owner can't be tested. `ClientSimSession.SetOwner` gives any GameObject an owner of its own. +See [ClientSimSession](../reference/clientsim-session#ownership). + +## A test scene with no scene descriptor + +ClientSim refuses to start without a scene descriptor, and Unity Test Framework's test scene has +none. `ClientSimSession.Start` adds one when no loaded scene has one. + +VRChat Worlds SDK versions before 3.8.2 also stop Play Mode outright when the scene has no +descriptor, before any test code runs, so every Play Mode run hangs. Nothing in a test can work +around that, which is why the kit requires 3.8.2 or later. + +## A crash after every test passed + +`EditorSceneManager.LoadSceneInPlayMode`, which loads a scene asset into a Play Mode test, drops +the operation it loads with. When the garbage collector finalized that operation as Unity exited, +Unity crashed after every test had passed, so a command-line run reported failure. `TestScene` +finalizes it right after the scene loads, while that's still safe. + +## Test code in a world build + +The kit compiles only when Unity Test Framework includes tests (`UNITY_INCLUDE_TESTS`), and so do +the test assemblies it creates. A world build never includes them, so the kit can stay installed +in a world you upload. diff --git a/docs/udon-test-kit/reference/callback-spy-and-call-log.md b/docs/udon-test-kit/reference/callback-spy-and-call-log.md new file mode 100644 index 0000000..94313e1 --- /dev/null +++ b/docs/udon-test-kit/reference/callback-spy-and-call-log.md @@ -0,0 +1,77 @@ +--- +id: callback-spy-and-call-log +title: CallbackSpy and CallLog +sidebar_position: 4 +--- + +# CallbackSpy and CallLog + +`UdonTestKit.CallbackSpy` is a test spy for code that calls an UdonSharpBehaviour's methods by +name, such as with `SendCustomEvent`. `UdonTestKit.CallLog` creates spies and records every call +they receive, in order, as `"GameObjectName.CallbackName"`. + +```csharp +using var calls = new CallLog(); +CallbackSpy listener = calls.CreateSpy("Listener"); +scoreboard.Subscribe(listener, nameof(CallbackSpy.CallbackA)); + +scoreboard.AddPoint(); + +Assert.That(calls, Is.EqualTo(new[] { "Listener.CallbackA" })); +``` + +Both work in Edit Mode and Play Mode tests. + +## CallLog + +`CallLog` is an `IReadOnlyList` of the calls so far, so NUnit constraints such as +`Is.EqualTo(new[] { ... })` and `Has.Member(...)` work on it directly. It's also `IDisposable`. + +- `CreateSpy(string name = "Spy")` creates a GameObject with that name in the active scene, adds a + `CallbackSpy` to it, and returns the spy. +- `CreateSpy(string name)` does the same with your own subclass of `CallbackSpy`. +- `Clear()` forgets the calls recorded so far, such as the ones a test's setup caused. It doesn't + destroy any spy. +- `Dispose()` destroys the GameObject of every spy it created, skipping any already destroyed. + `using var calls = new CallLog();` is all the cleanup a test needs. + +Spies created from the same log record into it in the order the calls arrive, so one assertion +checks both how many calls each spy got and their order across spies. + +## CallbackSpy + +`CallbackSpy` is an `UdonSharpBehaviour` with two callbacks, `CallbackA()` and `CallbackB()`, which +cover most tests. Each call is recorded under the spy's GameObject name at the time of the call. + +For callbacks named after what your code calls, subclass it and call `Record()` from each method. +`Record` takes the name of the method that called it: + +```csharp +public class DoorSpy : CallbackSpy +{ + public void Open() => Record(); + public void Close() => Record(); +} +``` + +`calls.CreateSpy("Door")` then records `"Door.Open"` and `"Door.Close"`. + +Put subclasses in an assembly that compiles for every platform, such as the `Doubles` assembly +[`TestAssemblyCreator`](./test-assembly-creator) creates. Unity won't add a behaviour from an +Editor-only assembly to a GameObject, and an Edit Mode test assembly is Editor-only. + +## Calls by name outside Udon + +Tests run your UdonSharpBehaviours as plain C#, not as Udon. In that mode, `SendCustomEvent` +calls a public method with no parameters and exactly the given name. Any other name does nothing +and raises no error, so a misspelled callback name passes silently: check what the log recorded, +not just that nothing failed. + +`SendCustomEventDelayedSeconds` and `SendCustomEventDelayedFrames` don't call anything outside +Udon. Code under test that relies on them needs its delayed method called directly from the test. + +## Failure modes + +- A `CallbackSpy` that a `CallLog` didn't create throws `InvalidOperationException` on its first + call, naming the spy and the callback. Add a spy with `CreateSpy`, not `AddComponent`. +- Spies record names only, never arguments. diff --git a/docs/udon-test-kit/reference/clientsim-project-settings.md b/docs/udon-test-kit/reference/clientsim-project-settings.md new file mode 100644 index 0000000..6f07a9f --- /dev/null +++ b/docs/udon-test-kit/reference/clientsim-project-settings.md @@ -0,0 +1,48 @@ +--- +id: clientsim-project-settings +title: ClientSimProjectSettings +sidebar_position: 7 +--- + +# ClientSimProjectSettings + +`UdonTestKit.ClientSimProjectSettings` knows the project settings ClientSim checks before it +starts, and the change that makes each one pass. Its menu item, **Tools > Udon Test Kit > Fix +ClientSim Project Settings**, applies the change for every setting that fails. + +## The settings + +| Setting | Change it makes | +| --- | --- | +| Input axes | ClientSim's own input axes, written over `ProjectSettings/InputManager.asset`. | +| Input type | **Active Input Handling** set to both input systems. | +| Audio spatializer | ClientSim's own audio settings. | +| VRChat layers | VRChat's layers, through the VRChat SDK. | +| Collision matrix | VRChat's layer collision matrix, through the VRChat SDK. | + +All but one use ClientSim's or the VRChat SDK's own fix. For the input type, ClientSim's own +setter picks the Input System alone, which ClientSim's check then rejects, so the kit sets both +input systems, the value ClientSim's settings window asks for. + +## Members + +- `Fix()` is the menu item. It applies the change for each setting that fails ClientSim's check and + logs the ones it changed, or that they were already correct. Settings that pass are left alone. +- `FixMenuPath` is the menu item's path, `"Tools/Udon Test Kit/Fix ClientSim Project Settings"`, + for tooling that runs it with `EditorApplication.ExecuteMenuItem`. + +## When it matters + +[`ClientSimSession.Start`](./clientsim-session) runs ClientSim's check before ClientSim starts. When +the check fails, `Start` fails the test with a message such as: + +``` +ClientSim's project settings check fails on: input type, collision matrix. Fix them with Tools > Udon Test Kit > Fix ClientSim Project Settings. +``` + +Without that, ClientSim would open its settings window as it starts. In batch mode, that window +logs errors, and Unity Test Framework fails a test on any error log it didn't expect, so the test +fails without naming any setting. + +The fix changes files under `ProjectSettings`. Commit them, so the check passes on every machine +that runs the tests, including a build server. diff --git a/docs/udon-test-kit/reference/clientsim-session.md b/docs/udon-test-kit/reference/clientsim-session.md new file mode 100644 index 0000000..e2463a2 --- /dev/null +++ b/docs/udon-test-kit/reference/clientsim-session.md @@ -0,0 +1,99 @@ +--- +id: clientsim-session +title: ClientSimSession +sidebar_position: 1 +--- + +# ClientSimSession + +`UdonTestKit.ClientSimSession` starts ClientSim for a Play Mode test, gives the test real +`VRCPlayerApi` players, and ends ClientSim so that nothing of it is left running. +[`ClientSimTestBase`](./clientsim-test-base) holds one and ends it after every test; create one +yourself only when your test class doesn't extend it. + +```csharp +var session = new ClientSimSession(); +yield return session.Start(); + +VRCPlayerApi remote = session.SpawnRemotePlayer("Remote"); +// ... + +session.End(); +``` + +## Starting + +`Start(bool localPlayerIsMaster = true)` is a coroutine, so `yield return` it from a `[UnityTest]` +or a `[UnitySetUp]`. It returns once the local player has joined and, with player persistence on, +once that player's saved data is restored. ClientSim reports that it's ready before the restore, +and a PlayerData change made in between is never saved, so returning on "ready" alone would lose +the first thing a test writes. + +Before ClientSim starts, `Start` does two things: + +1. It runs the same project settings check ClientSim runs, and fails the test if it doesn't pass. + The failure message names each setting that fails and the menu item that fixes them, described + in [ClientSimProjectSettings](./clientsim-project-settings). +2. When no loaded scene has a scene descriptor, it adds one: a GameObject named + `__TestSceneDescriptor` with a single spawn point at the origin. ClientSim refuses to start + without a descriptor. When a scene already has one, such as your world's own scene, the + session adds nothing and the local player spawns at that descriptor's spawn point. + +With `localPlayerIsMaster: false`, ClientSim spawns a remote player before the local one, and +that remote player is the master. The session then starts with two players. + +If ClientSim isn't ready within 10 seconds, `Start` throws a `TimeoutException`. + +## Players + +`SpawnRemotePlayer(string displayName)` spawns a remote player and returns it. The player is in +the player list by the time the call returns, so a test can assert on it in the same frame. The +returned player is looked up by display name, so give every player its own. + +`RemovePlayer(VRCPlayerApi player)` removes a player, also within the call. + +`ClientSimSession.FindPlayerByName(string displayName)` is static and returns the player with that +display name, or `null` when there's none. + +## Ownership + +`SetOwner(VRCPlayerApi player, GameObject obj)` makes `player` the owner of `obj`, the way +`Networking.SetOwner` does in VRChat. + +ClientSim tracks an owner only for GameObjects with `VRCObjectSync` or `VRCObjectPool`. The master +owns every other GameObject, including one holding an UdonSharpBehaviour, and +`Networking.SetOwner` leaves it unchanged. In VRChat, every networked GameObject's owner can +change, so code that takes ownership can't be tested against ClientSim as it is. + +`SetOwner` gives such a GameObject an owner of its own. It adds a hidden component that ClientSim +reads and writes ownership through, starting from the GameObject's current owner, the master, as +in VRChat. Then it moves ownership to `player`. From then on, `Networking.SetOwner` calls on that +GameObject move its ownership too, including the ones the code under test makes. Calling +`SetOwner` again reuses the component instead of adding another. On a GameObject that already has +`VRCObjectSync` or `VRCObjectPool`, it only calls `Networking.SetOwner`. + +## Ending + +`End()` stops ClientSim, stops it saving player data, and removes the scene descriptor `Start` +added, if it added one. It's safe to call when no session is running, so a teardown can call it +unconditionally. + +ClientSim keeps each player's saved data on a hidden object that its own teardown never destroys. +That object keeps rewriting the player's save file after ClientSim stops, and overwrites whatever +a later session in the same scene saves. `End` destroys it. With nothing left running, the next +session in the same scene starts from the data the last one saved, the way a player who leaves and +rejoins does. + +## Saved data + +ClientSim saves each player's data in your project folder, one file per player and scene: + +- `ClientSimStorage/PlayerData/PlayerData__.json` +- `ClientSimStorage/PlayerObjects/PlayerObject__.json` + +`ClientSimSession.DeleteSavedData()` is static and deletes both kinds of file for the active scene, +for every player. Nothing else in the kit deletes saved data: in your world's own scene, those +files are your ClientSim data. + +Unity Test Framework gives its test scene a new name on every run, so each run leaves a few files +in `ClientSimStorage` that no later run reads. You can delete them whenever you like. diff --git a/docs/udon-test-kit/reference/clientsim-test-base.md b/docs/udon-test-kit/reference/clientsim-test-base.md new file mode 100644 index 0000000..7d6137d --- /dev/null +++ b/docs/udon-test-kit/reference/clientsim-test-base.md @@ -0,0 +1,70 @@ +--- +id: clientsim-test-base +title: ClientSimTestBase +sidebar_position: 2 +--- + +# ClientSimTestBase + +`UdonTestKit.ClientSimTestBase` is an abstract base class for Play Mode test fixtures that use +ClientSim. It holds a [`ClientSimSession`](./clientsim-session) in its protected `Session` field +and ends it after every test, and it can run every test in a [`TestScene`](./test-scene) of its +own. + +```csharp +public class PlayerCountTests : ClientSimTestBase +{ + [UnityTest] + public IEnumerator SpawnRemotePlayer_InClientSim_JoinsThePlayerList() + { + yield return Session.Start(); + + VRCPlayerApi remote = Session.SpawnRemotePlayer("Remote"); + + Assert.That(VRCPlayerApi.GetPlayerCount(), Is.EqualTo(2)); + } +} +``` + +The session doesn't start on its own. Each test starts it with `yield return Session.Start()`, so +a test can choose `localPlayerIsMaster: false`, or do work before ClientSim exists. + +## Constructors + +- `ClientSimTestBase()` runs every test in Unity Test Framework's own test scene, which the whole + run shares. +- `ClientSimTestBase(TestScene scene, TestSceneLifetime lifetime = TestSceneLifetime.PerTest)` runs + every test in `scene`. With `PerTest`, the scene is loaded fresh for each test and unloaded after + it. With `PerFixture`, it's loaded for the first test and unloaded after the last one. + +Pass the scene from your fixture's own constructor: + +```csharp +public class LobbyTests : ClientSimTestBase +{ + public LobbyTests() : base(TestScene.Empty("Lobby")) + { + } +} +``` + +## Lifecycle + +The base class adds four NUnit and Unity Test Framework methods. NUnit runs a base class's setup +before the derived class's and its teardown after, and Unity Test Framework runs every +`[UnityTearDown]` after every `[TearDown]`. Put together, a test with a derived class runs in this +order: + +1. `[UnitySetUp]` `LoadTestScene` loads the scene, if there is one, and makes it the active scene. +2. Your `[UnitySetUp]` and `[SetUp]`. +3. The test, which starts the session. +4. Your `[TearDown]`, while the session is still running. +5. `[TearDown]` `EndClientSimSession` calls `Session.End()`. +6. Your `[UnityTearDown]`, after ClientSim has ended and before the scene unloads. +7. `[UnityTearDown]` `UnloadPerTestScene` unloads a `PerTest` scene, destroying everything in it. + +After the fixture's last test, `[OneTimeTearDown]` `UnloadPerFixtureScene` unloads a `PerFixture` +scene. + +`Session.End()` runs after every test, even when the test failed or never started the session, +because ending a session that isn't running does nothing. diff --git a/docs/udon-test-kit/reference/invoke-unity-tests.md b/docs/udon-test-kit/reference/invoke-unity-tests.md new file mode 100644 index 0000000..f12f226 --- /dev/null +++ b/docs/udon-test-kit/reference/invoke-unity-tests.md @@ -0,0 +1,82 @@ +--- +id: invoke-unity-tests +title: Invoke-UnityTests.ps1 +sidebar_position: 9 +--- + +# Invoke-UnityTests.ps1 + +`Invoke-UnityTests.ps1` runs a Unity project's tests from PowerShell on Windows and prints a short +report. It's a single, standalone file: it doesn't use the kit or anything else beyond Unity Test +Framework, and it's meant to be copied into any Unity project that wants it, with or without the +kit. + +## Getting it + +The script is `Scripts~/Invoke-UnityTests.ps1` in the +[kit's repository](https://github.com/tsvrc/udon-test-kit). A Creator Companion install also has +it on disk, under `Packages/com.tsvrc.udon-test-kit/Scripts~`. Unity doesn't import folders ending +in `~`, so a `.unitypackage` of the kit can't include it. + +Copy it anywhere inside your project, such as its root folder. It finds the project by looking up +from its own folder. + +## Running it + +Close the Unity Editor first: batch mode can't open a project that another Editor has open. Then: + +```powershell +.\Invoke-UnityTests.ps1 +``` + +`Get-Help .\Invoke-UnityTests.ps1 -Detailed` shows the same options as the table below. + +## Options + +| Option | Default | +| --- | --- | +| `-TestMode` | `All`, which runs `EditMode` and then `PlayMode`. Pass one of those to run one mode. | +| `-AssemblyNames` | Your project's own assemblies, separated with `;` (see below). | +| `-ProjectPath` | The nearest folder with a `ProjectSettings\ProjectVersion.txt`, looking up from the script's folder, then from the current folder. | +| `-UnityPath` | The Editor version in `ProjectVersion.txt`, in Unity Hub's default install folder under `Program Files`. | +| `-ResultsPath` | A new folder under the temp folder, named after the date and time. | + +Without assembly names, Unity runs every test it finds, including the tests that come with +packages, such as the VRChat SDK's. The default list is every assembly definition under `Assets`, +plus the assemblies Unity compiles scripts without one into (`Assembly-CSharp`, +`Assembly-CSharp-firstpass`, `Assembly-CSharp-Editor` and `Assembly-CSharp-Editor-firstpass`), so +tests in `Editor` folders run too. A name with no tests in it is harmless. + +## What it does + +Each test mode starts Unity once, in batch mode, with `-runTests`. Unity writes the mode's results +to `.xml` and its log to `.log` in the results folder. + +For each mode, the script prints every test that didn't pass, with its result and message, then a +count: + +``` +PlayMode tests... +FAILED DoorTests.Open_ByNonOwner_StaysClosed + Expected: False + But was: True +12 passed, 1 failed, 0 inconclusive or skipped +``` + +An inconclusive test is an `Assume` that didn't hold, so it proved nothing. + +## Exit code + +The script exits with 1 when Unity reported a failed or inconclusive test, or a run that errored +out, and when a mode wrote no results, in which case it prints the log's path. Otherwise it exits +with 0, so a build server can use it as a step that fails the build. + +## Execution policy + +If PowerShell refuses to run the script because of its execution policy, allow local scripts once: + +```powershell +Set-ExecutionPolicy -Scope CurrentUser RemoteSigned +``` + +It's been run with Windows PowerShell 5.1, which comes with Windows. diff --git a/docs/udon-test-kit/reference/private-field-access.md b/docs/udon-test-kit/reference/private-field-access.md new file mode 100644 index 0000000..a11ec96 --- /dev/null +++ b/docs/udon-test-kit/reference/private-field-access.md @@ -0,0 +1,49 @@ +--- +id: private-field-access +title: PrivateFieldAccess +sidebar_position: 5 +--- + +# PrivateFieldAccess + +`UdonTestKit.PrivateFieldAccess` reads and writes fields, and calls methods, by name on any object +or type, whatever their access level. Use it to arrange or check state that a behaviour's public +members don't expose, without making that state public just for a test. + +```csharp +int remaining = PrivateFieldAccess.GetField(timer, "_remainingSeconds"); +PrivateFieldAccess.SetField(timer, "_remainingSeconds", 1); +PrivateFieldAccess.InvokeInstance(timer, "Tick"); +``` + +It works in Edit Mode and Play Mode tests. + +## Members + +| Member | Does | +| --- | --- | +| `SetField(object target, string fieldName, object value)` | Sets an instance field. | +| `SetField(Type staticType, string fieldName, object value)` | Sets a static field. | +| `GetField(object target, string fieldName)` | Returns an instance field's value as `T`. | +| `GetField(Type staticType, string fieldName)` | Returns a static field's value as `T`. | +| `InvokeInstance(object target, string methodName, params object[] args)` | Calls an instance method and returns its result. | +| `InvokeStatic(Type staticType, string methodName, params object[] args)` | Calls a static method and returns its result. | + +For a static class, pass `typeof(MyStaticClass)` as the type. + +## Lookup + +Each call looks for the member on the target's type, then on each base class in turn, and takes +the first match, public or not. `Type.GetField` and `Type.GetMethod` alone don't find a private +member declared on a base class, which is the usual case for state a framework base class keeps. + +## Failure modes + +- A name that isn't found on the type or any base class fails the test with a message such as + `Field '_remaining' not found on Timer.` A renamed member shows up as a failing test, not as a + silent no-op. +- Methods are found by name alone. A type that declares several overloads of the method throws + `AmbiguousMatchException`. +- `GetField` casts the value to `T`, so a wrong `T` throws `InvalidCastException`. +- An exception thrown by a method called through `InvokeInstance` or `InvokeStatic` reaches the + test wrapped in a `TargetInvocationException`; its `InnerException` is the original. diff --git a/docs/udon-test-kit/reference/test-assembly-creator.md b/docs/udon-test-kit/reference/test-assembly-creator.md new file mode 100644 index 0000000..d07a3da --- /dev/null +++ b/docs/udon-test-kit/reference/test-assembly-creator.md @@ -0,0 +1,56 @@ +--- +id: test-assembly-creator +title: TestAssemblyCreator +sidebar_position: 8 +--- + +# TestAssemblyCreator + +`UdonTestKit.TestAssemblyCreator` creates the test assemblies a world needs: an Edit Mode test +assembly, a Play Mode test assembly, and a test doubles assembly both of them reference. Run it from +**Assets > Create > Udon Test Kit > Test Assemblies**, or from the Project window's right-click +**Create** menu. + +## Where it creates them + +It creates a `Tests` folder inside the folder selected in the Project window, or inside the folder +of a selected file. When nothing under `Assets` is selected, it uses `Assets`. An existing `Tests` +folder is left alone, and the new one is named `Tests 1`, `Tests 2` and so on. + +## What it creates + +The assembly names start with your project's **Product Name** from Player Settings, without spaces +or symbols. A name that ends up empty, or starts with a digit, becomes `World`. For a product named +"My World": + +| Folder | Assembly | Platforms | Example | +| --- | --- | --- | --- | +| `Tests/EditMode` | `MyWorld.Tests.EditMode` | Editor | `ExampleEditModeTests.cs` | +| `Tests/PlayMode` | `MyWorld.Tests.PlayMode` | every platform | `ExamplePlayModeTests.cs` | +| `Tests/Doubles` | `MyWorld.Tests.Doubles` | every platform | `ExampleSpy.cs` | + +When assemblies with those names already exist, the new ones get a number, such as +`MyWorld2.Tests.EditMode`. Two assemblies with the same name stop a project compiling. + +Each assembly references the kit, UdonSharp, the VRChat SDK, ClientSim, and every assembly +definition of yours with scripts under `Assets`, other than test assemblies. The two test assemblies +also reference the doubles assembly and Unity Test Framework. All three compile only when Unity +Test Framework includes tests, so none of them reaches a world build, and none is referenced +automatically by other assemblies. + +The `Doubles` assembly is for behaviours your tests add to GameObjects, such as +[`CallbackSpy`](./callback-spy-and-call-log) subclasses. It has no NUnit reference. Unity won't add +a behaviour from the Editor-only Edit Mode assembly to a GameObject, and Unity requires Edit Mode +and Play Mode tests to be in separate assemblies, so behaviours both kinds of test use go here. + +Each example compiles and passes as created. + +## Edge cases + +- The list of your assemblies is read once, when you create the test assemblies. Add an assembly + definition later, and add it to the test assemblies' references yourself. +- Scripts without an assembly definition compile into `Assembly-CSharp`, which an assembly + definition can't reference. When any script under `Assets` has none, creating the assemblies logs + a warning. +- `TestAssemblyCreator.Create(string parentFolder = "Assets")` does the same from code, and returns + the new folder's path. diff --git a/docs/udon-test-kit/reference/test-run.md b/docs/udon-test-kit/reference/test-run.md new file mode 100644 index 0000000..021dc7b --- /dev/null +++ b/docs/udon-test-kit/reference/test-run.md @@ -0,0 +1,44 @@ +--- +id: test-run +title: TestRun +sidebar_position: 6 +--- + +# TestRun + +`UdonTestKit.TestRun.IsActive` tells editor tooling whether a Unity Test Framework run is in +progress. Tooling that rewrites assets when the domain reloads, an asset is imported or a scene +changes, such as a code generator, can check it and leave a test run alone. + +```csharp +if (TestRun.IsActive || EditorApplication.isPlayingOrWillChangePlaymode) +{ + return; +} +``` + +## When it's true + +`IsActive` is true: + +- from the moment a run reports its start until it reports its end or an error, whether it was + started from the Test Runner window or from the command line, and +- for the whole life of a batch-mode Unity process started with `-runTests`, from before the first + test is found. + +The start and end come from Unity Test Framework's public callbacks, which the kit registers again +on every domain load, since registered callbacks don't survive a reload. The flag between them +lives in `SessionState`, so it survives the domain reloads a run causes but not an Editor restart. +A run that crashes without reporting its end can't leave it set for good. + +## The reload into Play Mode + +A Play Mode run reports its start only after the domain reload into Play Mode. Tooling that reacts +to that reload sees `IsActive` false, so check `EditorApplication.isPlayingOrWillChangePlaymode` +as well, as in the example above. + +## Using it + +`TestRun` uses Editor APIs, so call it from Editor code. It's in the kit's `UdonTestKit` assembly: +scripts without an assembly definition can use it directly, and code in an assembly definition +needs a reference to `UdonTestKit`. diff --git a/docs/udon-test-kit/reference/test-scene.md b/docs/udon-test-kit/reference/test-scene.md new file mode 100644 index 0000000..9213f8b --- /dev/null +++ b/docs/udon-test-kit/reference/test-scene.md @@ -0,0 +1,81 @@ +--- +id: test-scene +title: TestScene +sidebar_position: 3 +--- + +# TestScene + +`UdonTestKit.TestScene` is a scene for Play Mode tests to run in: a new empty scene, or a scene +asset such as your world's own. Loading it makes it the active scene, so ClientSim and the objects +a test creates go into it, and unloading it destroys them. + +[`ClientSimTestBase`](./clientsim-test-base) loads and unloads one for you when you pass it to the +base constructor. You can also use it on its own. + +## Creating one + +- `TestScene.Empty(string name)` is a new empty scene with that name. It's created each time it's + loaded, so every load starts empty. +- `TestScene.FromAsset(string assetPath)` is the scene asset at that path, such as + `"Assets/Scenes/MyWorld.unity"`. It doesn't need to be in the build settings. + +Neither one loads anything until `Load` runs. + +## Members + +`Load()` is a coroutine: `yield return scene.Load()`. It loads the scene alongside the scenes +already loaded, so Unity Test Framework's own test scene stays loaded, and makes it the active +scene. It does nothing when the scene is already loaded. + +`Unload()` makes the scene that was active before `Load` active again, then unloads this scene and +everything in it. It finishes before it returns, so it also works where a test can't wait, such as +in `[OneTimeTearDown]`. It does nothing when the scene isn't loaded. + +`IsLoaded` is true between a `Load` that finished and the next `Unload`. + +## Using it without the base class + +Load it in a `[UnitySetUp]` and unload it once ClientSim has ended, such as in a +`[UnityTearDown]`: + +```csharp +private readonly TestScene _scene = TestScene.Empty("Inventory"); +private readonly ClientSimSession _session = new ClientSimSession(); + +[UnitySetUp] +public IEnumerator SetUp() +{ + yield return _scene.Load(); +} + +[UnityTearDown] +public IEnumerator TearDown() +{ + _session.End(); + _scene.Unload(); + yield break; +} +``` + +End the session before unloading the scene, the same order `ClientSimTestBase` uses. + +## Edge cases + +- `TestScene` works only in Play Mode tests. `FromAsset` loads through + `EditorSceneManager.LoadSceneInPlayMode`, which is why the scene doesn't need to be in the build + settings. +- Unity drops the operation `LoadSceneInPlayMode` loads with. When the garbage collector finalized + it as Unity exited, Unity crashed after every test had passed, so `Load` finalizes it right after + the scene loads, while that's still safe. +- `Unload` uses Unity's synchronous `SceneManager.UnloadScene`, which Unity marks obsolete because + it's unsafe inside physics callbacks. A test's setup and teardown aren't physics callbacks. + +## TestSceneLifetime + +`UdonTestKit.TestSceneLifetime` tells `ClientSimTestBase` how long its scene lives: + +- `PerTest`, the default, loads a fresh copy for every test and unloads it after. Each test starts + from the same scene, and whatever a test created is gone before the next one. +- `PerFixture` loads it for the first test in the class and unloads it after the last. The tests + share whatever the earlier ones changed in it. diff --git a/sidebars.ts b/sidebars.ts index ea7342c..cf27243 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -125,7 +125,20 @@ const sidebars: SidebarsConfig = { type: 'category', label: 'Udon Test Kit', link: {type: 'doc', id: 'udon-test-kit/intro'}, - items: [], + items: [ + { + type: 'category', + label: 'Reference', + key: 'udon-test-kit-reference', + items: [{type: 'autogenerated', dirName: 'udon-test-kit/reference'}], + }, + { + type: 'category', + label: 'Explanations', + key: 'udon-test-kit-explanations', + items: [{type: 'autogenerated', dirName: 'udon-test-kit/explanations'}], + }, + ], }, ], }; From b4a9f38882a20effd3453b153aaf9c108a7fe207 Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:49:39 -0600 Subject: [PATCH 04/37] Add Udon Test Kit how-to guides Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- .../how-to/adding-your-worlds-own-setup.md | 50 ++++++++++++++ .../how-to/checking-calls-made-by-name.md | 65 +++++++++++++++++++ .../fixing-clientsim-project-settings.md | 26 ++++++++ ...keeping-editor-tooling-out-of-test-runs.md | 39 +++++++++++ .../running-tests-from-the-command-line.md | 53 +++++++++++++++ .../running-tests-in-their-own-scene.md | 51 +++++++++++++++ .../how-to/sharing-a-session-between-tests.md | 50 ++++++++++++++ .../testing-a-player-who-isnt-the-owner.md | 50 ++++++++++++++ .../how-to/testing-saved-data.md | 59 +++++++++++++++++ ...-scripts-without-an-assembly-definition.md | 33 ++++++++++ .../reference/callback-spy-and-call-log.md | 1 + .../reference/clientsim-session.md | 5 +- .../reference/invoke-unity-tests.md | 3 +- .../reference/test-assembly-creator.md | 3 +- docs/udon-test-kit/reference/test-scene.md | 3 +- sidebars.ts | 6 ++ 16 files changed, 493 insertions(+), 4 deletions(-) create mode 100644 docs/udon-test-kit/how-to/adding-your-worlds-own-setup.md create mode 100644 docs/udon-test-kit/how-to/checking-calls-made-by-name.md create mode 100644 docs/udon-test-kit/how-to/fixing-clientsim-project-settings.md create mode 100644 docs/udon-test-kit/how-to/keeping-editor-tooling-out-of-test-runs.md create mode 100644 docs/udon-test-kit/how-to/running-tests-from-the-command-line.md create mode 100644 docs/udon-test-kit/how-to/running-tests-in-their-own-scene.md create mode 100644 docs/udon-test-kit/how-to/sharing-a-session-between-tests.md create mode 100644 docs/udon-test-kit/how-to/testing-a-player-who-isnt-the-owner.md create mode 100644 docs/udon-test-kit/how-to/testing-saved-data.md create mode 100644 docs/udon-test-kit/how-to/testing-scripts-without-an-assembly-definition.md diff --git a/docs/udon-test-kit/how-to/adding-your-worlds-own-setup.md b/docs/udon-test-kit/how-to/adding-your-worlds-own-setup.md new file mode 100644 index 0000000..bf2b5ca --- /dev/null +++ b/docs/udon-test-kit/how-to/adding-your-worlds-own-setup.md @@ -0,0 +1,50 @@ +--- +id: adding-your-worlds-own-setup +title: Add your world's own test setup +sidebar_position: 6 +--- + +# Add your world's own test setup + +When every test needs the same setup, such as your world's scene, a running session and the +objects your tests look up, put it in a base class of your own that extends `ClientSimTestBase`, +and extend that in your test classes. + +```csharp +public abstract class MyWorldTestBase : ClientSimTestBase +{ + protected Scoreboard Board; + + protected MyWorldTestBase() : base(TestScene.FromAsset("Assets/Scenes/MyWorld.unity")) + { + } + + [UnitySetUp] + public IEnumerator StartWorld() + { + yield return Session.Start(); + Board = Object.FindObjectOfType(); + } +} + +public class ScoreboardTests : MyWorldTestBase +{ + [UnityTest] + public IEnumerator AddPoint_ForLocalPlayer_ShowsOnePoint() + { + Board.AddPoint(); + yield return null; + + Assert.That(Board.PointsOf(Networking.LocalPlayer), Is.EqualTo(1)); + } +} +``` + +Use NUnit's attributes in your base class as you would in a test class. NUnit runs a base class's +setup before yours and its teardown after yours, so with `ClientSimTestBase` underneath: + +- your `[UnitySetUp]` and `[SetUp]` run with the test scene already loaded and active, +- your `[TearDown]` runs while the session is still running, and +- your `[UnityTearDown]` runs after ClientSim has ended, but before the test scene unloads. + +[ClientSimTestBase](../reference/clientsim-test-base#lifecycle) lists the full order. diff --git a/docs/udon-test-kit/how-to/checking-calls-made-by-name.md b/docs/udon-test-kit/how-to/checking-calls-made-by-name.md new file mode 100644 index 0000000..611d0d5 --- /dev/null +++ b/docs/udon-test-kit/how-to/checking-calls-made-by-name.md @@ -0,0 +1,65 @@ +--- +id: checking-calls-made-by-name +title: Check the calls your code makes by name +sidebar_position: 4 +--- + +# Check the calls your code makes by name + +UdonSharp code often calls another behaviour by method name, with `SendCustomEvent`, on a target +and method name it stored earlier, such as a subscriber. Pass a +[`CallbackSpy`](../reference/callback-spy-and-call-log) as that target, and check what its +`CallLog` recorded. + +## Steps + +1. Create a log with `using var`, so its spies are destroyed when the test ends, and create a spy: + + ```csharp + using var calls = new CallLog(); + CallbackSpy listener = calls.CreateSpy("Listener"); + ``` + +2. Give the spy to your code as the target, with one of its callbacks as the method name: + + ```csharp + scoreboard.Subscribe(listener, nameof(CallbackSpy.CallbackA)); + ``` + +3. Act, then assert on the calls, in order: + + ```csharp + scoreboard.AddPoint(); + + Assert.That(calls, Is.EqualTo(new[] { "Listener.CallbackA" })); + ``` + +Spies created from the same log record into it in the order the calls arrive, so one assertion +covers several spies and the order between them. To ignore calls your setup caused, call +`calls.Clear()` before acting. + +## Callbacks with your own names + +When your code calls a fixed method name, such as `OnRoundEnded`, subclass `CallbackSpy` and call +`Record()` from a method with that name: + +```csharp +public class RoundListenerSpy : CallbackSpy +{ + public void OnRoundEnded() => Record(); +} +``` + +Create it with `calls.CreateSpy("Listener")`, and it records +`"Listener.OnRoundEnded"`. Put the subclass in your `Doubles` assembly, so both Edit Mode and Play +Mode tests can use it: Unity won't add a behaviour from the Edit Mode test assembly to a GameObject. + +## What to watch for + +Outside Udon, as in a test, a call by name reaches only a public method with no parameters and +exactly that name. A misspelled name does nothing and raises no error, so always assert on what the +log recorded, not just that nothing failed. + +`SendCustomEventDelayedSeconds` and `SendCustomEventDelayedFrames` never call anything outside +Udon. When the code under test delays a call that way, call the delayed method directly from the +test. diff --git a/docs/udon-test-kit/how-to/fixing-clientsim-project-settings.md b/docs/udon-test-kit/how-to/fixing-clientsim-project-settings.md new file mode 100644 index 0000000..589c798 --- /dev/null +++ b/docs/udon-test-kit/how-to/fixing-clientsim-project-settings.md @@ -0,0 +1,26 @@ +--- +id: fixing-clientsim-project-settings +title: Fix the project settings ClientSim rejects +sidebar_position: 9 +--- + +# Fix the project settings ClientSim rejects + +ClientSim checks a few project settings before it starts. When a Play Mode test fails before +ClientSim starts, with a message such as this one, your project fails that check: + +``` +ClientSim's project settings check fails on: input type, collision matrix. Fix them with Tools > Udon Test Kit > Fix ClientSim Project Settings. +``` + +## Steps + +1. Run **Tools > Udon Test Kit > Fix ClientSim Project Settings**. It changes each setting that + fails and logs which ones it changed. Settings that already pass are left alone. +2. Commit the changed files under `ProjectSettings`, so the tests pass on every machine that runs + them, including a build server. + +You only need to do this once per project, unless something changes those settings again. + +See [ClientSimProjectSettings](../reference/clientsim-project-settings) for what each setting is +and the change it gets. diff --git a/docs/udon-test-kit/how-to/keeping-editor-tooling-out-of-test-runs.md b/docs/udon-test-kit/how-to/keeping-editor-tooling-out-of-test-runs.md new file mode 100644 index 0000000..2f9bb3a --- /dev/null +++ b/docs/udon-test-kit/how-to/keeping-editor-tooling-out-of-test-runs.md @@ -0,0 +1,39 @@ +--- +id: keeping-editor-tooling-out-of-test-runs +title: Keep editor tooling from reacting to a test run +sidebar_position: 7 +--- + +# Keep editor tooling from reacting to a test run + +Editor tooling that reacts to domain reloads, asset imports or scene changes, such as a code +generator, also reacts to what a test run does: the reloads it causes, the test scene it creates, +the objects tests add. Have the tooling skip its work while tests run, by checking +[`TestRun.IsActive`](../reference/test-run). + +```csharp +[InitializeOnLoad] +internal static class MyGenerator +{ + static MyGenerator() + { + EditorApplication.hierarchyChanged += Regenerate; + } + + private static void Regenerate() + { + if (TestRun.IsActive || EditorApplication.isPlayingOrWillChangePlaymode) + { + return; + } + // ... + } +} +``` + +`TestRun.IsActive` is true from when a run reports its start until it reports its end, across the +domain reloads in between, and for the whole of a batch-mode process started with `-runTests`. A +Play Mode run reports its start only after the domain reload into Play Mode, so tooling that runs +on that reload also checks `EditorApplication.isPlayingOrWillChangePlaymode`. + +If your tooling's code is in an assembly definition, add `UdonTestKit` to its references. diff --git a/docs/udon-test-kit/how-to/running-tests-from-the-command-line.md b/docs/udon-test-kit/how-to/running-tests-from-the-command-line.md new file mode 100644 index 0000000..b188263 --- /dev/null +++ b/docs/udon-test-kit/how-to/running-tests-from-the-command-line.md @@ -0,0 +1,53 @@ +--- +id: running-tests-from-the-command-line +title: Run tests from the command line +sidebar_position: 8 +--- + +# Run tests from the command line + +[`Invoke-UnityTests.ps1`](../reference/invoke-unity-tests) runs a Unity project's tests from +PowerShell on Windows and prints only what needs your attention: the tests that didn't pass, with +their messages, and a count of the rest. It's a generic script, independent of the kit: copy it +into any Unity project to use it there, whether or not that project has the kit. + +## Steps + +1. Get the script: `Scripts~/Invoke-UnityTests.ps1` in the + [kit's repository](https://github.com/tsvrc/udon-test-kit). If you installed the kit through the + Creator Companion, it's also in `Packages/com.tsvrc.udon-test-kit/Scripts~`. A `.unitypackage` + can't carry it. +2. Copy it into your project, such as its root folder. It finds the project by looking up from its + own folder. +3. Close the Unity Editor. Batch mode can't open a project another Editor has open. +4. Run it from that folder: + + ```powershell + .\Invoke-UnityTests.ps1 + ``` + +It runs your project's Edit Mode tests, then its Play Mode tests, each in its own batch-mode Unity +process, and exits with 1 when a test failed or was inconclusive, or a run didn't finish. That makes +it usable as a build step as it is. + +## Run part of the tests + +Run one mode: + +```powershell +.\Invoke-UnityTests.ps1 -TestMode PlayMode +``` + +Run particular test assemblies, separated with `;`: + +```powershell +.\Invoke-UnityTests.ps1 -AssemblyNames "MyWorld.Tests.EditMode;MyWorld.Tests.PlayMode" +``` + +By default, the script runs the tests in every assembly definition under `Assets` and in scripts +without one, and leaves out the tests that come with packages. The +[reference page](../reference/invoke-unity-tests) lists every option, including where it looks for +Unity and where it writes results. + +If PowerShell refuses to run the script because of its execution policy, allow local scripts once +with `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`. diff --git a/docs/udon-test-kit/how-to/running-tests-in-their-own-scene.md b/docs/udon-test-kit/how-to/running-tests-in-their-own-scene.md new file mode 100644 index 0000000..daa0ff1 --- /dev/null +++ b/docs/udon-test-kit/how-to/running-tests-in-their-own-scene.md @@ -0,0 +1,51 @@ +--- +id: running-tests-in-their-own-scene +title: Run tests in a scene of their own +sidebar_position: 1 +--- + +# Run tests in a scene of their own + +By default, every Play Mode test runs in Unity Test Framework's own test scene, which the whole run +shares, so whatever one test leaves behind is still there in the next. Give a test class a scene of +its own by passing a [`TestScene`](../reference/test-scene) to `ClientSimTestBase`. + +## An empty scene for every test + +```csharp +public class InventoryTests : ClientSimTestBase +{ + public InventoryTests() : base(TestScene.Empty("Inventory")) + { + } +} +``` + +Before your own setup runs, every test gets a new empty scene, which becomes the active scene. +ClientSim and the objects your test creates go into it, and it's unloaded after the test, +destroying all of them. + +## Your world's own scene + +```csharp +public class LobbyTests : ClientSimTestBase +{ + public LobbyTests() : base(TestScene.FromAsset("Assets/Scenes/MyWorld.unity"), TestSceneLifetime.PerFixture) + { + } +} +``` + +The scene doesn't need to be in the build settings. When it has a scene descriptor, the session +uses it, and the local player spawns at its spawn point. + +Choose the lifetime by what your tests do to the scene: + +- `TestSceneLifetime.PerTest`, the default, loads the scene again for every test, so each test + starts from the scene as saved. +- `TestSceneLifetime.PerFixture` loads it once for the whole class. A large scene loads faster + that way, but every test sees what the earlier ones changed, so keep it for tests that only read. + +ClientSim keeps saved data per scene name, so tests in your world's scene read and write the same +saved data you get when you press Play. To start a test from none, see +[Check that saved data survives a rejoin](./testing-saved-data). diff --git a/docs/udon-test-kit/how-to/sharing-a-session-between-tests.md b/docs/udon-test-kit/how-to/sharing-a-session-between-tests.md new file mode 100644 index 0000000..5f2afb0 --- /dev/null +++ b/docs/udon-test-kit/how-to/sharing-a-session-between-tests.md @@ -0,0 +1,50 @@ +--- +id: sharing-a-session-between-tests +title: Share one session between the tests in a class +sidebar_position: 5 +--- + +# Share one session between the tests in a class + +Starting ClientSim for every test is what keeps tests independent, but it takes time. For tests +that only read, such as several checks on the same starting state, one session can serve the whole +class. + +`ClientSimTestBase` ends its session after every test, so don't extend it here. Create a +[`ClientSimSession`](../reference/clientsim-session) yourself, start it in the first `[UnitySetUp]`, +and end it in `[OneTimeTearDown]`: + +```csharp +public class StartingStateTests +{ + private readonly ClientSimSession _session = new ClientSimSession(); + private bool _started; + + [UnitySetUp] + public IEnumerator StartSessionOnce() + { + if (_started) + { + yield break; + } + _started = true; + yield return _session.Start(); + } + + [OneTimeTearDown] + public void EndSession() + { + _session.End(); + } +} +``` + +NUnit uses one instance of the class for all of its tests, so `_started` stays true after the +first test. Unity Test Framework 1.1 has no setup that runs once per class and can wait for frames, +which is why the start goes in `[UnitySetUp]`. + +Tests that share a session also share whatever the earlier ones changed: players, owners, saved +data and objects in the scene. A test that changes any of them belongs in a class with a fresh +session per test. See +[Decision note: a fresh session for every test](../explanations/a-fresh-session-for-every-test) +for the tradeoff. diff --git a/docs/udon-test-kit/how-to/testing-a-player-who-isnt-the-owner.md b/docs/udon-test-kit/how-to/testing-a-player-who-isnt-the-owner.md new file mode 100644 index 0000000..12e0d6b --- /dev/null +++ b/docs/udon-test-kit/how-to/testing-a-player-who-isnt-the-owner.md @@ -0,0 +1,50 @@ +--- +id: testing-a-player-who-isnt-the-owner +title: Test what happens for a player who isn't the owner +sidebar_position: 2 +--- + +# Test what happens for a player who isn't the owner + +In ClientSim, the master owns every GameObject without `VRCObjectSync` or `VRCObjectPool`, and +`Networking.SetOwner` can't change that. The local player is the master by default, so code that +behaves differently for a player who doesn't own a GameObject never takes that path in a test. +[`Session.SetOwner`](../reference/clientsim-session#ownership) gives the GameObject another owner. + +## Steps + +1. Start the session and spawn a remote player: + + ```csharp + yield return Session.Start(); + VRCPlayerApi remote = Session.SpawnRemotePlayer("Remote"); + ``` + +2. Make the remote player the owner of the GameObject your behaviour is on: + + ```csharp + Session.SetOwner(remote, door.gameObject); + ``` + +3. Call your behaviour as the local player, who now doesn't own it, and assert on what it did: + + ```csharp + door.Open(); + + Assert.That(door.IsOpen, Is.False); + ``` + +After `Session.SetOwner`, `Networking.SetOwner` works on that GameObject too. If the code under +test takes ownership before acting, check that it did: + +```csharp +door.TakeOverAndOpen(); + +Assert.That(Networking.IsOwner(door.gameObject), Is.True); +``` + +## The local player as a non-master + +To test as a player who isn't the master, start the session with +`Session.Start(localPlayerIsMaster: false)`. ClientSim spawns a remote player before the local +one, and that player is the master. diff --git a/docs/udon-test-kit/how-to/testing-saved-data.md b/docs/udon-test-kit/how-to/testing-saved-data.md new file mode 100644 index 0000000..ab39040 --- /dev/null +++ b/docs/udon-test-kit/how-to/testing-saved-data.md @@ -0,0 +1,59 @@ +--- +id: testing-saved-data +title: Check that saved data survives a rejoin +sidebar_position: 3 +--- + +# Check that saved data survives a rejoin + +ClientSim saves each player's PlayerData and PlayerObjects to files in your project. Ending a +session stops it from saving, so the next session in the same scene starts from what the last one +saved, like a player who leaves and rejoins. That lets one test check that data survives a rejoin. + +## Steps + +1. Start a session and change the data: + + ```csharp + yield return Session.Start(); + PlayerData.SetString("coins", "10"); + ``` + +2. Wait a frame. ClientSim has saved the change by then: + + ```csharp + yield return null; + ``` + +3. End the session and start a new one, the rejoin: + + ```csharp + Session.End(); + yield return Session.Start(); + ``` + +4. Assert on what the new session restored: + + ```csharp + Assert.That(PlayerData.TryGetString(Networking.LocalPlayer, "coins", out string coins), Is.True); + Assert.That(coins, Is.EqualTo("10")); + ``` + +Keep the whole rejoin inside one test. NUnit doesn't promise an order between tests, so a test that +saves and a later test that reads would pass or fail depending on that order. + +## Starting from no saved data + +Saved data stays on disk after a test, so a later test in the same scene starts from it too. When a +test needs none, delete it before starting the session: + +```csharp +ClientSimSession.DeleteSavedData(); +yield return Session.Start(); +``` + +`DeleteSavedData` deletes what ClientSim saved for the active scene, for every player. In your +world's own scene, those files are also the ones you get when you press Play. + +Unity Test Framework names its test scene differently on every run, so each run leaves a few files +in `ClientSimStorage` that no later run reads. Delete them whenever you like. diff --git a/docs/udon-test-kit/how-to/testing-scripts-without-an-assembly-definition.md b/docs/udon-test-kit/how-to/testing-scripts-without-an-assembly-definition.md new file mode 100644 index 0000000..4cdd1e8 --- /dev/null +++ b/docs/udon-test-kit/how-to/testing-scripts-without-an-assembly-definition.md @@ -0,0 +1,33 @@ +--- +id: testing-scripts-without-an-assembly-definition +title: Test scripts that have no assembly definition +sidebar_position: 10 +--- + +# Test scripts that have no assembly definition + +Scripts without an assembly definition compile into `Assembly-CSharp`, which no assembly +definition can reference, including the test assemblies +[`TestAssemblyCreator`](../reference/test-assembly-creator) creates. Creating them logs a warning +when your project has such scripts. + +## Edit Mode tests + +Put the tests in any folder named `Editor` that has no assembly definition. Unity compiles them +into `Assembly-CSharp-Editor`, which can use your scripts, NUnit and the kit without any +references set up. Unity's Test Runner finds them there, and so does +[`Invoke-UnityTests.ps1`](../reference/invoke-unity-tests). + +## Play Mode tests + +Play Mode tests need an assembly definition of their own, so the scripts they test need one too: + +1. Create an assembly definition in the folder that holds your scripts (**Assets > Create > + Assembly Definition**). It covers the scripts in that folder and its subfolders. +2. Give it the references your scripts need, such as `UdonSharp.Runtime`, `VRC.SDKBase` and + `VRC.SDK3`. +3. Create a U# assembly definition next to it (**Assets > Create > U# Assembly Definition**) and + set its source assembly to the one you created, so UdonSharp keeps compiling those scripts for + Udon. +4. Add its name to the references of your Play Mode test assembly, or delete the test assemblies + and create them again, which picks it up. diff --git a/docs/udon-test-kit/reference/callback-spy-and-call-log.md b/docs/udon-test-kit/reference/callback-spy-and-call-log.md index 94313e1..804a32d 100644 --- a/docs/udon-test-kit/reference/callback-spy-and-call-log.md +++ b/docs/udon-test-kit/reference/callback-spy-and-call-log.md @@ -21,6 +21,7 @@ Assert.That(calls, Is.EqualTo(new[] { "Listener.CallbackA" })); ``` Both work in Edit Mode and Play Mode tests. +[Check the calls your code makes by name](../how-to/checking-calls-made-by-name) walks through it. ## CallLog diff --git a/docs/udon-test-kit/reference/clientsim-session.md b/docs/udon-test-kit/reference/clientsim-session.md index e2463a2..c8c33a5 100644 --- a/docs/udon-test-kit/reference/clientsim-session.md +++ b/docs/udon-test-kit/reference/clientsim-session.md @@ -71,6 +71,8 @@ in VRChat. Then it moves ownership to `player`. From then on, `Networking.SetOwn GameObject move its ownership too, including the ones the code under test makes. Calling `SetOwner` again reuses the component instead of adding another. On a GameObject that already has `VRCObjectSync` or `VRCObjectPool`, it only calls `Networking.SetOwner`. +[Test what happens for a player who isn't the owner](../how-to/testing-a-player-who-isnt-the-owner) +shows it in a test. ## Ending @@ -93,7 +95,8 @@ ClientSim saves each player's data in your project folder, one file per player a `ClientSimSession.DeleteSavedData()` is static and deletes both kinds of file for the active scene, for every player. Nothing else in the kit deletes saved data: in your world's own scene, those -files are your ClientSim data. +files are your ClientSim data. [Check that saved data survives a rejoin](../how-to/testing-saved-data) +uses both. Unity Test Framework gives its test scene a new name on every run, so each run leaves a few files in `ClientSimStorage` that no later run reads. You can delete them whenever you like. diff --git a/docs/udon-test-kit/reference/invoke-unity-tests.md b/docs/udon-test-kit/reference/invoke-unity-tests.md index f12f226..8093528 100644 --- a/docs/udon-test-kit/reference/invoke-unity-tests.md +++ b/docs/udon-test-kit/reference/invoke-unity-tests.md @@ -9,7 +9,8 @@ sidebar_position: 9 `Invoke-UnityTests.ps1` runs a Unity project's tests from PowerShell on Windows and prints a short report. It's a single, standalone file: it doesn't use the kit or anything else beyond Unity Test Framework, and it's meant to be copied into any Unity project that wants it, with or without the -kit. +kit. [Run tests from the command line](../how-to/running-tests-from-the-command-line) walks through +using it. ## Getting it diff --git a/docs/udon-test-kit/reference/test-assembly-creator.md b/docs/udon-test-kit/reference/test-assembly-creator.md index d07a3da..12c36fd 100644 --- a/docs/udon-test-kit/reference/test-assembly-creator.md +++ b/docs/udon-test-kit/reference/test-assembly-creator.md @@ -51,6 +51,7 @@ Each example compiles and passes as created. definition later, and add it to the test assemblies' references yourself. - Scripts without an assembly definition compile into `Assembly-CSharp`, which an assembly definition can't reference. When any script under `Assets` has none, creating the assemblies logs - a warning. + a warning. See + [Test scripts that have no assembly definition](../how-to/testing-scripts-without-an-assembly-definition). - `TestAssemblyCreator.Create(string parentFolder = "Assets")` does the same from code, and returns the new folder's path. diff --git a/docs/udon-test-kit/reference/test-scene.md b/docs/udon-test-kit/reference/test-scene.md index 9213f8b..5544573 100644 --- a/docs/udon-test-kit/reference/test-scene.md +++ b/docs/udon-test-kit/reference/test-scene.md @@ -11,7 +11,8 @@ asset such as your world's own. Loading it makes it the active scene, so ClientS a test creates go into it, and unloading it destroys them. [`ClientSimTestBase`](./clientsim-test-base) loads and unloads one for you when you pass it to the -base constructor. You can also use it on its own. +base constructor, as in [Run tests in a scene of their own](../how-to/running-tests-in-their-own-scene). +You can also use it on its own. ## Creating one diff --git a/sidebars.ts b/sidebars.ts index cf27243..830f10a 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -126,6 +126,12 @@ const sidebars: SidebarsConfig = { label: 'Udon Test Kit', link: {type: 'doc', id: 'udon-test-kit/intro'}, items: [ + { + type: 'category', + label: 'How-to guides', + key: 'udon-test-kit-how-to', + items: [{type: 'autogenerated', dirName: 'udon-test-kit/how-to'}], + }, { type: 'category', label: 'Reference', From 9376f458e6b05e2b4c9fe3e1932e89fdd9f2a2f4 Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:51:04 -0600 Subject: [PATCH 05/37] Add Udon Test Kit getting-started pages Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- docs/udon-test-kit/add-to-your-project.md | 101 ++++++++++++++++++ docs/udon-test-kit/first-test.md | 100 +++++++++++++++++ docs/udon-test-kit/intro.md | 20 ++++ .../current/udon-test-kit/intro.md | 21 ++++ sidebars.ts | 6 ++ 5 files changed, 248 insertions(+) create mode 100644 docs/udon-test-kit/add-to-your-project.md create mode 100644 docs/udon-test-kit/first-test.md diff --git a/docs/udon-test-kit/add-to-your-project.md b/docs/udon-test-kit/add-to-your-project.md new file mode 100644 index 0000000..a9c0fc1 --- /dev/null +++ b/docs/udon-test-kit/add-to-your-project.md @@ -0,0 +1,101 @@ +--- +id: add-to-your-project +title: Add Udon Test Kit to your project +sidebar_position: 2 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Add Udon Test Kit to your project + +Install the kit in a VRChat world project, then create the test assemblies your tests go in. The +VRChat Creator Companion (VCC) is the recommended way to install it, because it also resolves the +VRChat Worlds SDK version the kit needs. + +## Requirements + +- **Unity 2022.3.** +- **The [VRChat Worlds SDK](https://creators.vrchat.com/worlds/) 3.8.2 or later**, which includes + UdonSharp and ClientSim. Earlier versions stop Play Mode when a scene has no scene descriptor, + and Unity Test Framework's test scene has none, so every Play Mode run hangs. +- **Unity Test Framework**, which new Unity projects already include. + +## Install the package + + + + +If the `vpm.tsvrc.com` repository isn't in your Creator Companion yet, add it first. It's a +one-time step, the same as adding any other +[community repository](https://vcc.docs.vrchat.com/guides/community-repositories/): + +- **With VCC installed**, click + [Add the repository to Creator Companion](vcc://vpm/addRepo?url=https%3A%2F%2Fvpm.tsvrc.com%2Findex.json) + to open VCC at the add-repository prompt. +- **Or paste the listing URL yourself**: open VCC, go to **Settings > Packages > Add Repository**, + and paste: + + ``` + https://vpm.tsvrc.com/index.json + ``` + +Once the repository is added: + +1. Open the Creator Companion, select your project, and click **Manage Project**. +2. Find **Udon Test Kit** (`com.tsvrc.udon-test-kit`) in the package list and press the **+** next + to it. +3. Wait for Unity to finish importing and recompiling. + +The Creator Companion installs the kit in your project's `Packages` folder. + + + + +1. Download the latest `.unitypackage` from the kit's + [GitHub releases](https://github.com/tsvrc/udon-test-kit/releases). +2. With your project open, double-click the downloaded file, or use **Assets > Import Package > + Custom Package**, and import everything. +3. Wait for Unity to finish importing and recompiling. + +The kit goes in your project's `Assets` folder and works the same from there. Nothing here checks +the VRChat Worlds SDK version for you, so confirm it's 3.8.2 or later first. A `.unitypackage` can't +carry the command-line script; see +[Run tests from the command line](./how-to/running-tests-from-the-command-line) for where to get it. + + + + +You'll know it worked when Unity's **Tools** menu has an **Udon Test Kit** entry. + +:::warning +A project can hold only one copy of the kit. Two copies mean two assemblies named `UdonTestKit`, +which stop the whole project compiling. Don't import the `.unitypackage` into a project that +already has the kit in `Packages`, including a copy another package installed as its dependency. +::: + +## Create your test assemblies + +Unity Test Framework finds tests in test assemblies, and the kit creates the three a world needs. + +1. In the Project window, select the folder your tests should live in, such as `Assets`. +2. Choose **Assets > Create > Udon Test Kit > Test Assemblies**. +3. Wait for Unity to compile. + +You get a `Tests` folder with three assemblies, each with an example that works: + +- `EditMode` and `PlayMode` hold your tests. +- `Doubles` holds behaviours your tests add to GameObjects, such as spies with callbacks of your + own. Both test assemblies reference it. + +They reference the kit, UdonSharp, the VRChat SDK, ClientSim and every assembly definition of yours +under `Assets`, and like the kit, they're left out of your world's builds. +[TestAssemblyCreator](./reference/test-assembly-creator) describes each one. + +If your scripts have no assembly definition, the menu item logs a warning, and Play Mode tests +can't reach them yet. See +[Test scripts that have no assembly definition](./how-to/testing-scripts-without-an-assembly-definition). + +## Next + +Continue to [Write your first test](./first-test) to run the examples and write a test of your own. diff --git a/docs/udon-test-kit/first-test.md b/docs/udon-test-kit/first-test.md new file mode 100644 index 0000000..5895979 --- /dev/null +++ b/docs/udon-test-kit/first-test.md @@ -0,0 +1,100 @@ +--- +id: first-test +title: Write your first test +sidebar_position: 3 +--- + +# Write your first test + +Run the example tests, then write a Play Mode test with two players in a real ClientSim session and +an Edit Mode test with a spy. You need the test assemblies from +[Add Udon Test Kit to your project](./add-to-your-project). + +## Run the examples + +1. Open **Window > General > Test Runner**. +2. In the **PlayMode** tab, select `ExamplePlayModeTests` and click **Run Selected**. + +Unity enters Play Mode, ClientSim starts with a local player, the test spawns a second player, and +the test passes. + +If it fails before ClientSim starts, with a message naming project settings, run **Tools > Udon +Test Kit > Fix ClientSim Project Settings** once and run the test again. See +[Fix the project settings ClientSim rejects](./how-to/fixing-clientsim-project-settings). + +## Write a Play Mode test + +In your `Tests/PlayMode` folder, create `PlayerListTests.cs`: + +```csharp +using System.Collections; +using NUnit.Framework; +using UdonTestKit; +using UnityEngine.TestTools; +using VRC.SDKBase; + +public class PlayerListTests : ClientSimTestBase +{ + [UnityTest] + public IEnumerator RemovePlayer_AfterSpawning_LeavesOnlyTheLocalPlayer() + { + yield return Session.Start(); + VRCPlayerApi remote = Session.SpawnRemotePlayer("Remote"); + + Session.RemovePlayer(remote); + + Assert.That(VRCPlayerApi.GetPlayerCount(), Is.EqualTo(1)); + Assert.That(ClientSimSession.FindPlayerByName("Remote"), Is.Null); + } +} +``` + +`ClientSimTestBase` gives the test a `Session` and ends it after the test. `Session.Start` returns +once the local player has joined, and `SpawnRemotePlayer` and `RemovePlayer` take effect before +they return, so the test can assert straight away. + +Run it from the **PlayMode** tab. It passes. + +## Write an Edit Mode test with a spy + +UdonSharp code often calls other behaviours by method name, with `SendCustomEvent`. A spy records +those calls. In your `Tests/EditMode` folder, create `SpyTests.cs`: + +```csharp +using NUnit.Framework; +using UdonTestKit; + +public class SpyTests +{ + [Test] + public void SendCustomEvent_TwoSpies_RecordsBothInOrder() + { + using var calls = new CallLog(); + CallbackSpy first = calls.CreateSpy("First"); + CallbackSpy second = calls.CreateSpy("Second"); + + second.SendCustomEvent(nameof(CallbackSpy.CallbackB)); + first.SendCustomEvent(nameof(CallbackSpy.CallbackA)); + + Assert.That(calls, Is.EqualTo(new[] { "Second.CallbackB", "First.CallbackA" })); + } +} +``` + +Here the test makes the calls itself. In a real test, you pass the spy to your own code as the +behaviour it calls, and the log shows what your code called, in order. The `using` destroys both +spies when the test ends. + +Run it from the **EditMode** tab. It passes. + +## Where to go next + +To test your own behaviours, add them to GameObjects in a Play Mode test, drive them, and assert on +what they did. The how-to guides cover the usual next steps: + +- [Run tests in a scene of their own](./how-to/running-tests-in-their-own-scene), including your + world's scene. +- [Test what happens for a player who isn't the owner](./how-to/testing-a-player-who-isnt-the-owner). +- [Check that saved data survives a rejoin](./how-to/testing-saved-data). +- [Check the calls your code makes by name](./how-to/checking-calls-made-by-name). +- [Run tests from the command line](./how-to/running-tests-from-the-command-line). diff --git a/docs/udon-test-kit/intro.md b/docs/udon-test-kit/intro.md index f712227..f38d4e9 100644 --- a/docs/udon-test-kit/intro.md +++ b/docs/udon-test-kit/intro.md @@ -18,3 +18,23 @@ creates the test assemblies a world needs in one step. The kit compiles only when Unity Test Framework includes tests, so nothing from it reaches a world build. + +## New to the kit? + +Start with [Add Udon Test Kit to your project](./add-to-your-project), then +[Write your first test](./first-test): by the end you'll have a Play Mode test with two players and +an Edit Mode test with a spy, both passing in the Test Runner. + +## Already using it? + +Jump straight to what you need: + +- **Doing a specific task** — [run tests in a scene of their own](./how-to/running-tests-in-their-own-scene), + [test a player who isn't the owner](./how-to/testing-a-player-who-isnt-the-owner), + [check that saved data survives a rejoin](./how-to/testing-saved-data), or + [run tests from the command line](./how-to/running-tests-from-the-command-line). +- **Looking up a type's behavior** — start from [ClientSimSession](./reference/clientsim-session), + or browse Reference in the sidebar. +- **Understanding what the kit changes and why** — + [What the kit works around](./explanations/what-the-kit-works-around) and + [a fresh session for every test](./explanations/a-fresh-session-for-every-test). diff --git a/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md b/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md index 602d95c..34837fa 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md +++ b/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md @@ -19,3 +19,24 @@ que necesita un mundo. El kit solo se compila cuando Unity Test Framework incluye los tests, así que nada de él llega a la build de un mundo. + +## ¿Eres nuevo en el kit? + +Empieza por [Añade Udon Test Kit a tu proyecto](./add-to-your-project) y luego +[Escribe tu primer test](./first-test): al final tendrás un test de modo Play con dos jugadores y un +test de modo Edit con un spy, ambos pasando en el Test Runner. + +## ¿Ya lo usas? + +Ve directo a lo que necesitas: + +- **Resolver una tarea concreta** — + [ejecutar tests en una escena propia](./how-to/running-tests-in-their-own-scene), + [probar a un jugador que no es el dueño](./how-to/testing-a-player-who-isnt-the-owner), + [comprobar que los datos guardados sobreviven al volver a entrar](./how-to/testing-saved-data) o + [ejecutar tests desde la línea de comandos](./how-to/running-tests-from-the-command-line). +- **Buscar el comportamiento de un tipo concreto** — empieza por + [ClientSimSession](./reference/clientsim-session) o explora Reference en la barra lateral. +- **Entender qué cambia el kit y por qué** — + [Lo que el kit resuelve](./explanations/what-the-kit-works-around) y + [una sesión nueva para cada test](./explanations/a-fresh-session-for-every-test). diff --git a/sidebars.ts b/sidebars.ts index 830f10a..69d7903 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -126,6 +126,12 @@ const sidebars: SidebarsConfig = { label: 'Udon Test Kit', link: {type: 'doc', id: 'udon-test-kit/intro'}, items: [ + { + type: 'category', + label: 'Getting started', + key: 'udon-test-kit-getting-started', + items: ['udon-test-kit/add-to-your-project', 'udon-test-kit/first-test'], + }, { type: 'category', label: 'How-to guides', From b01a50fd13229bdf0dc5b3b089da77a0db1db307 Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:53:24 -0600 Subject: [PATCH 06/37] Point TsVRC's testing docs at Udon Test Kit Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- docs/tsvrc/add-to-your-project.md | 15 ++- docs/tsvrc/codegen/internals/ts-generator.md | 7 +- docs/tsvrc/how-to/set-up-automated-testing.md | 36 +++-- docs/tsvrc/how-to/writing-your-first-test.md | 27 ++-- docs/tsvrc/testing/testing-your-world.md | 123 +++++++----------- 5 files changed, 90 insertions(+), 118 deletions(-) diff --git a/docs/tsvrc/add-to-your-project.md b/docs/tsvrc/add-to-your-project.md index 240b55a..5cbd457 100644 --- a/docs/tsvrc/add-to-your-project.md +++ b/docs/tsvrc/add-to-your-project.md @@ -47,6 +47,9 @@ Once the repository's added: to install it. 3. Wait for Unity to finish importing and recompiling. +The Creator Companion also installs [Udon Test Kit](/docs/udon-test-kit/intro) +(`com.tsvrc.udon-test-kit`), which TsVRC depends on for its testing helpers. + @@ -56,6 +59,10 @@ Once the repository's added: Package > Custom Package**) and import everything. 3. Wait for Unity to finish importing and recompiling. +A `.unitypackage` can't bring the packages TsVRC depends on, so also import +[Udon Test Kit's](/docs/udon-test-kit/add-to-your-project) `.unitypackage`, unless your +project already has the kit. + Nothing here resolves the VRChat Worlds SDK version for you, so confirm it's already at `3.10.x` before importing. If your project also uses VCC for other packages, prefer the VCC tab instead: mixing a hand-imported package with VCC-managed ones makes future @@ -76,10 +83,10 @@ scripts extend and call into, and one editor-only assembly (`Tsvrc.Editor`) hold the codegen and Configure-window tooling. See [How TsVRC fits together](./core-concepts/how-it-fits-together) for how those two halves relate. -Three further assemblies, `Tsvrc.Testing.Framework`, `Tsvrc.Testing.Behaviours`, and -`Tsvrc.Testing.UI`, exist purely to support testing a *consumer's* own project, kept -separate from each other and from `Tsvrc.Runtime`/`Tsvrc.Editor` for the reasons covered -in [Testing your world](./testing/testing-your-world). +Two further assemblies, `Tsvrc.Testing.Framework` and `Tsvrc.Testing.UI`, exist purely to +support testing a *consumer's* own project, on top of Udon Test Kit, kept separate from +each other and from `Tsvrc.Runtime`/`Tsvrc.Editor` for the reasons covered in [Testing +your world](./testing/testing-your-world). ## Next diff --git a/docs/tsvrc/codegen/internals/ts-generator.md b/docs/tsvrc/codegen/internals/ts-generator.md index da6debe..fcf8816 100644 --- a/docs/tsvrc/codegen/internals/ts-generator.md +++ b/docs/tsvrc/codegen/internals/ts-generator.md @@ -23,9 +23,10 @@ Regenerate**, when a watched asset changes, or automatically after every recompi through `TsAssetWatcher` itself. - **Before a VRChat world build** (`TsBuildCompile`) — see below. -Every *automatic* trigger is gated by `AutomaticTriggersSuppressed`, which is true during -any automated test run (detected once, from the process's own `-runTests` command-line -flag) or inside an explicit `SuppressAutomaticTriggers()` scope. This exists because an +Every *automatic* trigger is gated by `AutomaticTriggersSuppressed`, which is true while +Unity Test Framework runs tests, whether from the Test Runner window or a `-runTests` +command-line run, while the Editor is entering or in Play Mode, and inside an explicit +`SuppressAutomaticTriggers()` scope. This exists because an automatic pass reacting to whatever scene happens to be open, a test's own temporary scene in particular, could otherwise silently regenerate a real project's output down to almost nothing (see [`TsLinkedScene`](../config/linked-scene) for the deeper reason this is diff --git a/docs/tsvrc/how-to/set-up-automated-testing.md b/docs/tsvrc/how-to/set-up-automated-testing.md index 78d1f6f..77902e7 100644 --- a/docs/tsvrc/how-to/set-up-automated-testing.md +++ b/docs/tsvrc/how-to/set-up-automated-testing.md @@ -21,30 +21,24 @@ generated-folder setting points (they don't need to be nested inside each other, under that one asmdef's root), referencing `Tsvrc.Runtime` plus whatever VRChat SDK/ UdonSharp assemblies your scripts use. -## 2. Add a test assembly +## 2. Create your test assemblies -Create an EditMode or PlayMode test assembly referencing your project's runtime asmdef, -`Tsvrc.Runtime`, and `Tsvrc.Testing.Framework` for the [reflection and ClientSim -helpers](../testing/testing-your-world). Add `Tsvrc.Testing.Behaviours` or -`Tsvrc.Testing.UI` too if your tests need the listener or list-item doubles they provide. +TsVRC's testing helpers build on Udon Test Kit, which the Creator Companion installs along +with TsVRC. Create your test assemblies with the kit's menu item, as described in +[Create your test assemblies](/docs/udon-test-kit/add-to-your-project#create-your-test-assemblies). +They already reference the kit, the VRChat SDK, ClientSim and your project's assembly from +step 1. -## 3. Arm reactive codegen suppression for Play Mode tests +## 3. Add TsVRC's testing assemblies -A test that creates or mutates scene objects can otherwise trigger a real codegen -regenerate pass against your project's actual `TsConfig` mid-test-run. Add a one-line -`[SetUpFixture]` subclass to your PlayMode test assembly to hold that back for the whole -run: +In the `EditMode` and `PlayMode` test assemblies, add these references: -```csharp -[SetUpFixture] -public class AutomaticTriggersSetUpFixture : AutomaticTriggersSetUpFixtureBase { } -``` +- `Tsvrc.Runtime`, if it isn't listed already. +- `Tsvrc.Testing.Framework`, for [`TsPlayModeTestBase`](../testing/testing-your-world). +- `Tsvrc.Testing.UI`, if your tests need its list doubles. -NUnit only discovers a `[SetUpFixture]` in the assembly it's physically compiled into, so -this needs adding once per test assembly. The base class living in -`Tsvrc.Testing.Framework` isn't enough by itself. +Add `Tsvrc.Runtime` to the `Doubles` assembly too, if your test doubles extend TsVRC types. -With these three pieces in place, extend -[`TsPlayModeTestBase`](../testing/testing-your-world#play-mode-tests-tsplaymodetestbase) -to write a Play Mode test, or call `PrivateFieldAccess`'s static methods directly for an -Edit Mode one. +That's all. TsVRC's codegen holds back on its own while tests run, so nothing else needs +arming. Extend [`TsPlayModeTestBase`](../testing/testing-your-world#play-mode-tests-tsplaymodetestbase) +to write a Play Mode test, or use Udon Test Kit's helpers directly in an Edit Mode one. diff --git a/docs/tsvrc/how-to/writing-your-first-test.md b/docs/tsvrc/how-to/writing-your-first-test.md index cf8888f..7f53340 100644 --- a/docs/tsvrc/how-to/writing-your-first-test.md +++ b/docs/tsvrc/how-to/writing-your-first-test.md @@ -20,7 +20,7 @@ How to write a real Play Mode test against your own `TsvrcBehaviour` subclass, o [UnityTest] public IEnumerator RoundReferee_StartsOnBegin() { - yield return StartClientSim(); + yield return Session.Start(); ``` 2. Build your project's generated root from code instead of loading a saved scene, and @@ -48,8 +48,8 @@ How to write a real Play Mode test against your own `TsvrcBehaviour` subclass, o ## Reaching a private field or method -Use [`PrivateFieldAccess`](../testing/testing-your-world) instead of making something -`public` just so a test can see it: +Use Udon Test Kit's [`PrivateFieldAccess`](/docs/udon-test-kit/reference/private-field-access) +instead of making something `public` just so a test can see it: ```csharp int remaining = PrivateFieldAccess.GetField(referee, "_remainingMilliseconds"); @@ -57,21 +57,24 @@ int remaining = PrivateFieldAccess.GetField(referee, "_remainingMillisecond ## Asserting an event actually fired -Wire a [`TsCallbackRecorder`](../testing/testing-your-world) as the subscriber instead of -writing a one-off listener class for every test: +Subscribe a spy from Udon Test Kit's +[`CallLog`](/docs/udon-test-kit/reference/callback-spy-and-call-log) instead of writing a +one-off listener class for every test: ```csharp -var recorder = builder.WithNew("Recorder"); -referee.TsSubscribe(recorder, RoundReferee.OnRoundEndedEvent, nameof(TsCallbackRecorder.CallbackA)); +using var calls = new CallLog(); +CallbackSpy listener = calls.CreateSpy("Listener"); +referee.TsSubscribe(listener, RoundReferee.OnRoundEndedEvent, nameof(CallbackSpy.CallbackA)); // ...drive the round to completion... -Assert.AreEqual(1, recorder.CallbackACount); +Assert.That(calls, Is.EqualTo(new[] { "Listener.CallbackA" })); ``` +The log records each call in order, so the same assertion also fails if the event fired +twice. + ## Why this shape -See [`Testing your world`](../testing/testing-your-world) for the full reasoning behind -building a root from code instead of a saved scene, the two Play Mode testing defects -patched automatically for any `TsPlayModeTestBase` subclass, and why the three testing -assemblies are split the way they are. +See [Testing your world](../testing/testing-your-world) for why the root is built from code +instead of loaded from a saved scene, and how each test gets a scene of its own. diff --git a/docs/tsvrc/testing/testing-your-world.md b/docs/tsvrc/testing/testing-your-world.md index 2396786..bcd02c1 100644 --- a/docs/tsvrc/testing/testing-your-world.md +++ b/docs/tsvrc/testing/testing-your-world.md @@ -6,32 +6,23 @@ sidebar_position: 1 # Testing your world -TsVRC ships a small set of testing helpers your own project can use to write real, -ClientSim-backed Play Mode tests (and plain reflection-based Edit Mode tests) against your -own `UdonSharpBehaviour`s, without hand-rolling VRChat SDK setup and working around its Play -Mode quirks yourself. See [Set up automated testing](../how-to/set-up-automated-testing) -for how to add these assemblies to your project, and [Write your first automated -test](../how-to/writing-your-first-test) for a worked example; this page covers what -these assemblies give you. - -Nothing here does anything to your project just by being referenced. Every piece of -behavior only activates when you actually use it. `PrivateFieldAccess` is a set of plain -static methods with no base class requirement; `TsPlayModeTestBase` and its ClientSim -machinery only run for a test class that actually extends it. - -## Reflection helpers: PrivateFieldAccess - -`SetField`/`GetField`/`InvokeStatic`/`InvokeInstance` reach private fields and methods on -any object or static type from a test, useful for asserting on internal state a -behaviour's public surface doesn't expose. Standalone, works in either Edit Mode or Play -Mode. +TsVRC's testing helpers build on [Udon Test Kit](/docs/udon-test-kit/intro), which runs Play Mode +tests in a real ClientSim session and works around the VRChat SDK problems that break them. TsVRC +depends on the kit, so the Creator Companion installs it along with TsVRC. + +This page covers what TsVRC adds for testing a TsVRC world: a base class that builds your +generated root from code, helpers for list UI, and codegen that holds back while tests run. For +everything else, such as players, ownership, saved data, spies and reaching private fields, use +the kit directly. See [Set up automated testing](../how-to/set-up-automated-testing) to add the +assemblies to your project, and [Write your first automated +test](../how-to/writing-your-first-test) for a worked example. ## Play Mode tests: TsPlayModeTestBase {/* #play-mode-tests-tsplaymodetestbase */} -Extend `TsPlayModeTestBase` instead of setting up ClientSim by hand. It exposes `Players` -(a `ClientSimPlayerEnvironment`, for spawning, removing, or finding real `VRCPlayerApi` -instances in the test), `StartClientSim(...)` to begin a session, and -`BuildTsRoot()` to construct your project's generated composition root from code. +`TsPlayModeTestBase` extends the kit's +[`ClientSimTestBase`](/docs/udon-test-kit/reference/clientsim-test-base). A test gets the kit's +`Session` for starting ClientSim and working with players, and `BuildTsRoot()` for +constructing your project's generated composition root from code. ```csharp public class MyManagerTests : TsPlayModeTestBase @@ -39,7 +30,7 @@ public class MyManagerTests : TsPlayModeTestBase [UnityTest] public IEnumerator MyManager_DoesTheThing() { - yield return StartClientSim(); + yield return Session.Start(); var builder = BuildTsRoot(); var myManager = builder.WithNew("MyManager"); @@ -51,6 +42,9 @@ public class MyManagerTests : TsPlayModeTestBase } ``` +Every test runs in an empty scene of its own, which is unloaded after the test. Every GameObject +the test or the builder created goes with it, so the next test starts from nothing. + `BuildTsRoot` composes your generated root with `AddComponent` rather than loading a saved scene, then drives the same `_TsLogStart`/`_TsMemoryStart`/`_TsGlobalStart`/`_TsPoolStart`/ `_TsConstructStart`/`_TsInstanceStart` sequence a real client only gets from Unity @@ -65,57 +59,30 @@ field; `With(fieldName, instance)` assigns an instance you already built your Any root field neither call touched is auto-filled with a bare `AddComponent` stand-in the moment `Build()` runs, so a generated stage that unconditionally iterates every field of its module (`_TsGlobalStart` calling `TsConstruct` on every registered global, for example) never -throws a null reference on a field the test never cared about. `TsPlayModeTestBase` tracks -and tears down every GameObject the builder created, including those auto-filled -stand-ins, automatically. - -Two known VRChat SDK and Unity Test Framework Play Mode testing defects are patched -automatically for any class extending `TsPlayModeTestBase`. One, a `ClientSim` player-object -leak that piles up conflicting writers on the same file across back-to-back tests, goes -through `FixupRegistry`, which runs every registered `IPlayModeEnvironmentFixup`'s hooks at -the matching lifecycle point; if a future SDK version fixes it upstream, disable it with -`FixupRegistry.Disable()` rather than forking the testing framework itself. The -other, restoring Unity Test Framework's own Play Mode result reporting (which VRChat SDK's -event-listener filter otherwise strips), isn't registered there: it patches itself in via -its own domain-load-time static constructor the first time it detects a `TsPlayModeTestBase` -subclass anywhere in the project, before `FixupRegistry` or any test lifecycle hook ever -runs. `FixupRegistry.Disable()` has no effect on it, since it's never part of the -registry's own list to begin with. - -Stopping TsVRC's own reactive codegen from regenerating against a test's temporary scene -during a test run (see [`TsGenerator`](../codegen/internals/ts-generator)'s -`SuppressAutomaticTriggers`) requires reaching an `internal` editor API from outside the -package, granted once, package-wide, via an `InternalsVisibleTo` attribute, rather than -something you need to configure yourself. - -That suppression itself isn't automatic per test class the way the `TsPlayModeTestBase` -fixups are. It has to be armed once per test assembly, for the assembly's entire run, via -`AutomaticTriggersSetUpFixtureBase`. See [Set up automated -testing](../how-to/set-up-automated-testing) for the one-line subclass this requires. - -## Two companion assemblies, and why they're separate - -`Tsvrc.Testing.Framework` is a plain, editor-only C# library: none of its own types are meant -to be `AddComponent`'d as real Udon components. Two more pieces need to actually run as -`UdonSharpBehaviour`s, so they live in their own sibling assemblies instead: - -- **`Tsvrc.Testing.Behaviours`** provides `TsCallbackRecorder`, a generic `TsSubscribe`/ - `TsEmit` listener double that records how many times each callback fired (and, via an - optional shared log, cross-listener firing order). -- **`Tsvrc.Testing.UI`** provides `TestListItem` (a generic `ListItem` double) and - `TsvrcListTestBuilder.Build(...)`, which wires a bare `TsvrcList`'s private fields the same - way you'd otherwise have to by hand. - -The split exists because of an UdonSharp constraint: `AddComponent` only works for scripts -in an assembly registered as a U# assembly, and once registered, UdonSharp Udon-compiles -*every* source file in that assembly. Keeping the reflection/ClientSim helpers in a -separate, non-U#-registered assembly is what stops them from being dragged into a -compilation context they were never meant to run under. Some of them reach `internal` -editor APIs that Udon's compiler simply can't resolve. Add whichever companion assembly a -given test actually needs, alongside `Tsvrc.Testing.Framework`; none of the three depend on -each other beyond that. All three carry a `UNITY_INCLUDE_TESTS` define constraint, so none -of them compile at all outside a test context, regardless of platform: nothing here can ship -in a built world by accident. - -See [Set up automated testing](../how-to/set-up-automated-testing) for how to reference -these assemblies from your own project. +throws a null reference on a field the test never cared about. + +## Codegen during a test run + +A test that creates or changes scene objects would otherwise trigger TsVRC's reactive codegen, +which could regenerate your project's real output against the test's temporary scene. +[`TsGenerator`](../codegen/internals/ts-generator) skips every automatic trigger while tests run, +from the Test Runner window or the command line, so there's nothing to set up for it. + +## List UI: Tsvrc.Testing.UI + +`Tsvrc.Testing.UI` provides `TestListItem`, a generic `ListItem` double, and +`TsvrcListTestBuilder.Build(...)`, which wires a bare `TsvrcList`'s private fields the same way +you'd otherwise have to by hand. + +## The assemblies + +TsVRC ships two testing assemblies, and a test assembly references whichever it needs: + +- **`Tsvrc.Testing.Framework`** holds `TsPlayModeTestBase` and the root builder. It's Editor-only + plain C#, and none of its types are added to GameObjects. +- **`Tsvrc.Testing.UI`** holds the list doubles, which tests add to GameObjects as components. + Unity won't add a behaviour from an Editor-only assembly to a GameObject, so this one targets + every platform. + +Both compile only when Unity Test Framework includes tests (`UNITY_INCLUDE_TESTS`), so neither +can reach a world build. From 6ea65325e69801f36a3cd8dc59ffdd31120eecea Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:54:05 -0600 Subject: [PATCH 07/37] Document the multi-project sidebar and cross-linking rules Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- CONTRIBUTING.md | 9 ++++++++- docs/udon-test-kit/reference/test-assembly-creator.md | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ef27b8..a2682e7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,14 @@ duplicating, words/phrases to avoid, sentence shape, formatting rules. Each project under the `tsvrc` org gets its own `docs//` folder here rather than its own docs repo. Adding docs for a new project means adding a folder here and a -matching top-level category in `sidebars.ts`, not creating a new repo. +matching top-level category in `sidebars.ts`, not creating a new repo. Every project shares +one sidebar, so a category whose label another project already uses, such as "Reference", +needs a unique `key`; without one, the Spanish build fails on a duplicate translation key. +`docs/udon-test-kit/` is set up this way. + +A project's docs stand on their own. When one project depends on another, as TsVRC does on +Udon Test Kit, the dependent project's pages link to the other's where a reader needs it, and +the project depended on never mentions its consumers. ## Adding a translation diff --git a/docs/udon-test-kit/reference/test-assembly-creator.md b/docs/udon-test-kit/reference/test-assembly-creator.md index 12c36fd..ca34722 100644 --- a/docs/udon-test-kit/reference/test-assembly-creator.md +++ b/docs/udon-test-kit/reference/test-assembly-creator.md @@ -54,4 +54,4 @@ Each example compiles and passes as created. a warning. See [Test scripts that have no assembly definition](../how-to/testing-scripts-without-an-assembly-definition). - `TestAssemblyCreator.Create(string parentFolder = "Assets")` does the same from code, and returns - the new folder's path. + the new folder's path. `TestAssemblyCreator.MenuPath` is the menu item's path. From ebad3f35889ee298ab27d56ec0131a0aace91823 Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:03:28 -0600 Subject: [PATCH 08/37] Drop the SDK version reason from the kit's requirements Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- docs/udon-test-kit/add-to-your-project.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/udon-test-kit/add-to-your-project.md b/docs/udon-test-kit/add-to-your-project.md index a9c0fc1..e655c86 100644 --- a/docs/udon-test-kit/add-to-your-project.md +++ b/docs/udon-test-kit/add-to-your-project.md @@ -17,8 +17,7 @@ VRChat Worlds SDK version the kit needs. - **Unity 2022.3.** - **The [VRChat Worlds SDK](https://creators.vrchat.com/worlds/) 3.8.2 or later**, which includes - UdonSharp and ClientSim. Earlier versions stop Play Mode when a scene has no scene descriptor, - and Unity Test Framework's test scene has none, so every Play Mode run hangs. + UdonSharp and ClientSim. - **Unity Test Framework**, which new Unity projects already include. ## Install the package From 1f59f9b15fdc55b6787e4dec6f0237829e7cb99b Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:14:57 -0600 Subject: [PATCH 09/37] Share one install page between projects Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- CONTRIBUTING.md | 5 ++ docs/install-a-package.md | 40 +++++++++++ docs/tsvrc/add-to-your-project.md | 71 ++++--------------- docs/tsvrc/how-to/set-up-automated-testing.md | 4 +- docs/tsvrc/testing/testing-your-world.md | 2 +- docs/udon-test-kit/add-to-your-project.md | 53 ++------------ .../current/install-a-package.md | 41 +++++++++++ sidebars.ts | 2 + 8 files changed, 107 insertions(+), 111 deletions(-) create mode 100644 docs/install-a-package.md create mode 100644 i18n/es/docusaurus-plugin-content-docs/current/install-a-package.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a2682e7..6877368 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,6 +26,11 @@ one sidebar, so a category whose label another project already uses, such as "Re needs a unique `key`; without one, the Spanish build fails on a duplicate translation key. `docs/udon-test-kit/` is set up this way. +A page every project shares, such as [Install a TsVRC package](docs/install-a-package.md), +sits at the root of `docs/` instead. Each project's own install page links to it and adds only +what's specific to that project: the package name, requirements, and how to check that it +worked. + A project's docs stand on their own. When one project depends on another, as TsVRC does on Udon Test Kit, the dependent project's pages link to the other's where a reader needs it, and the project depended on never mentions its consumers. diff --git a/docs/install-a-package.md b/docs/install-a-package.md new file mode 100644 index 0000000..f17f832 --- /dev/null +++ b/docs/install-a-package.md @@ -0,0 +1,40 @@ +--- +id: install-a-package +title: Install a TsVRC package +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Install a TsVRC package + +Every TsVRC package installs the same way. A project's own install page says which package to add, +what it needs, and how to check that it worked. + + + + +1. Add the TsVRC repository to the Creator Companion, once per computer: click + [Add to VCC](vcc://vpm/addRepo?url=https%3A%2F%2Fvpm.tsvrc.com%2Findex.json), or add + `https://vpm.tsvrc.com/index.json` as described in VRChat's + [Community Repositories](https://vcc.docs.vrchat.com/guides/community-repositories/) guide. +2. Click **Manage Project** next to your project, then **+** next to the package. + +The Creator Companion adds the package to your project's `Packages` folder, along with the packages +it depends on, and resolves the VRChat Worlds SDK version it needs. + + + + +1. Download the package's latest `.unitypackage` from the GitHub releases page its install page + links to. +2. With your project open, double-click the file, or use **Assets > Import Package > Custom + Package**, and import everything. + +The package goes in your project's `Assets` folder. A `.unitypackage` can't install the packages it +depends on or check your VRChat Worlds SDK version, so import those and check the version yourself. +In a project that uses the Creator Companion for other packages, use it for this one too: updates +and dependencies of a hand-imported package are yours to track. + + + diff --git a/docs/tsvrc/add-to-your-project.md b/docs/tsvrc/add-to-your-project.md index 5cbd457..fc41494 100644 --- a/docs/tsvrc/add-to-your-project.md +++ b/docs/tsvrc/add-to-your-project.md @@ -4,72 +4,25 @@ title: Add TsVRC to your project sidebar_position: 1 --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - # Add TsVRC to your project -Get TsVRC into a Unity project either through the VRChat Creator Companion (VCC) or by -importing a `.unitypackage` by hand. VCC is strongly recommended: it resolves the VRChat -Worlds SDK version TsVRC depends on for you, something a `.unitypackage` import leaves -you to sort out yourself. +Install TsVRC in a VRChat world project. ## Requirements - **Unity 2022.3.** TsVRC's package manifest targets this version specifically. -- **A Unity world project with the [VRChat Worlds - SDK](https://creators.vrchat.com/worlds/) `3.10.x`.** VCC resolves this automatically - when you install TsVRC; importing the `.unitypackage` needs it already in your project. +- **The [VRChat Worlds SDK](https://creators.vrchat.com/worlds/) `3.10.x`.** ## Install the package - - - -If TsVRC's repository isn't in your Creator Companion yet, add it first (a one-time -step, the same as adding any other [community -repository](https://vcc.docs.vrchat.com/guides/community-repositories/)): - -- **With VCC installed**, click [Add TsVRC to Creator - Companion](vcc://vpm/addRepo?url=https%3A%2F%2Fvpm.tsvrc.com%2Findex.json) - to open VCC directly to the add-repository prompt. -- **Or paste the listing URL manually**: open VCC, go to **Settings > Packages > Add - Repository**, and paste: - - ``` - https://vpm.tsvrc.com/index.json - ``` - -Once the repository's added: - -1. Open the Creator Companion, select your project, and click **Manage Project**. -2. Find **TsVRC** (`com.tsvrc.core`) in the package list and press the **+** next to it - to install it. -3. Wait for Unity to finish importing and recompiling. - -The Creator Companion also installs [Udon Test Kit](/docs/udon-test-kit/intro) -(`com.tsvrc.udon-test-kit`), which TsVRC depends on for its testing helpers. - - - - -1. Download the latest `.unitypackage` from [TsVRC's GitHub - Releases](https://github.com/tsvrc/tsvrc-core/releases). -2. With your project open, double-click the downloaded file (or use **Assets > Import - Package > Custom Package**) and import everything. -3. Wait for Unity to finish importing and recompiling. - -A `.unitypackage` can't bring the packages TsVRC depends on, so also import -[Udon Test Kit's](/docs/udon-test-kit/add-to-your-project) `.unitypackage`, unless your -project already has the kit. - -Nothing here resolves the VRChat Worlds SDK version for you, so confirm it's already at -`3.10.x` before importing. If your project also uses VCC for other packages, prefer the -VCC tab instead: mixing a hand-imported package with VCC-managed ones makes future -updates and dependency resolution your own responsibility to track. +Install **TsVRC** (`com.tsvrc.core`) as described in +[Install a TsVRC package](/docs/install-a-package). Its `.unitypackage` is on +[TsVRC's GitHub releases](https://github.com/tsvrc/tsvrc-core/releases). - - +TsVRC depends on [Udon Test Kit](/docs/udon-test-kit/intro) (`com.tsvrc.udon-test-kit`) for its +testing helpers. The Creator Companion installs it along with TsVRC. With the `.unitypackage`, +import [the kit's](/docs/udon-test-kit/add-to-your-project) too, unless your project already has +it. You'll know it worked when Unity's menu bar gains a **Tsvrc** menu. Everything TsVRC does from here runs from there. @@ -84,9 +37,9 @@ the codegen and Configure-window tooling. See [How TsVRC fits together](./core-concepts/how-it-fits-together) for how those two halves relate. Two further assemblies, `Tsvrc.Testing.Framework` and `Tsvrc.Testing.UI`, exist purely to -support testing a *consumer's* own project, on top of Udon Test Kit, kept separate from -each other and from `Tsvrc.Runtime`/`Tsvrc.Editor` for the reasons covered in [Testing -your world](./testing/testing-your-world). +support testing a *consumer's* own project, kept separate from each other and from +`Tsvrc.Runtime`/`Tsvrc.Editor` for the reasons covered in [Testing your +world](./testing/testing-your-world). ## Next diff --git a/docs/tsvrc/how-to/set-up-automated-testing.md b/docs/tsvrc/how-to/set-up-automated-testing.md index 77902e7..79bbbe7 100644 --- a/docs/tsvrc/how-to/set-up-automated-testing.md +++ b/docs/tsvrc/how-to/set-up-automated-testing.md @@ -23,8 +23,8 @@ UdonSharp assemblies your scripts use. ## 2. Create your test assemblies -TsVRC's testing helpers build on Udon Test Kit, which the Creator Companion installs along -with TsVRC. Create your test assemblies with the kit's menu item, as described in +TsVRC's testing helpers build on Udon Test Kit. Create your test assemblies with the kit's +menu item, as described in [Create your test assemblies](/docs/udon-test-kit/add-to-your-project#create-your-test-assemblies). They already reference the kit, the VRChat SDK, ClientSim and your project's assembly from step 1. diff --git a/docs/tsvrc/testing/testing-your-world.md b/docs/tsvrc/testing/testing-your-world.md index bcd02c1..2cd9065 100644 --- a/docs/tsvrc/testing/testing-your-world.md +++ b/docs/tsvrc/testing/testing-your-world.md @@ -8,7 +8,7 @@ sidebar_position: 1 TsVRC's testing helpers build on [Udon Test Kit](/docs/udon-test-kit/intro), which runs Play Mode tests in a real ClientSim session and works around the VRChat SDK problems that break them. TsVRC -depends on the kit, so the Creator Companion installs it along with TsVRC. +depends on the kit, so it's already in your project. This page covers what TsVRC adds for testing a TsVRC world: a base class that builds your generated root from code, helpers for list UI, and codegen that holds back while tests run. For diff --git a/docs/udon-test-kit/add-to-your-project.md b/docs/udon-test-kit/add-to-your-project.md index e655c86..7f58260 100644 --- a/docs/udon-test-kit/add-to-your-project.md +++ b/docs/udon-test-kit/add-to-your-project.md @@ -4,14 +4,9 @@ title: Add Udon Test Kit to your project sidebar_position: 2 --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - # Add Udon Test Kit to your project -Install the kit in a VRChat world project, then create the test assemblies your tests go in. The -VRChat Creator Companion (VCC) is the recommended way to install it, because it also resolves the -VRChat Worlds SDK version the kit needs. +Install the kit in a VRChat world project, then create the test assemblies your tests go in. ## Requirements @@ -22,48 +17,9 @@ VRChat Worlds SDK version the kit needs. ## Install the package - - - -If the `vpm.tsvrc.com` repository isn't in your Creator Companion yet, add it first. It's a -one-time step, the same as adding any other -[community repository](https://vcc.docs.vrchat.com/guides/community-repositories/): - -- **With VCC installed**, click - [Add the repository to Creator Companion](vcc://vpm/addRepo?url=https%3A%2F%2Fvpm.tsvrc.com%2Findex.json) - to open VCC at the add-repository prompt. -- **Or paste the listing URL yourself**: open VCC, go to **Settings > Packages > Add Repository**, - and paste: - - ``` - https://vpm.tsvrc.com/index.json - ``` - -Once the repository is added: - -1. Open the Creator Companion, select your project, and click **Manage Project**. -2. Find **Udon Test Kit** (`com.tsvrc.udon-test-kit`) in the package list and press the **+** next - to it. -3. Wait for Unity to finish importing and recompiling. - -The Creator Companion installs the kit in your project's `Packages` folder. - - - - -1. Download the latest `.unitypackage` from the kit's - [GitHub releases](https://github.com/tsvrc/udon-test-kit/releases). -2. With your project open, double-click the downloaded file, or use **Assets > Import Package > - Custom Package**, and import everything. -3. Wait for Unity to finish importing and recompiling. - -The kit goes in your project's `Assets` folder and works the same from there. Nothing here checks -the VRChat Worlds SDK version for you, so confirm it's 3.8.2 or later first. A `.unitypackage` can't -carry the command-line script; see -[Run tests from the command line](./how-to/running-tests-from-the-command-line) for where to get it. - - - +Install **Udon Test Kit** (`com.tsvrc.udon-test-kit`) as described in +[Install a TsVRC package](/docs/install-a-package). Its `.unitypackage` is on the kit's +[GitHub releases](https://github.com/tsvrc/udon-test-kit/releases). You'll know it worked when Unity's **Tools** menu has an **Udon Test Kit** entry. @@ -79,7 +35,6 @@ Unity Test Framework finds tests in test assemblies, and the kit creates the thr 1. In the Project window, select the folder your tests should live in, such as `Assets`. 2. Choose **Assets > Create > Udon Test Kit > Test Assemblies**. -3. Wait for Unity to compile. You get a `Tests` folder with three assemblies, each with an example that works: diff --git a/i18n/es/docusaurus-plugin-content-docs/current/install-a-package.md b/i18n/es/docusaurus-plugin-content-docs/current/install-a-package.md new file mode 100644 index 0000000..0cb8f80 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/install-a-package.md @@ -0,0 +1,41 @@ +--- +id: install-a-package +title: Instalar un paquete de TsVRC +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Instalar un paquete de TsVRC + +Todos los paquetes de TsVRC se instalan de la misma forma. La página de instalación de cada +proyecto indica qué paquete añadir, qué necesita y cómo comprobar que funcionó. + + + + +1. Añade el repositorio de TsVRC al Creator Companion, una vez por computadora: haz clic en + [Add to VCC](vcc://vpm/addRepo?url=https%3A%2F%2Fvpm.tsvrc.com%2Findex.json), o añade + `https://vpm.tsvrc.com/index.json` como se describe en la guía + [Community Repositories](https://vcc.docs.vrchat.com/guides/community-repositories/) de VRChat. +2. Haz clic en **Manage Project** junto a tu proyecto y luego en **+** junto al paquete. + +El Creator Companion añade el paquete a la carpeta `Packages` de tu proyecto, junto con los +paquetes de los que depende, y resuelve la versión del VRChat Worlds SDK que necesita. + + + + +1. Descarga el `.unitypackage` más reciente del paquete desde la página de releases de GitHub que + enlaza su página de instalación. +2. Con tu proyecto abierto, haz doble clic en el archivo, o usa **Assets > Import Package > Custom + Package**, e importa todo. + +El paquete queda en la carpeta `Assets` de tu proyecto. Un `.unitypackage` no puede instalar los +paquetes de los que depende ni comprobar tu versión del VRChat Worlds SDK, así que importa esos +paquetes y comprueba la versión tú mismo. En un proyecto que usa el Creator Companion para otros +paquetes, úsalo también para este: las actualizaciones y dependencias de un paquete importado a +mano quedan a tu cargo. + + + diff --git a/sidebars.ts b/sidebars.ts index 69d7903..7c4952e 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -152,6 +152,8 @@ const sidebars: SidebarsConfig = { }, ], }, + // Shared by every project, so it sits at the docs root rather than under one project. + 'install-a-package', ], }; From f33b75fe740f930789e214906f786c3925f263db Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:30:56 -0600 Subject: [PATCH 10/37] Mention the command-line test script in the kit's intro Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- docs/udon-test-kit/intro.md | 7 +++++++ .../current/udon-test-kit/intro.md | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/docs/udon-test-kit/intro.md b/docs/udon-test-kit/intro.md index f38d4e9..278ef34 100644 --- a/docs/udon-test-kit/intro.md +++ b/docs/udon-test-kit/intro.md @@ -16,6 +16,13 @@ did with all of it. Each test can run in a scene of its own, including your worl Spies record the calls your code makes by method name, such as `SendCustomEvent`, and a menu item creates the test assemblies a world needs in one step. +The kit also comes with `Invoke-UnityTests.ps1`, a PowerShell script that runs a project's Edit +Mode and Play Mode tests in one command, without clicking through the Test Runner. It prints only +the tests that didn't pass, with their messages, and exits with an error code when any failed, so +it can check your tests before you commit or fail a build step. The script doesn't depend on the +kit: copy it into any Unity project. See +[Run tests from the command line](./how-to/running-tests-from-the-command-line). + The kit compiles only when Unity Test Framework includes tests, so nothing from it reaches a world build. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md b/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md index 34837fa..2d46dc6 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md +++ b/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md @@ -17,6 +17,13 @@ escena de tu propio mundo. Los spies registran las llamadas que tu código hace método, como `SendCustomEvent`, y un elemento de menú crea en un solo paso los assemblies de test que necesita un mundo. +El kit también incluye `Invoke-UnityTests.ps1`, un script de PowerShell que ejecuta los tests de +modo Edit y de modo Play de un proyecto con un solo comando, sin pasar por el Test Runner. Muestra +solo los tests que no pasaron, con sus mensajes, y termina con un código de error cuando alguno +falló, así que sirve para comprobar tus tests antes de hacer commit o para hacer fallar un paso de +build. El script no depende del kit: cópialo a cualquier proyecto de Unity. Consulta +[Ejecutar tests desde la línea de comandos](./how-to/running-tests-from-the-command-line). + El kit solo se compila cuando Unity Test Framework incluye los tests, así que nada de él llega a la build de un mundo. From 869520ce338e61264d38505d8476043542c1c5c6 Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:36:31 -0600 Subject: [PATCH 11/37] Add local search Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- CONTRIBUTING.md | 4 +- docs/tsvrc/intro.md | 2 +- docs/udon-test-kit/intro.md | 4 +- docusaurus.config.ts | 16 +- i18n/es/code.json | 30 + .../current/tsvrc/intro.md | 2 +- .../current/udon-test-kit/intro.md | 2 +- package-lock.json | 517 ++++++++++++++++++ package.json | 1 + 9 files changed, 571 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6877368..bc04f2f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,7 +51,9 @@ is a comment, not `{#id}`: `## Play Mode tests {/* #play-mode-tests */}`. Sideba labels live in `sidebars.ts` itself, not a translatable file, so they render in English for every locale regardless of translated content underneath. Don't override Docusaurus's theme chrome (pagination, admonition labels, sidebar buttons) in this site's own -`code.json`; it ships its own translated defaults per locale. +`code.json`; it ships its own translated defaults per locale. The search plugin +(`@easyops-cn/docusaurus-search-local`) ships no Spanish, so its `theme.SearchBar.*` and +`theme.SearchPage.*` strings are the exception and live in `code.json`. Before translating a technical term, check how the target language's own community or official docs (Unity's localized manual, GitHub's glossary, VRChat's community) actually diff --git a/docs/tsvrc/intro.md b/docs/tsvrc/intro.md index 6e211c8..42f44e3 100644 --- a/docs/tsvrc/intro.md +++ b/docs/tsvrc/intro.md @@ -29,7 +29,7 @@ Jump straight to what you need: [showing player positions](./how-to/showing-player-positions), [transferring data between clients](./how-to/transferring-data-between-clients), or [wiring a ready check](./how-to/wiring-a-ready-check). -- **Looking up a type's behavior** — start from +- **Looking up a type's behavior** — search for its name, start from [How TsVRC fits together](./core-concepts/how-it-fits-together) and follow its links, or browse Reference in the sidebar, grouped by area: players, networking, UI, codegen, and so on. diff --git a/docs/udon-test-kit/intro.md b/docs/udon-test-kit/intro.md index 278ef34..bad80b4 100644 --- a/docs/udon-test-kit/intro.md +++ b/docs/udon-test-kit/intro.md @@ -40,8 +40,8 @@ Jump straight to what you need: [test a player who isn't the owner](./how-to/testing-a-player-who-isnt-the-owner), [check that saved data survives a rejoin](./how-to/testing-saved-data), or [run tests from the command line](./how-to/running-tests-from-the-command-line). -- **Looking up a type's behavior** — start from [ClientSimSession](./reference/clientsim-session), - or browse Reference in the sidebar. +- **Looking up a type's behavior** — search for its name, start from + [ClientSimSession](./reference/clientsim-session), or browse Reference in the sidebar. - **Understanding what the kit changes and why** — [What the kit works around](./explanations/what-the-kit-works-around) and [a fresh session for every test](./explanations/a-fresh-session-for-every-test). diff --git a/docusaurus.config.ts b/docusaurus.config.ts index aa63bbb..427f73b 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -28,7 +28,21 @@ const config: Config = { markdown: { mermaid: true, }, - themes: ['@docusaurus/theme-mermaid'], + themes: [ + '@docusaurus/theme-mermaid', + [ + // Builds the search index at build time, so search works on GitHub Pages with no + // external service. + '@easyops-cn/docusaurus-search-local', + { + hashed: true, + language: ['en', 'es'], + indexBlog: false, + explicitSearchResultPath: true, + highlightSearchTermsOnTargetPage: true, + }, + ], + ], i18n: { defaultLocale: 'en', diff --git a/i18n/es/code.json b/i18n/es/code.json index bbf370d..9748b46 100644 --- a/i18n/es/code.json +++ b/i18n/es/code.json @@ -208,5 +208,35 @@ }, "support.options.kofi.cta": { "message": "Apoya en Ko-fi" + }, + "theme.SearchBar.label": { + "message": "Buscar" + }, + "theme.SearchBar.seeAll": { + "message": "Ver todos los resultados" + }, + "theme.SearchBar.noResultsText": { + "message": "Sin resultados" + }, + "theme.SearchBar.searchInContext": { + "message": "Ver todos los resultados en \"{context}\"" + }, + "theme.SearchBar.seeAllOutsideContext": { + "message": "Ver todos los resultados fuera de \"{context}\"" + }, + "theme.SearchPage.existingResultsTitle": { + "message": "Resultados de búsqueda para \"{query}\"" + }, + "theme.SearchPage.emptyResultsTitle": { + "message": "Buscar en la documentación" + }, + "theme.SearchPage.documentsFound.plurals": { + "message": "1 documento encontrado|{count} documentos encontrados" + }, + "theme.SearchPage.noResultsText": { + "message": "No se encontraron documentos" + }, + "theme.SearchPage.searchContext.everywhere": { + "message": "En todas partes" } } diff --git a/i18n/es/docusaurus-plugin-content-docs/current/tsvrc/intro.md b/i18n/es/docusaurus-plugin-content-docs/current/tsvrc/intro.md index 378feeb..72c3084 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/tsvrc/intro.md +++ b/i18n/es/docusaurus-plugin-content-docs/current/tsvrc/intro.md @@ -32,7 +32,7 @@ Ve directo a lo que necesitas: [mostrar las posiciones de los jugadores](./how-to/showing-player-positions), [transferir datos entre clientes](./how-to/transferring-data-between-clients) o [conectar un ready check](./how-to/wiring-a-ready-check). -- **Buscar el comportamiento de un tipo concreto** — empieza por +- **Buscar el comportamiento de un tipo concreto** — busca su nombre, empieza por [Cómo encaja TsVRC](./core-concepts/how-it-fits-together) y sigue sus enlaces, o explora Reference en la barra lateral, agrupado por área: jugadores, red, UI, codegen, etc. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md b/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md index 2d46dc6..58ca782 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md +++ b/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md @@ -42,7 +42,7 @@ Ve directo a lo que necesitas: [probar a un jugador que no es el dueño](./how-to/testing-a-player-who-isnt-the-owner), [comprobar que los datos guardados sobreviven al volver a entrar](./how-to/testing-saved-data) o [ejecutar tests desde la línea de comandos](./how-to/running-tests-from-the-command-line). -- **Buscar el comportamiento de un tipo concreto** — empieza por +- **Buscar el comportamiento de un tipo concreto** — busca su nombre, empieza por [ClientSimSession](./reference/clientsim-session) o explora Reference en la barra lateral. - **Entender qué cambia el kit y por qué** — [Lo que el kit resuelve](./explanations/what-the-kit-works-around) y diff --git a/package-lock.json b/package-lock.json index d9842e6..3ad6120 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,11 +7,13 @@ "": { "name": "tsvrc-docs", "version": "0.0.0", + "license": "CC-BY-4.0", "dependencies": { "@docusaurus/core": "3.10.2", "@docusaurus/faster": "3.10.2", "@docusaurus/preset-classic": "3.10.2", "@docusaurus/theme-mermaid": "^3.10.2", + "@easyops-cn/docusaurus-search-local": "0.55.3", "@fontsource/ibm-plex-mono": "^5.3.0", "@fontsource/ibm-plex-sans": "^5.3.0", "@fontsource/space-grotesk": "^5.3.0", @@ -4254,6 +4256,125 @@ "node": ">=20.0" } }, + "node_modules/@easyops-cn/autocomplete.js": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@easyops-cn/autocomplete.js/-/autocomplete.js-0.38.1.tgz", + "integrity": "sha512-drg76jS6syilOUmVNkyo1c7ZEBPcPuK+aJA7AksM5ZIIbV57DMHCywiCr+uHyv8BE5jUTU98j/H7gVrkHrWW3Q==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "immediate": "^3.2.3" + } + }, + "node_modules/@easyops-cn/docusaurus-search-local": { + "version": "0.55.3", + "resolved": "https://registry.npmjs.org/@easyops-cn/docusaurus-search-local/-/docusaurus-search-local-0.55.3.tgz", + "integrity": "sha512-PlMKmxuonvZ1C+/zJS1hJaF1xaxD1TsUvOMzpP6DdQMtZqmTaYjcLGM12ePzl0m1rp3AgfTBeDbFNHQhwnH/dA==", + "license": "MIT", + "dependencies": { + "@docusaurus/plugin-content-docs": "^2 || ^3", + "@docusaurus/theme-translations": "^2 || ^3", + "@docusaurus/utils": "^2 || ^3", + "@docusaurus/utils-common": "^2 || ^3", + "@docusaurus/utils-validation": "^2 || ^3", + "@easyops-cn/autocomplete.js": "^0.38.1", + "@node-rs/jieba": "^1.6.0", + "cheerio": "^1.0.0", + "clsx": "^2.1.1", + "comlink": "^4.4.2", + "debug": "^4.2.0", + "fs-extra": "^10.0.0", + "klaw-sync": "^6.0.0", + "lunr": "^2.3.9", + "lunr-languages": "^1.4.0", + "mark.js": "^8.11.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@docusaurus/theme-common": "^2 || ^3", + "open-ask-ai": "^0.7.3", + "react": "^16.14.0 || ^17 || ^18 || ^19", + "react-dom": "^16.14.0 || 17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "open-ask-ai": { + "optional": true + } + } + }, + "node_modules/@easyops-cn/docusaurus-search-local/node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/@easyops-cn/docusaurus-search-local/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@easyops-cn/docusaurus-search-local/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@easyops-cn/docusaurus-search-local/node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, "node_modules/@emnapi/core": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", @@ -4987,6 +5108,283 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@node-rs/jieba": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba/-/jieba-1.10.4.tgz", + "integrity": "sha512-GvDgi8MnBiyWd6tksojej8anIx18244NmIOc1ovEw8WKNUejcccLfyu8vj66LWSuoZuKILVtNsOy4jvg3aoxIw==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@node-rs/jieba-android-arm-eabi": "1.10.4", + "@node-rs/jieba-android-arm64": "1.10.4", + "@node-rs/jieba-darwin-arm64": "1.10.4", + "@node-rs/jieba-darwin-x64": "1.10.4", + "@node-rs/jieba-freebsd-x64": "1.10.4", + "@node-rs/jieba-linux-arm-gnueabihf": "1.10.4", + "@node-rs/jieba-linux-arm64-gnu": "1.10.4", + "@node-rs/jieba-linux-arm64-musl": "1.10.4", + "@node-rs/jieba-linux-x64-gnu": "1.10.4", + "@node-rs/jieba-linux-x64-musl": "1.10.4", + "@node-rs/jieba-wasm32-wasi": "1.10.4", + "@node-rs/jieba-win32-arm64-msvc": "1.10.4", + "@node-rs/jieba-win32-ia32-msvc": "1.10.4", + "@node-rs/jieba-win32-x64-msvc": "1.10.4" + } + }, + "node_modules/@node-rs/jieba-android-arm-eabi": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-android-arm-eabi/-/jieba-android-arm-eabi-1.10.4.tgz", + "integrity": "sha512-MhyvW5N3Fwcp385d0rxbCWH42kqDBatQTyP8XbnYbju2+0BO/eTeCCLYj7Agws4pwxn2LtdldXRSKavT7WdzNA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-android-arm64": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-android-arm64/-/jieba-android-arm64-1.10.4.tgz", + "integrity": "sha512-XyDwq5+rQ+Tk55A+FGi6PtJbzf974oqnpyCcCPzwU3QVXJCa2Rr4Lci+fx8oOpU4plT3GuD+chXMYLsXipMgJA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-darwin-arm64": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-darwin-arm64/-/jieba-darwin-arm64-1.10.4.tgz", + "integrity": "sha512-G++RYEJ2jo0rxF9626KUy90wp06TRUjAsvY/BrIzEOX/ingQYV/HjwQzNPRR1P1o32a6/U8RGo7zEBhfdybL6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-darwin-x64": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-darwin-x64/-/jieba-darwin-x64-1.10.4.tgz", + "integrity": "sha512-MmDNeOb2TXIZCPyWCi2upQnZpPjAxw5ZGEj6R8kNsPXVFALHIKMa6ZZ15LCOkSTsKXVC17j2t4h+hSuyYb6qfQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-freebsd-x64": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-freebsd-x64/-/jieba-freebsd-x64-1.10.4.tgz", + "integrity": "sha512-/x7aVQ8nqUWhpXU92RZqd333cq639i/olNpd9Z5hdlyyV5/B65LLy+Je2B2bfs62PVVm5QXRpeBcZqaHelp/bg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-linux-arm-gnueabihf": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-arm-gnueabihf/-/jieba-linux-arm-gnueabihf-1.10.4.tgz", + "integrity": "sha512-crd2M35oJBRLkoESs0O6QO3BBbhpv+tqXuKsqhIG94B1d02RVxtRIvSDwO33QurxqSdvN9IeSnVpHbDGkuXm3g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-linux-arm64-gnu": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-arm64-gnu/-/jieba-linux-arm64-gnu-1.10.4.tgz", + "integrity": "sha512-omIzNX1psUzPcsdnUhGU6oHeOaTCuCjUgOA/v/DGkvWC1jLcnfXe4vdYbtXMh4XOCuIgS1UCcvZEc8vQLXFbXQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-linux-arm64-musl": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-arm64-musl/-/jieba-linux-arm64-musl-1.10.4.tgz", + "integrity": "sha512-Y/tiJ1+HeS5nnmLbZOE+66LbsPOHZ/PUckAYVeLlQfpygLEpLYdlh0aPpS5uiaWMjAXYZYdFkpZHhxDmSLpwpw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-linux-x64-gnu": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-x64-gnu/-/jieba-linux-x64-gnu-1.10.4.tgz", + "integrity": "sha512-WZO8ykRJpWGE9MHuZpy1lu3nJluPoeB+fIJJn5CWZ9YTVhNDWoCF4i/7nxz1ntulINYGQ8VVuCU9LD86Mek97g==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-linux-x64-musl": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-x64-musl/-/jieba-linux-x64-musl-1.10.4.tgz", + "integrity": "sha512-uBBD4S1rGKcgCyAk6VCKatEVQb6EDD5I40v/DxODi5CuZVCANi9m5oee/MQbAoaX7RydA2f0OSCE9/tcwXEwUg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-wasm32-wasi": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-wasm32-wasi/-/jieba-wasm32-wasi-1.10.4.tgz", + "integrity": "sha512-Y2umiKHjuIJy0uulNDz9SDYHdfq5Hmy7jY5nORO99B4pySKkcrMjpeVrmWXJLIsEKLJwcCXHxz8tjwU5/uhz0A==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/jieba-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@node-rs/jieba-win32-arm64-msvc": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-win32-arm64-msvc/-/jieba-win32-arm64-msvc-1.10.4.tgz", + "integrity": "sha512-nwMtViFm4hjqhz1it/juQnxpXgqlGltCuWJ02bw70YUDMDlbyTy3grCJPpQQpueeETcALUnTxda8pZuVrLRcBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-win32-ia32-msvc": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-win32-ia32-msvc/-/jieba-win32-ia32-msvc-1.10.4.tgz", + "integrity": "sha512-DCAvLx7Z+W4z5oKS+7vUowAJr0uw9JBw8x1Y23Xs/xMA4Em+OOSiaF5/tCJqZUCJ8uC4QeImmgDFiBqGNwxlyA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-win32-x64-msvc": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-win32-x64-msvc/-/jieba-win32-x64-msvc-1.10.4.tgz", + "integrity": "sha512-+sqemSfS1jjb+Tt7InNbNzrRh1Ua3vProVvC4BZRPg010/leCbGFFiQHpzcPRfpxAXZrzG5Y0YBTsPzN/I4yHQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -8749,6 +9147,12 @@ "node": ">=10" } }, + "node_modules/comlink": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz", + "integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==", + "license": "Apache-2.0" + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -10527,6 +10931,31 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.24.5", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", @@ -12136,6 +12565,12 @@ "node": ">=16.x" } }, + "node_modules/immediate": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", + "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -12781,6 +13216,15 @@ "node": ">=0.10.0" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -13220,6 +13664,18 @@ "yallist": "^3.0.2" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "license": "MIT" + }, + "node_modules/lunr-languages": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/lunr-languages/-/lunr-languages-1.22.0.tgz", + "integrity": "sha512-zNog8491aJ5Fl4WGe+aiMcc851d7d5s31nuDjfR0qv5zoVFSBB8/nq/WLMP9Xrtx+UeLX7jDvKFEmus4V5aKZA==", + "license": "MPL-1.1" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -13230,6 +13686,12 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "license": "MIT" + }, "node_modules/markdown-extensions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", @@ -16356,6 +16818,18 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parse5/node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -20271,6 +20745,15 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", @@ -21131,6 +21614,40 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 9342498..969f67d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@docusaurus/faster": "3.10.2", "@docusaurus/preset-classic": "3.10.2", "@docusaurus/theme-mermaid": "^3.10.2", + "@easyops-cn/docusaurus-search-local": "0.55.3", "@fontsource/ibm-plex-mono": "^5.3.0", "@fontsource/ibm-plex-sans": "^5.3.0", "@fontsource/space-grotesk": "^5.3.0", From b51579da0719a2d95dd571c5ac8984e488f83b8f Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:44:58 -0600 Subject: [PATCH 12/37] Make the homepage personal and list every project with its badges Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- i18n/es/code.json | 46 ++++++++++----------- src/components/homepage/Hero.tsx | 49 ++++++++--------------- src/components/homepage/ProjectCard.tsx | 53 +++++++++++++++++++++---- src/components/homepage/Projects.tsx | 12 +++--- src/components/homepage/TrustBar.tsx | 40 ------------------- src/pages/index.tsx | 4 +- 6 files changed, 90 insertions(+), 114 deletions(-) delete mode 100644 src/components/homepage/TrustBar.tsx diff --git a/i18n/es/code.json b/i18n/es/code.json index 9748b46..0342b2f 100644 --- a/i18n/es/code.json +++ b/i18n/es/code.json @@ -8,38 +8,32 @@ "framework.notice.body": { "message": "Además, sigue siendo un trabajo en curso. Cada parte tiene sus propios tests, y los ejecutamos antes de cada versión, pero no podemos prometer haber cubierto todos los casos. Guarda una copia de seguridad de tu proyecto antes de añadir TsVRC o de actualizarlo, solo por si acaso. Si algo se rompe de tu lado, no podemos arreglarlo por ti, pero nos gustaría mucho saberlo. Abre un issue en GitHub y le echaremos un vistazo." }, - "homepage.hero.title": { - "message": "Herramientas para crear mundos y avatares de VRChat" + "homepage.hero.greeting": { + "message": "Hola, soy Toni" }, - "homepage.hero.subtitle": { - "message": "TsVRC crea herramientas de código abierto para creadores de VRChat, empezando con un framework para estructurar código de mundos en UdonSharp." - }, - "homepage.hero.cta.explore": { - "message": "Explorar TsVRC" - }, - "homepage.hero.cta.github": { - "message": "Ver en GitHub" - }, - "homepage.trust.starsAlt": { - "message": "Estrellas de GitHub de tsvrc/tsvrc-core" - }, - "homepage.trust.lastCommitAlt": { - "message": "Último commit de tsvrc/tsvrc-core" - }, - "homepage.trust.licenseAlt": { - "message": "Licencia de tsvrc/tsvrc-core" + "homepage.hero.intro": { + "message": "Creo herramientas para creadores de VRChat. TsVRC es el nombre con el que las publico, y la mayoría son de código abierto." }, "homepage.projects.tsvrc.description": { "message": "Una forma estructurada de crear mundos de VRChat con UdonSharp: inicio consistente, dependencias ya conectadas y código generado en vez de conectarlo todo a mano." }, - "homepage.projects.learnMore": { - "message": "Más información" - }, "homepage.projects.udonTestKit.description": { "message": "Tests automatizados para mundos de VRChat: los tests de modo Play ejecutan una sesión real de ClientSim con jugadores reales, sin los problemas del SDK que de otro modo los rompen." }, - "homepage.projects.readTheDocs": { - "message": "Leer la documentación" + "homepage.projects.docs": { + "message": "Documentación" + }, + "homepage.projects.badge.stars": { + "message": "Estrellas de GitHub de {repo}" + }, + "homepage.projects.badge.license": { + "message": "Licencia de {repo}" + }, + "homepage.projects.badge.release": { + "message": "Última versión de {repo}" + }, + "homepage.projects.badge.lastCommit": { + "message": "Último commit de {repo}" }, "homepage.projects.status.available": { "message": "Disponible" @@ -51,13 +45,13 @@ "message": "Proyectos" }, "homepage.projects.description": { - "message": "Cada proyecto de TsVRC vive en este mismo sitio, y los nuevos llegarán aquí a medida que se publiquen." + "message": "Todo lo que he construido hasta ahora. Cada proyecto tiene su documentación aquí y su código en GitHub." }, "homepage.meta.title": { "message": "TsVRC" }, "homepage.meta.description": { - "message": "TsVRC crea herramientas de código abierto para creadores de mundos y avatares de VRChat." + "message": "Herramientas de código abierto de Toni para creadores de VRChat, publicadas como TsVRC." }, "framework.hero.title": { "message": "Un framework para mundos de VRChat construido sobre UdonSharp" diff --git a/src/components/homepage/Hero.tsx b/src/components/homepage/Hero.tsx index e4a591f..91c38f1 100644 --- a/src/components/homepage/Hero.tsx +++ b/src/components/homepage/Hero.tsx @@ -1,42 +1,27 @@ -import Link from '@docusaurus/Link'; -import useBaseUrl from '@docusaurus/useBaseUrl'; import Translate from '@docusaurus/Translate'; export default function Hero() { - const logo = useBaseUrl('img/tsvrc-logo.png'); return (
-
- -

- - Tools for building VRChat worlds and avatars - -

-

- - TsVRC builds open-source tools for VRChat creators, starting with a framework for - structuring UdonSharp world code. +

+
+ +

+ Hi, I'm Toni +

+
+

+ + I build tools for VRChat creators. TsVRC is the name I publish them under, and most of + them are open source.

-
- - Explore TsVRC - - - View on GitHub - -
); diff --git a/src/components/homepage/ProjectCard.tsx b/src/components/homepage/ProjectCard.tsx index b4b3bb6..629881f 100644 --- a/src/components/homepage/ProjectCard.tsx +++ b/src/components/homepage/ProjectCard.tsx @@ -5,16 +5,45 @@ export type Project = { name: string; status: 'available' | 'planned'; description: string; - href: string; - cta: string; + docsHref: string; + // The GitHub repository, as "owner/name". + repo: string; }; +const badgeStyle = 'style=flat&color=58a6ff&labelColor=161b22'; + function statusLabel(status: Project['status']): string { return status === 'available' ? translate({ id: 'homepage.projects.status.available', message: 'Available' }) : translate({ id: 'homepage.projects.status.planned', message: 'Planned' }); } +function badges(repo: string) { + const github = `https://github.com/${repo}`; + return [ + { + src: `https://img.shields.io/github/stars/${repo}?label=stars&${badgeStyle}`, + href: `${github}/stargazers`, + alt: translate({ id: 'homepage.projects.badge.stars', message: 'GitHub stars for {repo}' }, { repo }), + }, + { + src: `https://img.shields.io/github/license/${repo}?${badgeStyle}`, + href: github, + alt: translate({ id: 'homepage.projects.badge.license', message: 'License of {repo}' }, { repo }), + }, + { + src: `https://img.shields.io/github/v/release/${repo}?label=release&${badgeStyle}`, + href: `${github}/releases`, + alt: translate({ id: 'homepage.projects.badge.release', message: 'Latest release of {repo}' }, { repo }), + }, + { + src: `https://img.shields.io/github/last-commit/${repo}?label=last%20commit&${badgeStyle}`, + href: `${github}/commits`, + alt: translate({ id: 'homepage.projects.badge.lastCommit', message: 'Last commit to {repo}' }, { repo }), + }, + ]; +} + export default function ProjectCard({ project }: { project: Project }) { return (
@@ -26,12 +55,22 @@ export default function ProjectCard({ project }: { project: Project }) {

{project.description}

+
+ {badges(project.repo).map((badge) => ( + + {badge.alt} + + ))} +
+
+
+ + {translate({ id: 'homepage.projects.docs', message: 'Docs' })} → + + + GitHub → +
- - {project.cta} → - ); } diff --git a/src/components/homepage/Projects.tsx b/src/components/homepage/Projects.tsx index a2dd74e..21fa416 100644 --- a/src/components/homepage/Projects.tsx +++ b/src/components/homepage/Projects.tsx @@ -4,7 +4,7 @@ import ProjectCard, { type Project } from '@site/src/components/homepage/Project function useProjects(): Project[] { return [ { - name: 'TsVRC', + name: 'TsVRC Core', status: 'available', description: translate({ id: 'homepage.projects.tsvrc.description', @@ -12,8 +12,8 @@ function useProjects(): Project[] { 'A structured way to build VRChat worlds with UdonSharp: consistent startup, ' + 'wired-up dependencies, and generated code instead of hand-wiring everything yourself.', }), - href: '/framework', - cta: translate({ id: 'homepage.projects.learnMore', message: 'Learn more' }), + docsHref: '/docs/tsvrc/intro', + repo: 'tsvrc/tsvrc-core', }, { name: 'Udon Test Kit', @@ -24,8 +24,8 @@ function useProjects(): Project[] { 'Automated tests for VRChat worlds: Play Mode tests run a real ClientSim session with ' + 'real players, without the SDK problems that otherwise break them.', }), - href: '/docs/udon-test-kit/intro', - cta: translate({ id: 'homepage.projects.readTheDocs', message: 'Read the docs' }), + docsHref: '/docs/udon-test-kit/intro', + repo: 'tsvrc/udon-test-kit', }, ]; } @@ -39,7 +39,7 @@ export default function Projects() {

- Every TsVRC project lives on this one site, and new ones will land here as they ship. + Everything I've built so far. Each project has its docs here and its code on GitHub.

diff --git a/src/components/homepage/TrustBar.tsx b/src/components/homepage/TrustBar.tsx deleted file mode 100644 index d651476..0000000 --- a/src/components/homepage/TrustBar.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { translate } from '@docusaurus/Translate'; - -export default function TrustBar() { - return ( -
-
- - {translate({ - - - {translate({ - - - {translate({ - -
-
- ); -} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index e701032..95e171f 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -2,7 +2,6 @@ import type { ReactNode } from 'react'; import Layout from '@theme/Layout'; import { translate } from '@docusaurus/Translate'; import Hero from '@site/src/components/homepage/Hero'; -import TrustBar from '@site/src/components/homepage/TrustBar'; import Projects from '@site/src/components/homepage/Projects'; export default function Home(): ReactNode { @@ -11,10 +10,9 @@ export default function Home(): ReactNode { title={translate({ id: 'homepage.meta.title', message: 'TsVRC' })} description={translate({ id: 'homepage.meta.description', - message: 'TsVRC builds open-source tools for VRChat world and avatar creators.', + message: "Toni's open-source tools for VRChat creators, published as TsVRC.", })}> - ); From 8142db45d0d5a0c4484ff5c2d08377999df5447b Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:46:55 -0600 Subject: [PATCH 13/37] Link the homepage avatar to Toni's GitHub Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- i18n/es/code.json | 3 +++ src/components/homepage/Hero.tsx | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/i18n/es/code.json b/i18n/es/code.json index 0342b2f..3672e96 100644 --- a/i18n/es/code.json +++ b/i18n/es/code.json @@ -11,6 +11,9 @@ "homepage.hero.greeting": { "message": "Hola, soy Toni" }, + "homepage.hero.avatarAlt": { + "message": "Toni en GitHub" + }, "homepage.hero.intro": { "message": "Creo herramientas para creadores de VRChat. TsVRC es el nombre con el que las publico, y la mayoría son de código abierto." }, diff --git a/src/components/homepage/Hero.tsx b/src/components/homepage/Hero.tsx index 91c38f1..3a528d7 100644 --- a/src/components/homepage/Hero.tsx +++ b/src/components/homepage/Hero.tsx @@ -1,17 +1,20 @@ -import Translate from '@docusaurus/Translate'; +import Link from '@docusaurus/Link'; +import Translate, { translate } from '@docusaurus/Translate'; export default function Hero() { return (
- + + {translate({ +

Hi, I'm Toni

From 1f2028a318076b77ed39b381fb59636ace63f9aa Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:47:44 -0600 Subject: [PATCH 14/37] Scope the homepage to VRChat world creation Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- i18n/es/code.json | 4 ++-- src/components/homepage/Hero.tsx | 4 ++-- src/pages/index.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/i18n/es/code.json b/i18n/es/code.json index 3672e96..479f25f 100644 --- a/i18n/es/code.json +++ b/i18n/es/code.json @@ -15,7 +15,7 @@ "message": "Toni en GitHub" }, "homepage.hero.intro": { - "message": "Creo herramientas para creadores de VRChat. TsVRC es el nombre con el que las publico, y la mayoría son de código abierto." + "message": "Creo herramientas para crear mundos de VRChat. TsVRC es el nombre con el que las publico, y la mayoría son de código abierto." }, "homepage.projects.tsvrc.description": { "message": "Una forma estructurada de crear mundos de VRChat con UdonSharp: inicio consistente, dependencias ya conectadas y código generado en vez de conectarlo todo a mano." @@ -54,7 +54,7 @@ "message": "TsVRC" }, "homepage.meta.description": { - "message": "Herramientas de código abierto de Toni para creadores de VRChat, publicadas como TsVRC." + "message": "Herramientas de código abierto de Toni para crear mundos de VRChat, publicadas como TsVRC." }, "framework.hero.title": { "message": "Un framework para mundos de VRChat construido sobre UdonSharp" diff --git a/src/components/homepage/Hero.tsx b/src/components/homepage/Hero.tsx index 3a528d7..9e0dbed 100644 --- a/src/components/homepage/Hero.tsx +++ b/src/components/homepage/Hero.tsx @@ -21,8 +21,8 @@ export default function Hero() {

- I build tools for VRChat creators. TsVRC is the name I publish them under, and most of - them are open source. + I build tools for making VRChat worlds. TsVRC is the name I publish them under, and + most of them are open source.

diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 95e171f..a7577b1 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -10,7 +10,7 @@ export default function Home(): ReactNode { title={translate({ id: 'homepage.meta.title', message: 'TsVRC' })} description={translate({ id: 'homepage.meta.description', - message: "Toni's open-source tools for VRChat creators, published as TsVRC.", + message: "Toni's open-source tools for making VRChat worlds, published as TsVRC.", })}> From a58a5a5b79470e5088342033f8968f6e59058d1e Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:49:37 -0600 Subject: [PATCH 15/37] Drop the first person from the projects intro Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- i18n/es/code.json | 2 +- src/components/homepage/Projects.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/i18n/es/code.json b/i18n/es/code.json index 479f25f..6dbc7f8 100644 --- a/i18n/es/code.json +++ b/i18n/es/code.json @@ -48,7 +48,7 @@ "message": "Proyectos" }, "homepage.projects.description": { - "message": "Todo lo que he construido hasta ahora. Cada proyecto tiene su documentación aquí y su código en GitHub." + "message": "Todo lo construido hasta ahora. Cada proyecto tiene su documentación aquí y su código en GitHub." }, "homepage.meta.title": { "message": "TsVRC" diff --git a/src/components/homepage/Projects.tsx b/src/components/homepage/Projects.tsx index 21fa416..398dba7 100644 --- a/src/components/homepage/Projects.tsx +++ b/src/components/homepage/Projects.tsx @@ -39,7 +39,7 @@ export default function Projects() {

- Everything I've built so far. Each project has its docs here and its code on GitHub. + Everything built so far. Each project has its docs here and its code on GitHub.

From 47fba46e811fa8d945e5960c091e99e151824251 Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:54:43 -0600 Subject: [PATCH 16/37] Rework the homepage project cards and drop the Framework link Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- docusaurus.config.ts | 9 -- i18n/es/code.json | 19 ++- i18n/es/docusaurus-theme-classic/footer.json | 4 - i18n/es/docusaurus-theme-classic/navbar.json | 4 - package-lock.json | 10 ++ package.json | 1 + src/components/homepage/Hero.tsx | 4 +- src/components/homepage/ProjectCard.tsx | 127 ++++++++++++------- src/components/homepage/Projects.tsx | 4 +- 9 files changed, 105 insertions(+), 77 deletions(-) diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 427f73b..0d6df5a 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -83,11 +83,6 @@ const config: Config = { src: 'img/tsvrc-logo.png', }, items: [ - { - to: '/framework', - position: 'left', - label: 'Framework', - }, { type: 'docSidebar', sidebarId: 'tutorialSidebar', @@ -113,10 +108,6 @@ const config: Config = { footer: { style: 'dark', links: [ - { - label: 'Framework', - to: '/framework', - }, { label: 'Docs', to: '/docs/tsvrc/intro', diff --git a/i18n/es/code.json b/i18n/es/code.json index 6dbc7f8..c3ccf74 100644 --- a/i18n/es/code.json +++ b/i18n/es/code.json @@ -23,20 +23,17 @@ "homepage.projects.udonTestKit.description": { "message": "Tests automatizados para mundos de VRChat: los tests de modo Play ejecutan una sesión real de ClientSim con jugadores reales, sin los problemas del SDK que de otro modo los rompen." }, - "homepage.projects.docs": { - "message": "Documentación" + "homepage.projects.github": { + "message": "{name} en GitHub" }, - "homepage.projects.badge.stars": { - "message": "Estrellas de GitHub de {repo}" + "homepage.projects.stat.stars": { + "message": "Estrellas" }, - "homepage.projects.badge.license": { - "message": "Licencia de {repo}" + "homepage.projects.stat.license": { + "message": "Licencia" }, - "homepage.projects.badge.release": { - "message": "Última versión de {repo}" - }, - "homepage.projects.badge.lastCommit": { - "message": "Último commit de {repo}" + "homepage.projects.stat.release": { + "message": "Última versión" }, "homepage.projects.status.available": { "message": "Disponible" diff --git a/i18n/es/docusaurus-theme-classic/footer.json b/i18n/es/docusaurus-theme-classic/footer.json index 4a53372..1e999f4 100644 --- a/i18n/es/docusaurus-theme-classic/footer.json +++ b/i18n/es/docusaurus-theme-classic/footer.json @@ -1,8 +1,4 @@ { - "link.item.label.Framework": { - "message": "Framework", - "description": "The label of footer link with label=Framework linking to /framework" - }, "link.item.label.Docs": { "message": "Documentación", "description": "The label of footer link with label=Docs linking to /docs/tsvrc/intro" diff --git a/i18n/es/docusaurus-theme-classic/navbar.json b/i18n/es/docusaurus-theme-classic/navbar.json index b6ae456..7eea6d3 100644 --- a/i18n/es/docusaurus-theme-classic/navbar.json +++ b/i18n/es/docusaurus-theme-classic/navbar.json @@ -7,10 +7,6 @@ "message": "Logotipo de TsVRC", "description": "The alt text of navbar logo" }, - "item.label.Framework": { - "message": "Framework", - "description": "Navbar item with label Framework" - }, "item.label.Docs": { "message": "Documentación", "description": "Navbar item with label Docs" diff --git a/package-lock.json b/package-lock.json index 3ad6120..b9d75a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@fontsource/ibm-plex-sans": "^5.3.0", "@fontsource/space-grotesk": "^5.3.0", "@mdx-js/react": "^3.0.0", + "@primer/octicons-react": "19.40.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", @@ -5654,6 +5655,15 @@ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "license": "MIT" }, + "node_modules/@primer/octicons-react": { + "version": "19.40.0", + "resolved": "https://registry.npmjs.org/@primer/octicons-react/-/octicons-react-19.40.0.tgz", + "integrity": "sha512-z1yyW2WaZxf9t+295e1Ajqj0KXWAg3ZaNk5Q7jMYYsz2e1Pzbya4bkpip+pd1ndmlaYh4kInlmCOAgyDFU+ncw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.3" + } + }, "node_modules/@rspack/binding": { "version": "1.7.12", "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.12.tgz", diff --git a/package.json b/package.json index 969f67d..c193102 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@fontsource/ibm-plex-sans": "^5.3.0", "@fontsource/space-grotesk": "^5.3.0", "@mdx-js/react": "^3.0.0", + "@primer/octicons-react": "19.40.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", diff --git a/src/components/homepage/Hero.tsx b/src/components/homepage/Hero.tsx index 9e0dbed..5a703a9 100644 --- a/src/components/homepage/Hero.tsx +++ b/src/components/homepage/Hero.tsx @@ -3,8 +3,8 @@ import Translate, { translate } from '@docusaurus/Translate'; export default function Hero() { return ( -
-
+
+
= { stars: 'stars', license: 'license', release: 'v/release' }; +const statIcons: Record = { stars: StarIcon, license: LawIcon, release: TagIcon }; + +function statLabel(kind: StatKind): string { + switch (kind) { + case 'stars': + return translate({ id: 'homepage.projects.stat.stars', message: 'Stars' }); + case 'license': + return translate({ id: 'homepage.projects.stat.license', message: 'License' }); + case 'release': + return translate({ id: 'homepage.projects.stat.release', message: 'Latest release' }); + } +} + +// Shields' JSON endpoints accept requests from any site and are cached for 30 minutes, so +// visitors don't use up GitHub's limit of 60 anonymous API requests an hour. Shields reports a +// private or missing repository, or one with no releases, as the value itself; those are left out. +function useRepoStats(repo: string): Stat[] { + const [stats, setStats] = useState([]); + useEffect(() => { + let cancelled = false; + const kinds = Object.keys(statPaths) as StatKind[]; + Promise.all( + kinds.map((kind) => + fetch(`https://img.shields.io/github/${statPaths[kind]}/${repo}.json`) + .then((response) => (response.ok ? response.json() : null)) + .then((badge) => + typeof badge?.value === 'string' && !/not found|no releases|invalid|inaccessible/i.test(badge.value) + ? { kind, value: badge.value } + : null, + ) + .catch(() => null), + ), + ).then((results) => { + if (!cancelled) { + setStats(results.filter((stat): stat is Stat => stat !== null)); + } + }); + return () => { + cancelled = true; + }; + }, [repo]); + return stats; +} function statusLabel(status: Project['status']): string { return status === 'available' @@ -18,59 +66,48 @@ function statusLabel(status: Project['status']): string { : translate({ id: 'homepage.projects.status.planned', message: 'Planned' }); } -function badges(repo: string) { - const github = `https://github.com/${repo}`; - return [ - { - src: `https://img.shields.io/github/stars/${repo}?label=stars&${badgeStyle}`, - href: `${github}/stargazers`, - alt: translate({ id: 'homepage.projects.badge.stars', message: 'GitHub stars for {repo}' }, { repo }), - }, - { - src: `https://img.shields.io/github/license/${repo}?${badgeStyle}`, - href: github, - alt: translate({ id: 'homepage.projects.badge.license', message: 'License of {repo}' }, { repo }), - }, - { - src: `https://img.shields.io/github/v/release/${repo}?label=release&${badgeStyle}`, - href: `${github}/releases`, - alt: translate({ id: 'homepage.projects.badge.release', message: 'Latest release of {repo}' }, { repo }), - }, - { - src: `https://img.shields.io/github/last-commit/${repo}?label=last%20commit&${badgeStyle}`, - href: `${github}/commits`, - alt: translate({ id: 'homepage.projects.badge.lastCommit', message: 'Last commit to {repo}' }, { repo }), - }, - ]; -} - +// The whole card links to the project's docs: the title's link stretches over the card, and the +// GitHub link sits above it. export default function ProjectCard({ project }: { project: Project }) { + const stats = useRepoStats(project.repo); return ( -
-
+
+
-

{project.name}

+

+ + {project.name} + +

{statusLabel(project.status)}
-

{project.description}

-
- {badges(project.repo).map((badge) => ( - - {badge.alt} - - ))} -
-
-
- - {translate({ id: 'homepage.projects.docs', message: 'Docs' })} → - - - GitHub → + +
+

{project.description}

+
    + {stats.map((stat) => { + const StatIcon = statIcons[stat.kind]; + return ( +
  • + + {statLabel(stat.kind)}: + {stat.value} +
  • + ); + })} +
); } diff --git a/src/components/homepage/Projects.tsx b/src/components/homepage/Projects.tsx index 398dba7..e37c5ce 100644 --- a/src/components/homepage/Projects.tsx +++ b/src/components/homepage/Projects.tsx @@ -4,7 +4,7 @@ import ProjectCard, { type Project } from '@site/src/components/homepage/Project function useProjects(): Project[] { return [ { - name: 'TsVRC Core', + name: 'Core', status: 'available', description: translate({ id: 'homepage.projects.tsvrc.description', @@ -33,7 +33,7 @@ function useProjects(): Project[] { export default function Projects() { const projects = useProjects(); return ( -
+

Projects

From 099e5ce812fbb593610c8c099a0bc1b3f3b2bffe Mon Sep 17 00:00:00 2001 From: Toni Seas <83423392+ToniSeas@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:59:01 -0600 Subject: [PATCH 17/37] Give the homepage the logo's colors Signed-off-by: Toni Seas <83423392+ToniSeas@users.noreply.github.com> --- i18n/es/code.json | 2 +- src/components/homepage/Hero.tsx | 17 ++++++++----- src/components/homepage/ProjectCard.tsx | 4 ++-- src/components/homepage/Projects.tsx | 32 +++++++++++++------------ src/css/custom.css | 27 ++++++++++++++++++++- 5 files changed, 57 insertions(+), 25 deletions(-) diff --git a/i18n/es/code.json b/i18n/es/code.json index c3ccf74..f716060 100644 --- a/i18n/es/code.json +++ b/i18n/es/code.json @@ -9,7 +9,7 @@ "message": "Además, sigue siendo un trabajo en curso. Cada parte tiene sus propios tests, y los ejecutamos antes de cada versión, pero no podemos prometer haber cubierto todos los casos. Guarda una copia de seguridad de tu proyecto antes de añadir TsVRC o de actualizarlo, solo por si acaso. Si algo se rompe de tu lado, no podemos arreglarlo por ti, pero nos gustaría mucho saberlo. Abre un issue en GitHub y le echaremos un vistazo." }, "homepage.hero.greeting": { - "message": "Hola, soy Toni" + "message": "Hola, soy {name}" }, "homepage.hero.avatarAlt": { "message": "Toni en GitHub" diff --git a/src/components/homepage/Hero.tsx b/src/components/homepage/Hero.tsx index 5a703a9..822712c 100644 --- a/src/components/homepage/Hero.tsx +++ b/src/components/homepage/Hero.tsx @@ -1,25 +1,30 @@ import Link from '@docusaurus/Link'; import Translate, { translate } from '@docusaurus/Translate'; +// The logo's tile as a band: its charcoal, its near-white, and its blue used once, on the name. export default function Hero() { return ( -
-
+
+
{translate({ -

- Hi, I'm Toni +

+ Toni }}> + {"Hi, I'm {name}"} +

-

+

I build tools for making VRChat worlds. TsVRC is the name I publish them under, and most of them are open source. diff --git a/src/components/homepage/ProjectCard.tsx b/src/components/homepage/ProjectCard.tsx index ab1f027..f49dbf2 100644 --- a/src/components/homepage/ProjectCard.tsx +++ b/src/components/homepage/ProjectCard.tsx @@ -71,7 +71,7 @@ function statusLabel(status: Project['status']): string { export default function ProjectCard({ project }: { project: Project }) { const stats = useRepoStats(project.repo); return ( -

+

@@ -96,7 +96,7 @@ export default function ProjectCard({ project }: { project: Project }) {

{project.description}

-