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
81 changes: 70 additions & 11 deletions app/components/form.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import "@testing-library/jest-dom/vitest"
import { render, screen } from "@mcansh/remix-testing-library"
import { describe, it, expect, vi } from "vitest"
import { fireEvent, render, screen } from "@mcansh/remix-testing-library"
import { fromPartial } from "@total-typescript/shoehorn"
import type { Handle } from "remix/component"
import { afterEach, beforeEach, describe, it, expect, vi } from "vitest"
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find direct imports from vite/vitest in TS/TSX files.
# Expectation: no direct imports from "vite" or "vitest"; test APIs should come from "vite-plus/test".
rg -nP 'from\s+["'\''](?:vite|vitest)(?:/[^"'\'']*)?["'\'']' --type ts --type tsx -C2

Repository: mcansh/remix-wordle

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Read the form.test.tsx file to check the actual imports
head -20 app/components/form.test.tsx

Repository: mcansh/remix-wordle

Length of output: 479


🏁 Script executed:

#!/bin/bash
# Check if vite-plus is in package.json and what test dependencies are listed
cat package.json | head -50

Repository: mcansh/remix-wordle

Length of output: 1466


🏁 Script executed:

#!/bin/bash
# Check more of devDependencies and look for vite-plus reference
cat package.json | tail -30

Repository: mcansh/remix-wordle

Length of output: 1205


🏁 Script executed:

#!/bin/bash
# Find other test files and check their imports
find . -name "*.test.ts" -o -name "*.test.tsx" | head -10

Repository: mcansh/remix-wordle

Length of output: 408


🏁 Script executed:

#!/bin/bash
# Check imports in several test files to see the pattern
for file in ./app/components/keyboard.test.tsx ./app/utils/auth-session.test.ts ./app/middleware/auth.test.ts; do
  echo "=== $file ==="
  head -5 "$file"
  echo ""
done

Repository: mcansh/remix-wordle

Length of output: 707


🏁 Script executed:

#!/bin/bash
# Check all test files to see which import pattern each uses
echo "Files importing from 'vitest':"
grep -l "from [\"']vitest[\"']" ./app/**/*.test.* 2>/dev/null | sort

echo ""
echo "Files importing from 'vite-plus/test':"
grep -l "from [\"']vite-plus/test[\"']" ./app/**/*.test.* 2>/dev/null | sort

Repository: mcansh/remix-wordle

Length of output: 463


🏁 Script executed:

#!/bin/bash
# Check letter-input.test.tsx to see which import it uses (appeared in both lists)
head -10 ./app/components/letter-input.test.tsx

Repository: mcansh/remix-wordle

Length of output: 392


Import test APIs from vite-plus/test instead of vitest.

Line 3 imports directly from vitest, but per project guidelines, test APIs should be imported from the vite-plus wrapper.

Proposed fix
-import { afterEach, beforeEach, describe, it, expect, vi } from "vitest"
+import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { afterEach, beforeEach, describe, it, expect, vi } from "vitest"
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/form.test.tsx` at line 3, The test file imports test helpers
directly from "vitest"; update the import statement used in this file (the named
imports afterEach, beforeEach, describe, it, expect, vi) to come from
"vite-plus/test" instead of "vitest" so the project uses the vite-plus wrapper
for test APIs; locate the import line that currently reads import { afterEach,
beforeEach, describe, it, expect, vi } from "vitest" and change the module
specifier to "vite-plus/test".


import { GuessForm } from "./form"

Expand All @@ -15,16 +17,32 @@ vi.mock("../routes", () => ({
}))

describe("GuessForm", () => {
let handle: Handle

beforeEach(() => {
handle = fromPartial<Handle>({
update: vi.fn(async () => {}),
})
})

beforeEach(() => {
window.sessionStorage.clear()
})

afterEach(() => {
vi.useRealTimers()
})

it.skip("renders 5 letter inputs", () => {
let Component = GuessForm()
let Component = GuessForm(handle)
render(Component({ currentGuess: 0 }))

let inputs = screen.getAllByRole("textbox")
expect(inputs).toHaveLength(5)
})

it("renders inputs with correct labels", () => {
let Component = GuessForm()
let Component = GuessForm(handle)
render(Component({ currentGuess: 0 }))

for (let i = 1; i <= 5; i++) {
Expand All @@ -33,7 +51,7 @@ describe("GuessForm", () => {
})

it("renders form with correct attributes", () => {
let Component = GuessForm()
let Component = GuessForm(handle)
let { container } = render(Component({ currentGuess: 0 }))

let form = container.querySelector("form")
Expand All @@ -44,7 +62,7 @@ describe("GuessForm", () => {
})

it("renders cheat input when cheat prop is true", () => {
let Component = GuessForm()
let Component = GuessForm(handle)
let { container } = render(Component({ currentGuess: 0, cheat: true }))

let form = container.querySelector("form")
Expand All @@ -54,7 +72,7 @@ describe("GuessForm", () => {
})

it("does not render cheat input when cheat prop is false", () => {
let Component = GuessForm()
let Component = GuessForm(handle)
let { container } = render(Component({ currentGuess: 0, cheat: false }))

let form = container.querySelector("form")
Expand All @@ -63,7 +81,7 @@ describe("GuessForm", () => {
})

it("does not render cheat input when cheat prop is undefined", () => {
let Component = GuessForm()
let Component = GuessForm(handle)
let { container } = render(Component({ currentGuess: 0 }))

let form = container.querySelector("form")
Expand All @@ -72,7 +90,7 @@ describe("GuessForm", () => {
})

it("passes error message to letter inputs", () => {
let Component = GuessForm()
let Component = GuessForm(handle)
let { container } = render(Component({ currentGuess: 0, error: "invalid word" }))

let form = container.querySelector("form")
Expand All @@ -84,7 +102,7 @@ describe("GuessForm", () => {
})

it("first input has auto focus", () => {
let Component = GuessForm()
let Component = GuessForm(handle)
let { container } = render(Component({ currentGuess: 0 }))

let form = container.querySelector("form")
Expand All @@ -94,7 +112,7 @@ describe("GuessForm", () => {
})

it("has correct input attributes", () => {
let Component = GuessForm()
let Component = GuessForm(handle)
let { container } = render(Component({ currentGuess: 0 }))

let form = container.querySelector("form")
Expand All @@ -104,4 +122,45 @@ describe("GuessForm", () => {
expect(input).toHaveAttribute("pattern", "[a-zA-Z]{1}")
expect(input).toHaveAttribute("maxLength", "1")
})

it("enables cheat when typing c-h-e-a-t sequence within 2 seconds", async () => {
vi.useFakeTimers()
let Component = GuessForm(handle)
render(Component({ currentGuess: 0 }))

let input = screen.getByRole("textbox", { name: "letter 1" })
expect(input).toBeInTheDocument()

for (let letter of "cheat") {
fireEvent.keyDown(input, { key: letter })
vi.advanceTimersByTime(300)
}

vi.runAllTimers()
let cheatInput = await screen.findByDisplayValue("true")
expect(cheatInput).toHaveAttribute("name", "cheat")
expect(window.sessionStorage.getItem("wordle-cheat-enabled")).toBe("true")
})

it("does not enable cheat when c-h-e-a-t sequence takes more than 2 seconds", () => {
vi.useFakeTimers()
let Component = GuessForm(handle)
let { container } = render(Component({ currentGuess: 0 }))

let form = container.querySelector("form")
let input = form?.querySelector('input[name="letter"]')
expect(input).toBeInTheDocument()

fireEvent.keyDown(input!, { key: "c" })
vi.advanceTimersByTime(2_100)
for (let letter of "heat") {
fireEvent.keyDown(input!, { key: letter })
vi.advanceTimersByTime(100)
}
vi.runAllTimers()

let cheatInput = form?.querySelector('input[name="cheat"]')
expect(cheatInput).not.toBeInTheDocument()
expect(window.sessionStorage.getItem("wordle-cheat-enabled")).toBeNull()
})
})
65 changes: 62 additions & 3 deletions app/components/form.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
"use client"

import type { Handle } from "remix/component"
import { on, keysEvents } from "remix/component"

import { LETTER_INPUTS } from "#app/constants.ts"
import { CHEAT_SESSION_KEY, LETTER_INPUTS } from "#app/constants.ts"
import { routes } from "#app/routes.ts"

import { LetterInput } from "./letter-input"

export function GuessForm() {
const CHEAT_CODE = "cheat"
const CHEAT_WINDOW_MS = 2_000

export function GuessForm(handle: Handle) {
let cheatEnabled = false
let cheatBuffer = ""
let cheatStartedAt = 0
let hydrated = false

return ({
currentGuess,
cheat,
Expand All @@ -17,6 +26,19 @@ export function GuessForm() {
error?: string
cheat?: boolean
}) => {
if (!hydrated) {
hydrated = true
cheatEnabled = cheat === true
if (typeof window !== "undefined") {
cheatEnabled =
cheatEnabled || window.sessionStorage.getItem(CHEAT_SESSION_KEY) === "true"
}
}

if (cheat && !cheatEnabled) {
cheatEnabled = true
}

return (
<form
method="POST"
Expand Down Expand Up @@ -51,9 +73,46 @@ export function GuessForm() {
}
}
}),
on("keydown", async (event) => {
let target = event.target
if (!(target instanceof HTMLInputElement)) return
if (!/^[a-zA-Z]$/.test(event.key)) return

let now = Date.now()
let letter = event.key.toLowerCase()
if (cheatBuffer === "" || now - cheatStartedAt > CHEAT_WINDOW_MS) {
cheatBuffer = letter
cheatStartedAt = now
} else {
cheatBuffer += letter
}

if (!CHEAT_CODE.startsWith(cheatBuffer)) {
if (letter === CHEAT_CODE[0]) {
cheatBuffer = letter
cheatStartedAt = now
} else {
cheatBuffer = ""
cheatStartedAt = 0
}
return
}

if (cheatBuffer === CHEAT_CODE && now - cheatStartedAt <= CHEAT_WINDOW_MS) {
cheatBuffer = ""
cheatStartedAt = 0
if (!cheatEnabled) {
cheatEnabled = true
if (typeof window !== "undefined") {
window.sessionStorage.setItem(CHEAT_SESSION_KEY, "true")
}
await handle.update()
}
}
}),
]}
>
{cheat ? <input type="hidden" name="cheat" value="true" /> : null}
{cheatEnabled ? <input type="hidden" name="cheat" value="true" /> : null}
{LETTER_INPUTS.map((index) => (
<LetterInput key={`input-number-${index}`} index={index} errorMessage={error} />
))}
Expand Down
31 changes: 31 additions & 0 deletions app/components/keyboard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import "@testing-library/jest-dom/vitest"
import { render, screen } from "@mcansh/remix-testing-library"
import { describe, expect, it } from "vitest"
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find direct imports from vite/vitest in TS/TSX files.
# Expectation: no direct imports from "vite" or "vitest"; test APIs should come from "vite-plus/test".
rg -nP 'from\s+["'\''](?:vite|vitest)(?:/[^"'\'']*)?["'\'']' --type ts --type tsx -C2

Repository: mcansh/remix-wordle

Length of output: 90


🏁 Script executed:

# Search for direct vitest/vite imports in test files
rg 'from\s+["\'](?:vite|vitest)' app/components/keyboard.test.tsx

# Also check the specific line in context
head -20 app/components/keyboard.test.tsx | cat -n

Repository: mcansh/remix-wordle

Length of output: 215


🏁 Script executed:

cat -n app/components/keyboard.test.tsx | head -10

Repository: mcansh/remix-wordle

Length of output: 457


🏁 Script executed:

# Check full file for vitest imports
grep -n "vitest" app/components/keyboard.test.tsx

# Check if vite-plus is configured
cat package.json | grep -A5 -B5 "vite-plus"

Repository: mcansh/remix-wordle

Length of output: 636


Import test APIs from vite-plus/test.

Line 3 imports directly from vitest; use the vite-plus/test wrapper instead.

-import { describe, expect, it } from "vitest"
+import { describe, expect, it } from "vite-plus/test"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { describe, expect, it } from "vitest"
import { describe, expect, it } from "vite-plus/test"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/keyboard.test.tsx` at line 3, The test currently imports test
helpers directly from "vitest" (describe, expect, it) in
app/components/keyboard.test.tsx; update the import to use the project's wrapper
by importing those symbols from "vite-plus/test" instead (replace the import
line that references "vitest" so describe, expect, it are imported from
"vite-plus/test").


import { LetterState } from "#app/utils/game.ts"

import { Keyboard } from "./keyboard"

describe("Keyboard", () => {
it("renders all keyboard letters as buttons", () => {
let Component = Keyboard()
render(
Component({
board: [
[
{ letter: "q", state: LetterState.Blank },
{ letter: "w", state: LetterState.Match },
],
[{ letter: "a", state: LetterState.Present }],
[{ letter: "z", state: LetterState.Miss }],
],
}),
)

let letters = ["q", "w", "a", "z"]
for (let letter of letters) {
let button = screen.getByRole("button", { name: `keyboard letter ${letter}` })
expect(button).toHaveTextContent(letter)
}
})
})
6 changes: 4 additions & 2 deletions app/components/keyboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ export function Keyboard() {
>
{row.map((letter) => {
return (
<div
<button
type="button"
data-state={letter.state}
class={`flex size-10 items-center justify-center rounded text-center text-white uppercase data-[state=Blank]:bg-gray-400 data-[state=Match]:bg-green-500 data-[state=Miss]:bg-red-500 data-[state=Present]:bg-yellow-500`}
key={`keyboard-letter-${letter.letter}`}
aria-label={`keyboard letter ${letter.letter}`}
>
{letter.letter}
</div>
</button>
Comment on lines +18 to +26
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect keyboard usage and possible delegated click handling.
# Expectation: if keyboard keys remain buttons, there should be a click/activation path that inserts the letter.
rg -n -C4 '<Keyboard|Keyboard\\(|keyboard-letter|keyboard letter|onClick|on\\("click"' --type ts --type tsx

Repository: mcansh/remix-wordle

Length of output: 254


🏁 Script executed:

#!/bin/bash
# Fixed search for Keyboard component usage and click handlers
echo "=== Files containing Keyboard component usage ==="
rg -l "Keyboard" --type ts --type tsx

echo -e "\n=== Looking for click handlers in keyboard.tsx ==="
cat app/components/keyboard.tsx

echo -e "\n=== Searching for Keyboard component instantiation and usage ==="
rg "Keyboard\(" --type ts --type tsx -A3 -B3

echo -e "\n=== Looking for button click handlers in the file ==="
rg "onClick|on:click" app/components/keyboard.tsx

Repository: mcansh/remix-wordle

Length of output: 1430


🏁 Script executed:

#!/bin/bash
# Find where Keyboard component is imported and used
echo "=== Searching for imports of keyboard component ==="
rg "from.*keyboard|import.*Keyboard" --type=ts --type=js

echo -e "\n=== Searching for Keyboard usage in game files ==="
rg -l "board" app/components/ | head -5 | while read f; do echo "File: $f"; head -20 "$f"; done

echo -e "\n=== Check all component files for Keyboard or keyboard usage ==="
find app/components -type f -name "*.tsx" -o -name "*.ts" | head -20

echo -e "\n=== Looking for event delegation patterns ==="
rg "delegate|addEventListener|onClickCapture" --type=ts --type=js -B2 -A2

Repository: mcansh/remix-wordle

Length of output: 3736


🏁 Script executed:

#!/bin/bash
# Check parent component implementations
echo "=== home/page.tsx content (where Keyboard is used) ==="
cat app/controllers/home/page.tsx

echo -e "\n=== history/game.tsx content (where Keyboard is used) ==="
cat app/controllers/history/game.tsx

Repository: mcansh/remix-wordle

Length of output: 5584


Use non-interactive elements for keyboard status display.

These buttons are focusable and announced as interactive, but clicking them does nothing. Since the keyboard serves only to display letter states (matched, missed, present), not for user input, use <div> elements styled as buttons or explicitly disable them instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/keyboard.tsx` around lines 18 - 26, The current JSX uses an
interactive <button> to render keyboard letter states (the element with
data-state={letter.state} and key={`keyboard-letter-${letter.letter}`}) but it
is non-functional; replace the <button> with a non-interactive element (e.g.,
<div> or <span>) while preserving data-state, class, key and aria-label, and
ensure it is not focusable (set tabIndex={-1}) or use aria-disabled="true" if
you must keep a button; this removes misleading interactivity while keeping
styling and accessibility labels consistent.

)
})}
</div>
Expand Down
1 change: 1 addition & 0 deletions app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export const WORD_LENGTH = 5
export const LETTER_INPUTS = [...Array(WORD_LENGTH).keys()]
export const TOTAL_GUESSES = 6
export const REVEAL_WORD = "cheat"
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

REVEAL_WORD appears to be unused now (search shows no references outside this constants file). Since the cheat flow has moved to a session key, consider removing REVEAL_WORD (or reintroducing its usage) to avoid leaving a misleading/obsolete constant behind.

Suggested change
export const REVEAL_WORD = "cheat"

Copilot uses AI. Check for mistakes.
export const CHEAT_SESSION_KEY = "wordle-cheat-enabled"
10 changes: 7 additions & 3 deletions app/controllers/home/controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Controller } from "remix/fetch-router"
import { redirect } from "remix/response/redirect"
import { Session } from "remix/session"

import { REVEAL_WORD, WORD_LENGTH } from "#app/constants.ts"
import { CHEAT_SESSION_KEY, WORD_LENGTH } from "#app/constants.ts"
import { getReturnToQuery, requireAuth } from "#app/middleware/auth.ts"
import { createGuess, getFullBoard, getTodaysGame, isGameComplete } from "#app/models/game.ts"
import { routes } from "#app/routes.ts"
Expand Down Expand Up @@ -65,7 +65,11 @@ export const home = {
session.flash("error", error)
}

return redirect(routes.home.index.href(undefined, data.value.cheat ? { cheat: "true" } : {}))
if (data.value.cheat) {
session.set(CHEAT_SESSION_KEY, true)
}

return redirect(routes.home.index.href())
Comment on lines +68 to +72
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

The PR title suggests this is just adopting @total-typescript/shoehorn, but this change also alters the cheat mechanism (query-param based -> session based) and changes redirect behavior. Please update the PR title/description to reflect the functional behavior change, or split the dependency/tooling change from the feature change to keep history/review focused.

Copilot uses AI. Check for mistakes.
},

async index(context) {
Expand All @@ -81,7 +85,7 @@ export const home = {

let showModal = isGameComplete(game.status)

let showWord = showModal || context.url.searchParams.has(REVEAL_WORD) ? board.word : undefined
let showWord = showModal || session.get(CHEAT_SESSION_KEY) === true ? board.word : undefined

let errorMessage = session.get("error") || undefined

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@prisma/adapter-pg": "catalog:",
"@prisma/client": "catalog:",
"@standard-schema/spec": "catalog:",
"@total-typescript/shoehorn": "catalog:",
"bcryptjs": "catalog:",
"bullmq": "catalog:",
"clsx": "catalog:",
Expand Down
Loading