Skip to content
Open
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
3 changes: 3 additions & 0 deletions e2e/constants/timeouts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,7 @@ export const Timeouts = {

/** Network request to fetch language JSON data */
dataFetch: 10_000,

/** Deliberate pause after Level End loads, before clicking Replay */
replaySettleDelay: 2_000,
} as const;
13 changes: 8 additions & 5 deletions e2e/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Where to find test case documentation

The test cases covered here correspond to the **FeedTheMonsterJS QA Test Case Document** (FTM_TC_001 – FTM_TC_0016).
The test cases covered here correspond to the **FeedTheMonsterJS QA Test Case Document** (FTM_TC_001 – FTM_TC_0017).
Refer to that document for:
- Detailed step-by-step instructions
- Expected outcomes per step
Expand All @@ -17,7 +17,7 @@ Each test uses the `FTM_TC_XXX` prefix in its title to match the document's TC n

**[ftm-assessment-survey-flow.spec.ts](ftm-assessment-survey-flow.spec.ts)** — the default run target.

All 16 TCs live in one `test.describe.serial` block. The browser opens **once**, runs every test case in order (TC_001 → TC_0016), and closes once at the end. No navigation restarts between test cases — each test picks up exactly where the previous left off.
All 17 TCs live in one `test.describe.serial` block. The browser opens **once**, runs every test case in order (TC_001 → TC_0017), and closes once at the end. No navigation restarts between test cases — each test picks up exactly where the previous left off.

```bash
npm run test:e2e # runs ftm-assessment-survey-flow.spec.ts (headed off)
Expand All @@ -29,7 +29,9 @@ npm run test:e2e:ui # Playwright UI — inspect each step

## Isolated spec files (`isolated/`)

The `isolated/` subfolder contains the same 16 TCs split by feature area. Each file is **self-contained** — it navigates from scratch to its required starting state — so you can run one file on its own when debugging a failure without re-running the full suite.
The `isolated/` subfolder contains the same 17 TCs split by feature area. Each file is **self-contained** — it navigates from scratch to its required starting state — so you can run one file on its own when debugging a failure without re-running the full suite.

`tc-017-level-replay.spec.ts` is the one exception: it continues directly from the Level End screen `tc-016-level-completion.spec.ts` reaches (which deliberately does not click Map), so it is not independently runnable on its own without TC_016 having already run in the same `getPage()` session.

Files use 3-digit zero-padded prefixes so they sort into TC flow order alphabetically.
Audio is **not** mocked in isolated files — real audio plays (same as the primary flow file).
Expand All @@ -42,12 +44,13 @@ Audio is **not** mocked in isolated files — real audio plays (same as the prim
| [isolated/tc-006-008-gameplay.spec.ts](isolated/tc-006-008-gameplay.spec.ts) | TC_006–TC_008 | Gameplay UI; stones appear on canvas; drag-and-drop |
| [isolated/tc-009-013-assessment.spec.ts](isolated/tc-009-013-assessment.spec.ts) | TC_009–TC_013 | Assessment overlay; correct drag; green feedback; wrong drag |
| [isolated/tc-014-015-mini-game.spec.ts](isolated/tc-014-015-mini-game.spec.ts) | TC_0014–TC_0015 | Treasure chest canvas visible; click 5 stones; mini game completes |
| [isolated/tc-016-level-completion.spec.ts](isolated/tc-016-level-completion.spec.ts) | TC_0016 | Jar fill animation; level end screen; map/next buttons |
| [isolated/tc-016-level-completion.spec.ts](isolated/tc-016-level-completion.spec.ts) | TC_0016 | Jar fill animation; level end screen; map/next buttons visible (stays on Level End for TC_017) |
| [isolated/tc-017-level-replay.spec.ts](isolated/tc-017-level-replay.spec.ts) | TC_0017 | Replay button restarts the same level with fresh, interactive puzzle state |

These files are excluded from `npm run test:e2e` via `testIgnore: ['**/isolated/**']` in `playwright.config.ts`.

```bash
# Run the full isolated suite (all 7 files, sequential)
# Run the full isolated suite (all 8 files, sequential)
npm run test:e2e:isolated

# Run one specific file
Expand Down
22 changes: 16 additions & 6 deletions e2e/tests/ftm-assessment-survey-flow.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* FeedTheMonsterJS – Full E2E Flow (TC_001 – TC_016)
* FeedTheMonsterJS – Full E2E Flow (TC_001 – TC_017)
*
* Orchestrator: runs all test cases as one serial worker in a single shared
* browser session. Test logic lives exclusively in the files under isolated/.
Expand All @@ -11,7 +11,8 @@
* Natural assessment trigger (TC_011) → Assessment completion (TC_012) →
* Mini-game (TC_014–TC_015) →
* Remaining post-mini-game puzzles (TC_013) →
* Natural level completion (TC_016)
* Natural level completion (TC_016) →
* Level replay (TC_017)
*
* Key design decisions:
* • TC_008 completes puzzle 1 and holds a 2 s stability pause.
Expand All @@ -25,7 +26,10 @@
* transition (mini-game starts automatically).
* • TC_013 is registered AFTER TC_014–TC_015 so it runs after the mini-game
* completes; it finishes the remaining puzzles so TC_016 appears naturally.
* • TC_016 waits for the natural level end — no manual event publishing.
* • TC_016 waits for the natural level end — no manual event publishing — and
* deliberately does NOT click Map, so the Level End screen stays loaded for TC_017.
* • TC_017 clicks Replay on that same Level End screen and verifies the same
* level reloads into a fresh, interactive gameplay state.
*
* To debug a specific area in isolation, run the corresponding file directly
* from e2e/tests/isolated/.
Expand All @@ -43,8 +47,9 @@ import { registerTests as tc006_008 } from './isolated/tc-006-008-gameplay.spec'
import { registerTC009_012, registerTC013 } from './isolated/tc-009-013-assessment.spec';
import { registerTests as tc014_015 } from './isolated/tc-014-015-mini-game.spec';
import { registerTests as tc016 } from './isolated/tc-016-level-completion.spec';
import { registerTests as tc017 } from './isolated/tc-017-level-replay.spec';

test.describe.serial('FeedTheMonsterJS – Full E2E Flow (TC_001 – TC_016)', () => {
test.describe.serial('FeedTheMonsterJS – Full E2E Flow (TC_001 – TC_017)', () => {
test.describe.configure({ retries: 0 });

let page: Page;
Expand Down Expand Up @@ -96,8 +101,13 @@ test.describe.serial('FeedTheMonsterJS – Full E2E Flow (TC_001 – TC_016)', (
// game reaches the natural level-end flow.
registerTC013(() => page, fullState);

// ── Natural level completion (TC_016, last step) ───────────────────────────
// ── Natural level completion (TC_016) ───────────────────────────────────────
// Waits for the level-end screen to appear naturally (progress jar → LevelEnd).
// Checks stars, buttons, and navigates back to level selection.
// Checks stars and buttons; stays on the Level End screen for TC_017.
tc016(() => page);

// ── Level replay (TC_017, last step) ────────────────────────────────────────
// Clicks Replay on the Level End screen TC_016 reached, and verifies the same
// level reloads into a fresh, interactive gameplay state.
tc017(() => page);
});
10 changes: 2 additions & 8 deletions e2e/tests/isolated/tc-016-level-completion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* → progress jar animation → LevelEnd scene.
*
* This test does NOT publish any events — it simply waits for the natural transition.
* It deliberately does NOT click Map, so the Level End screen remains loaded for
* FTM_TC_017 (Replay), which runs immediately after this test in the orchestrator.
*
* Run via the orchestrator: e2e/tests/ftm-assessment-survey-flow.spec.ts
*/
Expand All @@ -15,7 +17,6 @@ import { test, expect } from '../../fixtures/game-fixtures';
import type { Page } from '@playwright/test';
import { Selectors } from '../../constants/selectors';
import { Timeouts } from '../../constants/timeouts';
import { LevelSelectionPage } from '../../pages/level-selection-page';
import { LevelEndPage } from '../../pages/level-end-page';

export function registerTests(getPage: () => Page): void {
Expand Down Expand Up @@ -71,12 +72,5 @@ export function registerTests(getPage: () => Page): void {
timeout: Timeouts.domUpdate,
});
});

await test.step('Clicking map button returns user to level selection', async () => {
await page.locator(LevelEndPage.SELECTORS.mapButton).click();
await expect(page.locator(LevelSelectionPage.SELECTOR)).toBeVisible({
timeout: Timeouts.sceneTransition,
});
});
});
}
74 changes: 74 additions & 0 deletions e2e/tests/isolated/tc-017-level-replay.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* FTM_TC_017 | Level Replay
*
* Continues directly from FTM_TC_016 (tc-016-level-completion.spec.ts), which
* deliberately stays on the Level End screen instead of clicking Map. Clicks
* Replay (#levelend-retry-btn) and verifies the same level reloads into a
* fresh, interactive gameplay state.
*
* Precondition: the orchestrator plays Level 2 (0-based currentLevel === 1),
* which always renders the Replay button regardless of star count
* (levelend-scene.ts renderButtonsHTML — Replay is hidden only for a passed
* Level 1 / currentLevel === 0).
*
* Run via the orchestrator: e2e/tests/ftm-assessment-survey-flow.spec.ts
*/

import { test, expect } from '../../fixtures/game-fixtures';
import type { Page } from '@playwright/test';
import { Timeouts } from '../../constants/timeouts';
import { LevelEndPage } from '../../pages/level-end-page';
import { GameplayPage } from '../../pages/gameplay-page';
import { assertCanvasHasContent } from '../../helpers/canvas-helpers';
import { getHitboxCenter, waitForStonesReady } from '../../helpers';

export function registerTests(getPage: () => Page): void {
test('FTM_TC_017 | Level Replay | Replay button restarts the same level with fresh, interactive puzzle state', async () => {
const page = getPage();
const levelEndPage = new LevelEndPage(page);
const gameplayPage = new GameplayPage(page);

await test.step('Level end screen from TC_016 is still loaded', async () => {
await levelEndPage.assertLevelEndVisible();
});

await test.step('Wait for UI to settle before interacting with Replay', async () => {
await page.waitForTimeout(Timeouts.replaySettleDelay);
});

await test.step('Click Replay button', async () => {
await levelEndPage.clickRetryButton();
});

await test.step('Gameplay scene reloads after Replay', async () => {
await gameplayPage.waitForGameplayScene();
});

await test.step('Click the monster hotspot to trigger stones on the fresh puzzle', async () => {
await page.waitForFunction(
() => (window as any).__ftm?.gameStateService?.getHitBoxRanges?.() != null,
{ timeout: Timeouts.sceneTransition },
);
const hitboxCenter = await getHitboxCenter(page);
expect(hitboxCenter).not.toBeNull();
const canvasBB = await gameplayPage.mainCanvas.boundingBox();
expect(canvasBB).not.toBeNull();
await page.mouse.click(
canvasBB!.x + hitboxCenter!.x,
canvasBB!.y + hitboxCenter!.y,
);
});
Comment on lines +35 to +60

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

gameplay_files="$(fd -i -t f 'gameplay-page\.ts$' e2e || true)"
while IFS= read -r file; do
  [ -z "$file" ] && continue
  rg -n -A30 -B5 '\bwaitForGameplayScene\s*\(' "$file" || true
done <<< "$gameplay_files"

rg -n -A25 -B10 'levelend|clickRetryButton|retry-btn|Replay' e2e/pages e2e/helpers || true

Repository: curiouslearning/FeedTheMonsterJS

Length of output: 7158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

test_file="e2e/tests/isolated/tc-017-level-replay.spec.ts"
if [ -f "${test_file}" ]; then
  sed -n '1,140p' "${test_file}" | cat -n
else
  echo "test file not found"
fi

playground_files="$(fd -i -t f 'playground|gameplay|level-end' e2e/src-e2e e2e pages 2>/dev/null || true)"
echo "PLAYGROUND_FILES=${playground_files}"

for file in $playground_files; do
  [ -z "$file" ] && continue
  base="$(basename "$file")"
  echo "--- ${file} (size $(wc -l < "$file")) ---"
  rg -n -A8 -B8 'getHitBoxRanges|hitBoxRanges|playground|canvas|levelEnd|gameStateService|Scene|setLevel|loadLevel|replay' "$file" || true
done

rg -n -A12 -B8 'getHitBoxRanges|hitBoxRanges|gameStateService' e2e || true

Repository: curiouslearning/FeedTheMonsterJS

Length of output: 50388


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- GameplayPage implementation ---"
sed -n '1,110p' e2e/pages/gameplay-page.ts | cat -n

echo "--- LevelEndPage retry implementation ---"
sed -n '80,115p' e2e/pages/level-end-page.ts | cat -n

echo "--- Reusable wait helpers around playable/gameplay state ---"
rg -n -A20 -B8 'playable|canPlay|isPlayable|isLevelPlayable|gamePlay.*loaded|gamePlay.*ready|loadLevel|reload|retry|GamePlay|gamePlayService|getHitBoxRanges|sceneHandler|currentScene' e2e e2e/src-e2e --glob '!e2e/tests/isolated/*' | head -n 240 || true

echo "--- Candidate source files containing retry/loadState/startPlayback ---"
rg -n 'retry|retryBtn|retry.*click|set.*Play|startPlayback|loadLevel|reset|reload|gameStateService.getHitBoxRanges|currentScene|activeScene' e2e/src-e2e e2e/pages e2e/includes --glob '!**/*.spec.ts' --glob '!**/*.test.ts' | head -n 240 || true

echo "--- Search for waitForStonesReady / getHitboxCenter wrappers beyond direct lines ---"
rg -n -A30 -B10 'waitForStonesReady|getHitboxCenter|gameStateService.*hitBoxRanges|hitBoxRanges' e2e e2e/src-e2e --glob '!e2e/tests/isolated/*' | head -n 260 || true

# Behavioral probe: model the current post-replay conditions and show stale-state risk.
node - <<'JS'
const conditions = {
  timeoutIsOnlyElapsed: true,
  waitForGameplaySceneOnlyChecks: ['`#canvas` visible', '`#pause-button` visible'],
  replayButtonSelectorUsed: '`#levelend-retry-btn`',
  hitboxPrecondition: 'window.__ftm?.gameStateService?.getHitBoxRanges() != null',
};

const staleScenario = {
  oldLevelEndStillOverlaying: true,
  oldGameplaySceneStillActive: true,
  uiButtonsVisibleButOverlayBlocked: true,
  gameHitBoxRangesAvailable: true,
};

console.log(
  JSON.stringify(
    {
      conditions,
      staleScenario,
      currentTestWillWaitFor: [...conditions.waitForGameplaySceneOnlyChecks, conditions.hitboxPrecondition],
      missingPostReplayConditions: [
        '`#levelEnd` hidden / inactive',
        'active gameplay scene not same as overlay before retry',
        'gamePlayData.puzzles reset or currentPuzzleIndex reset',
        'stoneHandler.stonesHasLoaded reset',
      ],
    },
    null,
    2
  )
);
JS

Repository: curiouslearning/FeedTheMonsterJS

Length of output: 30316


Wait for the replay scene to clear before reading game state.

waitForGameplayScene() only checks that #canvas and #pause-button are visible, while getHitboxCenter() reads whatever __ftm.gameStateService.getHitBoxRanges() returns. If #levelEnd is still active after Replay, this step can click the stale Level End hitbox. Add the level-end deactivation check before reading hitbox state, or update waitForGameplayScene() to enforce it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/tests/isolated/tc-017-level-replay.spec.ts` around lines 35 - 60, Ensure
the Replay flow waits for the level-end overlay to deactivate before reading
hitbox state or clicking the fresh puzzle. Update the “Gameplay scene reloads
after Replay” step or the gameplay scene wait helper used by
waitForGameplayScene() to verify `#levelEnd` is inactive, while preserving the
existing canvas and pause-button readiness checks.


await test.step('Gameplay canvas re-renders with interactive content', async () => {
await waitForStonesReady(page);
await assertCanvasHasContent(page, GameplayPage.SELECTORS.mainCanvas);
});

await test.step('Same level (Level 2) context is maintained after replay', async () => {
const restartedLevel = await page.evaluate(
() => (window as any).__ftm?.gameStateService?.gamePlayData?.selectedLevelNumber,
);
expect(restartedLevel).toBe(1); // 0-based → "Level 2"
});
});
}
Loading