Skip to content
Merged
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
23 changes: 23 additions & 0 deletions app/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
106 changes: 106 additions & 0 deletions app/components/pages/utilities/dead-pixel-check.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement>(null);
const dialogRef = useRef<HTMLDialogElement>(null);
const screenRef = useRef<HTMLButtonElement>(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 (
<>
<Button ref={startRef} type="button" onClick={startTest}>
<MaximizeIcon data-icon="inline-start" aria-hidden />
Start test
</Button>
<dialog
ref={dialogRef}
className="dead-pixel-test"
aria-label="Dead-pixel check"
onCancel={(event) => {
event.preventDefault();
stopTest();
}}
onClose={exitFullscreen}
>
<button
ref={screenRef}
type="button"
className="dead-pixel-test-surface"
style={{ backgroundColor: COLORS[colorIndex] }}
aria-label="Next test color. Press Escape to stop the test."
onClick={() => setColorIndex((index) => (index + 1) % COLORS.length)}
onContextMenu={(event) => event.preventDefault()}
onKeyDown={(event) => {
if (event.code === 'Space') {
event.preventDefault();
if (!event.repeat) setColorIndex((index) => (index + 1) % COLORS.length);
} else if (event.key === 'Escape') {
event.preventDefault();
stopTest();
}
}}
/>
</dialog>
</>
);
}
1 change: 1 addition & 0 deletions app/lib/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
1 change: 1 addition & 0 deletions app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
46 changes: 46 additions & 0 deletions app/routes/utilities/dead-pixel-check.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Meta {...metadata} />
<article className="rounded-md bg-card p-6 shadow">
<h1 className="font-serif text-xl">Dead-pixel Check</h1>
<p className="mt-2 text-sm text-muted-foreground">
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.
</p>

<section className="mt-4" aria-labelledby="dead-pixel-instructions">
<h2 id="dead-pixel-instructions" className="font-medium">
How it works
</h2>
<ol className="mt-2 flex list-decimal flex-col gap-2 pl-5 text-sm text-muted-foreground">
<li>Gently clean your screen with a soft cloth and click &ldquo;Start test&rdquo;.</li>
<li>Press F11 if your browser does not automatically enter full-screen mode.</li>
<li>Look for pixels that stay dark, stay bright, or show an unexpected color.</li>
<li>Left-click or press Space to cycle through white, black, red, green, and blue.</li>
<li>Press Esc to exit full-screen mode, stop the test, and return to this page.</li>
</ol>
</section>

<div className="mt-6">
<DeadPixelCheck />
</div>
<footer className="mt-4 text-xs text-muted-foreground">
Take your time inspecting each color. The test cycles only when you click or press Space.
</footer>
</article>
</>
);
}
5 changes: 5 additions & 0 deletions app/routes/utilities/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<DeadPixelCheck />);
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(<DeadPixelCheck />);
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(<DeadPixelCheck />);
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(<DeadPixelCheck />);
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();
});
});