Skip to content

Add Playwright UI tests, test harness and CI workflow - #27

Merged
ssfinney merged 5 commits into
mainfrom
codex/scaffold-testing-architecture-for-shandoku
Mar 23, 2026
Merged

Add Playwright UI tests, test harness and CI workflow#27
ssfinney merged 5 commits into
mainfrom
codex/scaffold-testing-architecture-for-shandoku

Conversation

@ssfinney

Copy link
Copy Markdown
Owner

Motivation

  • Add end-to-end UI coverage to catch regressions in core gameplay and Rift flows with deterministic scenarios.
  • Provide a lightweight test harness inside the app to reliably set and inspect game state for Playwright tests.
  • Run UI tests automatically on push and pull requests via GitHub Actions to prevent regressions.

Description

  • Add Playwright configuration in playwright.config.js with a local webServer on port 4173, two projects (chromium and mobile-chrome), and reporters/tracing options.
  • Add multiple Playwright tests under tests/ (ui.smoke.spec.js, ui.mobile.spec.js, gameplay.placeholder.spec.js) that exercise loading, selection, entering numbers, conflicts, notes mode, resume flow, and deterministic Rift behavior.
  • Expose a test API when ?e2e=1 is present by adding TEST_MODE and window.__shandokuTest helpers in script.js, plus helper functions normalizeTestBoard and applyTestBoardState, and reduce the splash delay in test mode.
  • Add CI workflow .github/workflows/ui-tests.yml to install Node, Playwright browsers, run npm run test:ui, and upload artifacts on failure.
  • Update package.json to add @playwright/test, http-server, and scripts test:serve, test:ui, and test:ui:headed.
  • Ignore Playwright output directories by adding playwright-report/ and test-results/ to .gitignore.
  • Bump service worker cache name in sw.js to shandoku-wife-edition-v6 to reflect assets changes.

Testing

  • No automated test execution was performed as part of this commit; the CI workflow was added to run npm run test:ui on push and pull requests.
  • The repo now supports running the suite locally with npm install, npm run test:serve, and npm run test:ui (or npm run test:ui:headed).

Codex Task

@github-actions

github-actions Bot commented Mar 22, 2026

Copy link
Copy Markdown

🌸 Diff Haiku

UI tests arrive,
Harness sets the game’s clear state,
CI guards the flow.


Generated by spit-the-diff

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request adds Playwright UI tests to the Shandoku project, along with a test harness and a CI workflow. The goal is to improve the app's reliability by automatically testing core gameplay and Rift flows, and to prevent regressions by running these tests on every push and pull request. The changes include adding Playwright configuration, test files, a test API, and a CI workflow file, as well as updating the package.json and .gitignore files.

Highlights

  • Playwright UI Tests: Introduced Playwright UI tests to provide end-to-end coverage for core gameplay and Rift flows, aiming to catch regressions with deterministic scenarios.
  • Test Harness: Integrated a lightweight test harness within the app, enabling reliable setting and inspection of game state for Playwright tests.
  • CI Workflow: Established a CI workflow using GitHub Actions to automatically run UI tests on push and pull requests, preventing regressions.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/ui-tests.yml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive Playwright test suite, a test harness, and a CI workflow, which is a significant improvement for ensuring UI stability and catching regressions. The tests are well-structured, covering smoke, mobile, and specific gameplay scenarios. The test harness exposed on the window object is a practical approach for setting up deterministic states. My review includes a few suggestions to enhance code robustness and maintainability, such as using URLSearchParams for more reliable query parameter handling and centralizing common test setup logic to reduce duplication.

Comment thread script.js Outdated
const RIFT_STATUS_DELAY_REDUCED_MOTION_MS = 500;
const RIFT_EVALUATE_DEBOUNCE_MS = 140;
const RIFT_NON_CONFLICT_CHECK_INTERVAL = 5;
const TEST_MODE = window.location.search.includes('e2e=1');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using window.location.search.includes('e2e=1') can be brittle as it might lead to false positives (e.g., with a URL like ?some_other_param_e2e=1). A more robust and standard approach is to use the URLSearchParams API, which is specifically designed for parsing URL query strings.

Suggested change
const TEST_MODE = window.location.search.includes('e2e=1');
const TEST_MODE = new URLSearchParams(window.location.search).has('e2e=1');

Comment thread script.js
Comment on lines +837 to +919
function normalizeTestBoard(board, fallbackValue=0){
if(!Array.isArray(board)||board.length!==GRID_SIZE){
return Array.from({length:GRID_SIZE},()=>Array.from({length:GRID_SIZE},()=>fallbackValue));
}
return board.map(row=>{
if(!Array.isArray(row)||row.length!==GRID_SIZE) return Array.from({length:GRID_SIZE},()=>fallbackValue);
return row.map(value=>{
const n=Number(value);
if(Number.isInteger(n)&&n>=0&&n<=9) return n;
return fallbackValue;
});
});
}

function applyTestBoardState(payload={}){
const nextGrid=normalizeTestBoard(payload.grid,0);
const nextStartingGrid=normalizeTestBoard(payload.startingGrid,0);
grid=cloneGrid(nextGrid);
startingGrid=cloneGrid(nextStartingGrid);
notes=Array.from({length:GRID_SIZE},()=>Array.from({length:GRID_SIZE},()=>new Set()));
selected=(payload.selected&&Number.isInteger(payload.selected.r)&&Number.isInteger(payload.selected.c))
?{r:Math.max(0,Math.min(8,payload.selected.r)),c:Math.max(0,Math.min(8,payload.selected.c))}
:null;
elapsed=normalizeElapsed(payload.elapsed);
notesMode=!!payload.notesMode;
autoCleanup=payload.autoCleanup!==false;
history=[]; future=[];
boardShellEl.classList.remove('victory-glow');
clearRiftVisualState();
riftState={active:false,sequenceRunning:false,nodes:[],hasTriggered:false,cooldownUntil:0,copyKey:'pattern'};
movesSinceSolvabilityCheck=0;
captureLastSolvableSnapshot();
render();
saveGame();
}

function exposeTestApi(){
window.__shandokuTest={
resetStorage(){
localStorage.removeItem(STORAGE_KEY);
},
setBoardState(payload){
applyTestBoardState(payload);
},
getState(){
return {
grid:cloneGrid(grid),
startingGrid:cloneGrid(startingGrid),
selected:selected?{...selected}:null,
notesMode,
autoCleanup,
errorCount:countErrors(),
status:statusEl.textContent
};
},
selectCell(r,c){
selected={r:Math.max(0,Math.min(8,r)),c:Math.max(0,Math.min(8,c))};
render();
saveGame();
},
placeNumber(n){
placeNumber(n);
},
newGame(){
newGame();
},
forceRift(payload={}){
const r=Number.isInteger(payload.r)?Math.max(0,Math.min(8,payload.r)):4;
const c=Number.isInteger(payload.c)?Math.max(0,Math.min(8,payload.c)):4;
clearRiftVisualState();
riftState.active=true;
riftState.sequenceRunning=false;
riftState.nodes=[{r,c}];
riftState.hasTriggered=true;
riftState.cooldownUntil=Date.now()+RIFT_COOLDOWN_MS;
boardShellEl.classList.add('rift-active');
statusEl.classList.add('rift-status');
render();
setStatus('Rift node found. Tap the marked cell.');
saveGame();
}
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These helper functions are intended only for testing but are included in the main application script. While they are guarded by the TEST_MODE flag, they still add to the production bundle size and increase the application's potential attack surface. For larger applications, it's a best practice to use a build process (like tree-shaking with environment variables) to completely remove such test-specific code from production builds.

Comment thread tests/ui.smoke.spec.js Outdated
Comment on lines +3 to +10
async function openFreshGame(page) {
await page.addInitScript(() => {
localStorage.clear();
sessionStorage.clear();
});
await page.goto('/?e2e=1');
await expect(page.locator('#board .cell')).toHaveCount(81);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This openFreshGame helper function is very useful. Similar setup logic (clearing storage, navigating to the test page) is duplicated in tests/gameplay.placeholder.spec.js and tests/ui.mobile.spec.js. To improve maintainability and reduce code duplication, consider extracting this helper into a shared utility file (e.g., tests/test-utils.js) and importing it where needed across your test suites.

@ssfinney

Copy link
Copy Markdown
Owner Author

@codex review and implement Gemini's code review suggestions

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e1ba80a921

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tests/ui.smoke.spec.js Outdated
Comment on lines +4 to +7
await page.addInitScript(() => {
localStorage.clear();
sessionStorage.clear();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove persistent init script from fresh-game helper

openFreshGame uses page.addInitScript to clear storage, but Playwright runs init scripts before every navigation in that page, including page.reload(). In the "saved game can be resumed" test, the reload therefore clears localStorage right before app boot, so the resume modal cannot appear and the test fails for the wrong reason.

Useful? React with 👍 / 👎.

@ssfinney
ssfinney merged commit 86e8a5b into main Mar 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant