feat(reader): surface brightness on desktop, not just the mobile sheet
The brightness slider lived only in the mobile reader settings sheet, so desktop users couldn't dim — and could be left stuck dimmed by a value a phone had persisted (brightness is shared via localStorage and the --reader-dim effect is form-factor agnostic). Add an inline brightness slider to the desktop reader nav, marked `desktop-control` alongside the existing mode toggle and gap select. It binds the same `brightness` state, so a value set on either form factor stays in sync. Also mocks `/me/read-progress` in the reader-mode e2e setup — without it the GET/PUT fell through to the (absent) backend and stalled the reader load, which is why those specs couldn't run headless. The spec is now hermetic and covers the new desktop control (it dims/undims, and a phone-persisted value is recoverable). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,15 @@ async function mockReaderApis(page: Page) {
|
||||
body: JSON.stringify({ error: { code: 'unauthenticated', message: '' } })
|
||||
})
|
||||
);
|
||||
// Anonymous reader: read-progress reads/writes are unauthenticated. Without
|
||||
// this the GET/PUT fall through to the (absent) backend and stall the load.
|
||||
await page.route('**/api/v1/me/read-progress/**', (route) =>
|
||||
route.fulfill({
|
||||
status: 401,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ error: { code: 'unauthenticated', message: '' } })
|
||||
})
|
||||
);
|
||||
await page.route(`**/api/v1/mangas/${mangaId}`, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
@@ -126,6 +135,7 @@ test.beforeEach(async ({ context }) => {
|
||||
try {
|
||||
localStorage.removeItem('mangalord-reader-mode');
|
||||
localStorage.removeItem('mangalord-reader-gap');
|
||||
localStorage.removeItem('mangalord-reader-brightness');
|
||||
} catch {
|
||||
// ignore — about:blank doesn't expose localStorage yet.
|
||||
}
|
||||
@@ -181,6 +191,64 @@ test('gap select updates the inline gap on the continuous container', async ({ p
|
||||
await expect(container).toHaveAttribute('style', /gap:\s*64px/);
|
||||
});
|
||||
|
||||
test('desktop reader nav exposes a brightness control that dims and undims the reader', async ({
|
||||
page
|
||||
}) => {
|
||||
// The brightness slider used to live only in the mobile settings sheet,
|
||||
// so desktop users couldn't dim — and could be stuck dimmed by a value a
|
||||
// phone had persisted. The desktop nav now carries its own slider.
|
||||
await mockReaderApis(page);
|
||||
await page.goto(`/manga/${mangaId}/chapter/${chapterId}`);
|
||||
|
||||
const slider = page.getByTestId('reader-brightness-desktop');
|
||||
await expect(slider).toBeVisible();
|
||||
|
||||
const dim = () =>
|
||||
page.evaluate(() =>
|
||||
document.documentElement.style.getPropertyValue('--reader-dim')
|
||||
);
|
||||
|
||||
// Default brightness is full → no dim overlay.
|
||||
await expect.poll(async () => Number(await dim())).toBe(0);
|
||||
|
||||
// Dimming raises the overlay opacity…
|
||||
await slider.fill('0.3');
|
||||
await expect.poll(async () => Number(await dim())).toBeGreaterThan(0);
|
||||
|
||||
// …and restoring it clears the dim, so a desktop user is never stuck.
|
||||
await slider.fill('1');
|
||||
await expect.poll(async () => Number(await dim())).toBe(0);
|
||||
});
|
||||
|
||||
test('a brightness value persisted by a phone is recoverable on desktop', async ({
|
||||
page,
|
||||
context
|
||||
}) => {
|
||||
// Seed a dimmed value as if set on mobile, then open the reader on a
|
||||
// desktop viewport: the dim applies and the desktop control reflects it.
|
||||
await context.addInitScript(() => {
|
||||
try {
|
||||
localStorage.setItem('mangalord-reader-brightness', '0.3');
|
||||
} catch {
|
||||
// about:blank — ignore
|
||||
}
|
||||
});
|
||||
await mockReaderApis(page);
|
||||
await page.goto(`/manga/${mangaId}/chapter/${chapterId}`);
|
||||
|
||||
const slider = page.getByTestId('reader-brightness-desktop');
|
||||
await expect(slider).toHaveValue('0.3');
|
||||
await expect
|
||||
.poll(async () =>
|
||||
Number(
|
||||
await page.evaluate(() =>
|
||||
document.documentElement.style.getPropertyValue('--reader-dim')
|
||||
)
|
||||
)
|
||||
)
|
||||
.toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('reader-mode preference set on one page is honored when the reader opens', async ({
|
||||
page,
|
||||
context
|
||||
|
||||
@@ -1094,6 +1094,28 @@
|
||||
|
||||
{/if}
|
||||
|
||||
<!-- Desktop brightness. On mobile this control lives in the reader
|
||||
settings sheet; desktop has no sheet, so it sits inline here.
|
||||
Shares the `brightness` state and the same `--reader-dim`
|
||||
effect, so a value set on either form factor stays in sync. -->
|
||||
<label class="brightness-field desktop-control">
|
||||
<Sun size={16} aria-hidden="true" />
|
||||
<span class="visually-hidden">Brightness</span>
|
||||
<input
|
||||
type="range"
|
||||
class="brightness-slider"
|
||||
min="0.3"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={brightness}
|
||||
oninput={(e) => {
|
||||
brightness = Number((e.currentTarget as HTMLInputElement).value);
|
||||
}}
|
||||
aria-label="Brightness"
|
||||
data-testid="reader-brightness-desktop"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="reader-settings-btn mobile-control"
|
||||
@@ -2181,6 +2203,20 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Inline desktop brightness control in the reader nav. The slider is
|
||||
full-width inside the mobile settings sheet; here it's capped so it
|
||||
sits neatly beside the mode toggle. */
|
||||
.brightness-field {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.brightness-field .brightness-slider {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
/* Reader-nav sits at the viewport top — the global mobile
|
||||
chrome is hidden on this route so there's no app bar above
|
||||
|
||||
Reference in New Issue
Block a user