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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions .github/workflows/main-branch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,14 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Configure GitHub Pages
uses: actions/configure-pages@v5
- name: Prepare static site bundle
run: |
rm -rf dist
mkdir -p dist
cp index.html dist/
cp landing.js dist/
cp style.css dist/
cp -R AI dist/AI
cp -R tests dist/tests
cp ai-instruct.txt dist/
run: python build.py --output dist
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
Expand Down Expand Up @@ -55,6 +51,8 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Build static site bundle
run: python build.py --output dist
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Build static site bundle
run: python build.py --output dist
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
81 changes: 81 additions & 0 deletions build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Build the Talk to Unity static site bundle.

This script prepares the distributable assets by copying the landing
experience, the AI application, and supporting files into a target
output directory. The workflow jobs call this script to ensure both
pages are included before running tests or publishing the bundle.
"""

from __future__ import annotations

import argparse
import shutil
from pathlib import Path

ROOT = Path(__file__).resolve().parent

FILES_TO_COPY = [
"index.html",
"landing.js",
"style.css",
"ai-instruct.txt",
]

DIRECTORIES_TO_COPY = [
"AI",
"tests",
]


def build(output_dir: str | Path = "dist") -> Path:
"""Build the static site bundle into ``output_dir``.

Args:
output_dir: Destination directory for the build. Relative paths
are resolved from the repository root.

Returns:
The absolute :class:`Path` to the created output directory.
"""

destination = Path(output_dir)
if not destination.is_absolute():
destination = ROOT / destination

if destination.exists():
shutil.rmtree(destination)
destination.mkdir(parents=True)

for file_name in FILES_TO_COPY:
source = ROOT / file_name
if not source.exists():
raise FileNotFoundError(f"Missing required file: {source}")
shutil.copy2(source, destination / source.name)

for directory in DIRECTORIES_TO_COPY:
source_dir = ROOT / directory
if not source_dir.exists():
raise FileNotFoundError(f"Missing required directory: {source_dir}")
shutil.copytree(source_dir, destination / directory)

return destination


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Build the Talk to Unity static site bundle.")
parser.add_argument(
"--output",
default="dist",
help="Destination directory for build artifacts (default: dist)",
)
return parser.parse_args()


def main() -> None:
args = parse_args()
output_path = build(args.output)
print(f"Built Talk to Unity site in {output_path}")


if __name__ == "__main__":
main()
24 changes: 24 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from __future__ import annotations

import sys
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))

import build


@pytest.fixture(scope="session")
def built_site(tmp_path_factory: pytest.TempPathFactory) -> Path:
output_dir = tmp_path_factory.mktemp("site")
build.build(output_dir)
return output_dir


@pytest.fixture(scope="session")
def app_js(built_site: Path) -> str:
return (built_site / "AI" / "app.js").read_text(encoding="utf-8")
24 changes: 10 additions & 14 deletions tests/test_auto_launch.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
from pathlib import Path
import re

ROOT = Path(__file__).resolve().parents[1]
APP_JS = (ROOT / "AI" / "app.js").read_text()

def test_auto_start_without_landing_section(app_js: str) -> None:
assert "const shouldAutoStartExperience = !launchButton && !landingSection;" in app_js
assert "if (shouldAutoStartExperience)" in app_js
assert "void startApplication();" in app_js

def test_auto_start_without_landing_section():
assert "const shouldAutoStartExperience = !launchButton && !landingSection;" in APP_JS
assert "if (shouldAutoStartExperience)" in APP_JS
assert "void startApplication();" in APP_JS


def test_app_js_defines_expected_functions():
function_names = set(re.findall(r"function[ \t]+(\w+)", APP_JS))
def test_app_js_defines_expected_functions(app_js: str) -> None:
function_names = set(re.findall(r"function[ \t]+(\w+)", app_js))
expected = {
"formatDependencyList",
"setStatusMessage",
Expand Down Expand Up @@ -65,16 +61,16 @@ def test_app_js_defines_expected_functions():
assert not missing, f"Missing functions from app.js: {missing}"


def test_start_application_sets_app_state():
match = re.search(r"async function startApplication\([^)]*\)\s*{(.*?)}\s*async function", APP_JS, re.S)
def test_start_application_sets_app_state(app_js: str) -> None:
match = re.search(r"async function startApplication\([^)]*\)\s*{(.*?)}\s*async function", app_js, re.S)
assert match, "startApplication definition not found"
body = match.group(1)
assert "appStarted = true;" in body or "appStarted = !0" in body
assert "bodyElement.dataset.appState = 'experience';" in body


def test_set_muted_state_announces_changes():
match = re.search(r"async function setMutedState\([^)]*\)\s*{(.*?)}\s*async function", APP_JS, re.S)
def test_set_muted_state_announces_changes(app_js: str) -> None:
match = re.search(r"async function setMutedState\([^)]*\)\s*{(.*?)}\s*async function", app_js, re.S)
assert match, "setMutedState definition not found"
body = match.group(1)
assert "isMuted = muted;" in body or "isMuted = true;" in body
Expand Down
30 changes: 30 additions & 0 deletions tests/test_build_artifacts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from __future__ import annotations

from pathlib import Path


def test_landing_assets_are_copied(built_site: Path) -> None:
for asset in ("index.html", "landing.js", "style.css", "ai-instruct.txt"):
asset_path = built_site / asset
assert asset_path.exists(), f"Missing landing asset in build output: {asset_path}"


def test_ai_application_assets_are_copied(built_site: Path) -> None:
ai_dir = built_site / "AI"
assert (ai_dir / "index.html").exists(), "AI index.html missing from build output"
assert (ai_dir / "app.js").exists(), "AI app.js missing from build output"
assert (ai_dir / "ai.css").exists(), "AI stylesheet missing from build output"


def test_ai_application_page_includes_new_redirect(built_site: Path) -> None:
ai_index = (built_site / "AI" / "index.html").read_text(encoding="utf-8")
assert "checks-passed=true" in ai_index
assert "Unity Voice Lab" in ai_index
assert '<script defer src="./app.js"></script>' in ai_index


def test_ai_styles_reflect_refined_layout(built_site: Path) -> None:
ai_css = (built_site / "AI" / "ai.css").read_text(encoding="utf-8")
assert "body[data-app-state='experience'] .voice-stage" in ai_css
assert "border-radius" in ai_css
assert "compatibility-notice" in ai_css