diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 0000000..d8ed752 --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,134 @@ +name: Continuous Integration + +on: + pull_request: + push: + branches: + - main + +jobs: + pr-tests: + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + name: Run Tests + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Ensure test scripts are executable + run: | + if compgen -G "test/*.sh" > /dev/null; then + chmod +x test/*.sh + fi + + - name: Execute pull request tests + run: | + set -euo pipefail + if compgen -G "test/*.sh" > /dev/null; then + for test_script in test/*.sh; do + echo "Running ${test_script}" + bash "${test_script}" + done + else + echo "No tests found in ./test" + fi + + pr-report: + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + name: Report Tests Statuses + needs: + - pr-tests + steps: + - name: Summarize pull request test results + run: echo "Pull request tests completed successfully." + + main-build: + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + name: Build and Upload Artifacts + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build static site + run: | + set -euo pipefail + mkdir -p build + cp index.html build/ + cp style.css build/ + cp landing.js build/ + + - name: Upload static site artifact + uses: actions/upload-artifact@v4 + with: + name: static-site + path: build + + main-report-build: + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + name: Report Build Status + needs: + - main-build + steps: + - name: Report build outcome + run: echo "Static site build completed successfully." + + main-tests: + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + name: Run Tests + needs: + - main-build + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Ensure test scripts are executable + run: | + if compgen -G "tests/*.sh" > /dev/null; then + chmod +x tests/*.sh + fi + + - name: Execute main branch tests + run: | + set -euo pipefail + if compgen -G "tests/*.sh" > /dev/null; then + for test_script in tests/*.sh; do + echo "Running ${test_script}" + bash "${test_script}" + done + else + echo "No tests found in ./tests" + fi + + main-report-tests: + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + name: Report Tests Statuses + needs: + - main-tests + steps: + - name: Summarize main branch test results + run: echo "Main branch tests completed successfully." + + deploy-pages: + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + name: Deploy to Pages + needs: + - main-build + steps: + - name: Deploy placeholder + run: echo "Deploying static site to GitHub Pages (placeholder)." diff --git a/README.md b/README.md index 221e4f9..77dc585 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Talk to Unity in Plain English - - +[](https://github.com/Unity-Lab-AI/Talk/actions/workflows/continuous-integration.yml) +[](https://github.com/Unity-Lab-AI/Talk/actions/workflows/continuous-integration.yml) Talk to Unity is a single web page that acts like a friendly concierge. The landing screen double-checks that your browser has everything it needs (secure connection, microphone, speech tools). Once every light turns green, a voice assistant named **Unity** wakes up so you can talk out loud and hear it answer back. diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/site_structure_test.sh b/test/site_structure_test.sh new file mode 100755 index 0000000..64751d9 --- /dev/null +++ b/test/site_structure_test.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "Running Talk to Unity smoke tests for pull requests..." +python -m unittest discover -s test -p 'test_*.py' -v diff --git a/test/test_landing_page_metadata.py b/test/test_landing_page_metadata.py new file mode 100644 index 0000000..6ebb7cc --- /dev/null +++ b/test/test_landing_page_metadata.py @@ -0,0 +1,114 @@ +"""Smoke tests for validating the landing page metadata used in pull requests.""" + +from __future__ import annotations + +from html.parser import HTMLParser +from pathlib import Path +import unittest + + +class _HeadStructureParser(HTMLParser): + """Minimal HTML parser that records metadata inside the document head.""" + + def __init__(self) -> None: + super().__init__() + self._in_head = False + self._in_title = False + self._in_noscript = False + self._current_title: list[str] = [] + self.titles: list[str] = [] + self.meta_tags: list[dict[str, str]] = [] + self.scripts: list[dict[str, str]] = [] + self.noscript_styles: list[dict[str, str]] = [] + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + attr_map = {name: value or "" for name, value in attrs} + if tag == "head": + self._in_head = True + elif tag == "title" and self._in_head: + self._in_title = True + self._current_title.clear() + + if self._in_head: + if tag == "meta": + self.meta_tags.append(attr_map) + elif tag == "script": + self.scripts.append(attr_map) + elif tag == "noscript": + self._in_noscript = True + + if self._in_noscript and tag == "link" and attr_map.get("rel") == "stylesheet": + self.noscript_styles.append(attr_map) + + def handle_endtag(self, tag: str) -> None: + if tag == "head": + self._in_head = False + elif tag == "title" and self._in_title: + title = "".join(self._current_title).strip() + if title: + self.titles.append(title) + self._in_title = False + elif tag == "noscript" and self._in_noscript: + self._in_noscript = False + + def handle_data(self, data: str) -> None: + if self._in_title: + self._current_title.append(data) + + +class LandingPageHeadTests(unittest.TestCase): + """Validates the metadata embedded in ``index.html``.""" + + @classmethod + def setUpClass(cls) -> None: # noqa: D401 - required by unittest + """Load and parse the landing page once for the entire suite.""" + + cls.index_html = Path("index.html").read_text(encoding="utf-8") + parser = _HeadStructureParser() + parser.feed(cls.index_html) + cls.parser = parser + + def test_document_title_mentions_unity_voice_lab(self) -> None: + """The page title should advertise the Unity Voice Lab system check.""" + + self.assertGreater(len(self.parser.titles), 0, "No