From 61c038609192d93a30f09e219fd9405624a1432a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:29:30 -0300 Subject: [PATCH 1/5] feat(DeadPixelChecker): add styles for dead pixel test component --- app/app.css | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/app.css b/app/app.css index 9c45743..5556bd8 100644 --- a/app/app.css +++ b/app/app.css @@ -152,3 +152,26 @@ @apply font-mono; } } + +/* Diagnostic colors must remain exact, with no theme overrides, transitions, or visible controls. */ +.dead-pixel-test, +.dead-pixel-test-surface { + position: fixed; + inset: 0; + width: 100%; + height: 100%; + max-width: none; + max-height: none; + margin: 0; + padding: 0; + border: 0; + border-radius: 0; + outline: none; + cursor: none; + transition: none; + forced-color-adjust: none; +} + +.dead-pixel-test { + overflow: hidden; +} From 5166ace669ea52dc059ac9bf6703d0829d9bdf1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:29:53 -0300 Subject: [PATCH 2/5] feat(routes): add dead pixel checker route --- app/routes.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/routes.ts b/app/routes.ts index 93f3c98..30ceeb3 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -31,6 +31,7 @@ export default [ ...prefix('/utilities', [ index('routes/utilities/index.tsx'), route('/character-counter', 'routes/utilities/character-counter.tsx'), + route('/dead-pixel-check', 'routes/utilities/dead-pixel-check.tsx'), route('/dice-roller', 'routes/utilities/dice-roller.tsx'), route('/lorem-ipsum', 'routes/utilities/lorem-ipsum.tsx'), route('/meme-generator', 'routes/utilities/meme-generator.tsx'), From 41270f3e888ae97d6c92f4074f31632ac06f7023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:30:16 -0300 Subject: [PATCH 3/5] feat(routes): add dead pixel check utility to the navigation --- app/lib/consts.ts | 1 + app/routes/utilities/index.tsx | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/app/lib/consts.ts b/app/lib/consts.ts index 1c046cf..5f1b90b 100644 --- a/app/lib/consts.ts +++ b/app/lib/consts.ts @@ -10,6 +10,7 @@ import D8Icon from '~/components/icons/d8.icon'; export const URLS = { utilities: [ { title: 'Char. Counter', url: '/utilities/character-counter' }, + { title: 'Dead-pixel Check', url: '/utilities/dead-pixel-check' }, { title: 'Dice Roller', url: '/utilities/dice-roller' }, { title: 'Lorem Ipsum Generator', url: '/utilities/lorem-ipsum' }, { title: 'Meme Generator', url: '/utilities/meme-generator' }, diff --git a/app/routes/utilities/index.tsx b/app/routes/utilities/index.tsx index e2d882f..999b56f 100644 --- a/app/routes/utilities/index.tsx +++ b/app/routes/utilities/index.tsx @@ -15,6 +15,11 @@ const utilities = [ href: '/utilities/character-counter', description: 'Count characters, words, and lines for any text input quickly.', }, + { + title: 'Dead-pixel Check', + href: '/utilities/dead-pixel-check', + description: 'Inspect your screen for dead pixels with a full-screen test in five solid colors.', + }, { title: 'Dice Roller', href: '/utilities/dice-roller', From 15905a4947eef98a50d581d5ad9187fd2530abfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:31:10 -0300 Subject: [PATCH 4/5] feat(DeadPixelChecker): implement dead pixel check page with instructions and utility component --- .../pages/utilities/dead-pixel-check.tsx | 106 ++++++++++++++++++ app/routes/utilities/dead-pixel-check.tsx | 46 ++++++++ 2 files changed, 152 insertions(+) create mode 100644 app/components/pages/utilities/dead-pixel-check.tsx create mode 100644 app/routes/utilities/dead-pixel-check.tsx diff --git a/app/components/pages/utilities/dead-pixel-check.tsx b/app/components/pages/utilities/dead-pixel-check.tsx new file mode 100644 index 0000000..6af7fea --- /dev/null +++ b/app/components/pages/utilities/dead-pixel-check.tsx @@ -0,0 +1,106 @@ +import { MaximizeIcon } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; +import { Button } from '~/components/ui/button'; + +const COLORS = ['#FFFFFF', '#000000', '#ff0000', '#00ff00', '#0000ff'] as const; + +export default function DeadPixelCheck() { + const startRef = useRef(null); + const dialogRef = useRef(null); + const screenRef = useRef(null); + const [colorIndex, setColorIndex] = useState(0); + + function exitFullscreen() { + if (document.fullscreenElement === screenRef.current && document.fullscreenElement) { + void document.exitFullscreen().catch(() => { + // The browser may already be leaving full-screen mode after Esc. + }); + } + } + + function stopTest() { + exitFullscreen(); + dialogRef.current?.close(); + } + + useEffect(() => { + const dialog = dialogRef.current; + const screen = screenRef.current; + let wasFullscreen = false; + function onFullscreenChange() { + if (document.fullscreenElement === screen) { + wasFullscreen = true; + } else if (wasFullscreen) { + wasFullscreen = false; + dialog?.close(); + // Exiting native fullscreen can reset focus after the modal has already closed. + startRef.current?.focus(); + } + } + + document.addEventListener('fullscreenchange', onFullscreenChange); + return () => { + document.removeEventListener('fullscreenchange', onFullscreenChange); + if (screen && document.fullscreenElement === screen) { + void document.exitFullscreen().catch(() => {}); + } + dialog?.close(); + }; + }, []); + + async function startTest() { + const dialog = dialogRef.current; + const screen = screenRef.current; + if (!dialog || !screen || dialog.open) return; + + setColorIndex(0); + // A native modal keeps focus inside the test and covers the app even if fullscreen is unavailable. + dialog.showModal(); + screen.focus(); + try { + // Request the inner surface: browsers do not allow fullscreen on a dialog itself. + await screen.requestFullscreen({ navigationUI: 'hide' }); + if (!dialog.open && document.fullscreenElement === screen) exitFullscreen(); + } catch { + // Keep the viewport-filling test open so the user can use F11 as instructed. + } + } + + return ( + <> + + { + event.preventDefault(); + stopTest(); + }} + onClose={exitFullscreen} + > + + + ); +} diff --git a/app/routes/utilities/dead-pixel-check.tsx b/app/routes/utilities/dead-pixel-check.tsx new file mode 100644 index 0000000..60b0bf3 --- /dev/null +++ b/app/routes/utilities/dead-pixel-check.tsx @@ -0,0 +1,46 @@ +import { Meta } from '~/components/application/meta'; +import DeadPixelCheck from '~/components/pages/utilities/dead-pixel-check'; +import type { Metadata } from '~/types/metadata'; + +export const metadata: Metadata = { + title: 'Dead-pixel Check', + description: 'Inspect your screen for dead pixels with a full-screen test in five solid colors.', + keywords: ['dead pixel check', 'screen test', 'LCD', 'subpixel defects'], + manifest: '/manifest.webmanifest', +}; + +export default function Page() { + return ( + <> + +
+

Dead-pixel Check

+

+ Dead pixels are defective pixels on an LCD screen that do not work as expected. Defects can appear + as dark spots, bright spots, or partial subpixel defects. Inspect your screen using five solid + colors. +

+ +
+

+ How it works +

+
    +
  1. Gently clean your screen with a soft cloth and click “Start test”.
  2. +
  3. Press F11 if your browser does not automatically enter full-screen mode.
  4. +
  5. Look for pixels that stay dark, stay bright, or show an unexpected color.
  6. +
  7. Left-click or press Space to cycle through white, black, red, green, and blue.
  8. +
  9. Press Esc to exit full-screen mode, stop the test, and return to this page.
  10. +
+
+ +
+ +
+
+ Take your time inspecting each color. The test cycles only when you click or press Space. +
+
+ + ); +} From cbb81f2a3d4410aa5b383a6dc999f7732f310ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:32:02 -0300 Subject: [PATCH 5/5] tests: add browser tests for DeadPixelCheck component functionality --- .../dead-pixel-check.browser.test.tsx | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/browser/components/utilities/dead-pixel-check.browser.test.tsx diff --git a/tests/browser/components/utilities/dead-pixel-check.browser.test.tsx b/tests/browser/components/utilities/dead-pixel-check.browser.test.tsx new file mode 100644 index 0000000..5fe2e1e --- /dev/null +++ b/tests/browser/components/utilities/dead-pixel-check.browser.test.tsx @@ -0,0 +1,80 @@ +import { afterEach, describe, expect, test, vi } from 'vite-plus/test'; +import { userEvent } from 'vite-plus/test/browser'; +import { render } from 'vitest-browser-react'; +import DeadPixelCheck from '~/components/pages/utilities/dead-pixel-check'; + +afterEach(() => vi.restoreAllMocks()); + +describe('DeadPixelCheck', () => { + test('cycles all five colors with left clicks and wraps back to white', async () => { + vi.spyOn(Element.prototype, 'requestFullscreen').mockRejectedValue(new Error('Fullscreen denied')); + const screen = await render(); + await screen.getByRole('button', { name: 'Start test' }).click(); + const surface = screen.getByRole('button', { name: /Next test color/ }); + await expect.element(surface).toHaveStyle({ backgroundColor: 'rgb(255, 255, 255)' }); + for (const color of [ + 'rgb(0, 0, 0)', + 'rgb(255, 0, 0)', + 'rgb(0, 255, 0)', + 'rgb(0, 0, 255)', + 'rgb(255, 255, 255)', + ]) { + await surface.click(); + await expect.element(surface).toHaveStyle({ backgroundColor: color }); + } + const bounds = surface.element().getBoundingClientRect(); + expect(bounds.x).toBe(0); + expect(bounds.y).toBe(0); + expect(bounds.width).toBe(window.innerWidth); + expect(bounds.height).toBe(window.innerHeight); + await userEvent.keyboard('{Escape}'); + await expect.element(screen.getByRole('dialog')).not.toBeInTheDocument(); + }); + + test('advances once per Space press, ignores right clicks, and restores focus on Escape', async () => { + vi.spyOn(Element.prototype, 'requestFullscreen').mockRejectedValue(new Error('Fullscreen denied')); + const screen = await render(); + const start = screen.getByRole('button', { name: 'Start test' }); + await start.click(); + const surface = screen.getByRole('button', { name: /Next test color/ }); + await userEvent.keyboard(' '); + await expect.element(surface).toHaveStyle({ backgroundColor: 'rgb(0, 0, 0)' }); + surface.element().dispatchEvent(new KeyboardEvent('keydown', { code: 'Space', repeat: true, bubbles: true })); + await surface.click({ button: 'right' }); + await expect.element(surface).toHaveStyle({ backgroundColor: 'rgb(0, 0, 0)' }); + await userEvent.keyboard('{Escape}'); + await expect.element(start).toHaveFocus(); + await start.click(); + await expect.element(surface).toHaveStyle({ backgroundColor: 'rgb(255, 255, 255)' }); + await userEvent.keyboard('{Escape}'); + }); + + test('requests fullscreen on the test surface and stops when the browser exits fullscreen', async () => { + let fullscreenElement: Element | null = null; + vi.spyOn(document, 'fullscreenElement', 'get').mockImplementation(() => fullscreenElement); + const screen = await render(); + const request = vi.spyOn(Element.prototype, 'requestFullscreen').mockImplementation(() => { + fullscreenElement = screen.getByRole('button', { name: /Next test color/ }).element(); + document.dispatchEvent(new Event('fullscreenchange')); + return Promise.resolve(); + }); + await screen.getByRole('button', { name: 'Start test' }).click(); + expect(request).toHaveBeenCalledWith({ navigationUI: 'hide' }); + expect(request.mock.contexts[0]).toBe(fullscreenElement); + expect(fullscreenElement).toBe(screen.getByRole('button', { name: /Next test color/ }).element()); + fullscreenElement = null; + document.dispatchEvent(new Event('fullscreenchange')); + await expect.element(screen.getByRole('button', { name: 'Start test' })).toHaveFocus(); + await expect.element(screen.getByRole('dialog')).not.toBeInTheDocument(); + }); + + test('works without the Fullscreen API and closes on Escape', async () => { + vi.spyOn(Element.prototype, 'requestFullscreen'); + Object.defineProperty(Element.prototype, 'requestFullscreen', { configurable: true, value: undefined }); + const screen = await render(); + await screen.getByRole('button', { name: 'Start test' }).click(); + await expect.element(screen.getByRole('dialog')).toBeVisible(); + await userEvent.keyboard('{Escape}'); + await expect.element(screen.getByRole('button', { name: 'Start test' })).toHaveFocus(); + }); +});