mirror of
https://github.com/readest/readest
synced 2026-04-21 13:37:44 +00:00
fix(style): clamp oversized hardcoded pixel widths and fix browser test flakiness (#3785)
Closes #3770. Add transformStylesheet rule that clips CSS width declarations exceeding the viewport with max-width and border-box sizing. Also add @testing-library/react to vitest browser optimizeDeps.include to prevent mid-test Vite reloads on CI. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
017a9338b3
commit
db35a4e203
6 changed files with 72 additions and 4 deletions
|
|
@ -33,3 +33,4 @@
|
|||
- [Always rebase before PR](feedback_pr_rebase.md) — rebase onto origin/main before creating PRs
|
||||
- [New branch per PR](feedback_pr_new_branch.md) — always create a fresh branch from main for each new PR/issue
|
||||
- [Upgrade gstack locally](feedback_gstack_upgrade.md) — always upgrade from the project's .claude/skills/gstack, not global
|
||||
- [No lookbehind regex](feedback_no_lookbehind_regex.md) — never use `(?<=)` or `(?<!)` in JS/TS; build check rejects them
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
name: No lookbehind regex
|
||||
description: Never use lookbehind assertions in JS/TS code — the build check rejects them for browser compatibility
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Never use lookbehind regex (`(?<=...)` or `(?<!...)`) in JavaScript/TypeScript source code. Use `(?:^|[^...])` or other alternatives instead.
|
||||
|
||||
**Why:** The project has a `check:lookbehind-regex` build check (`pnpm check:all`) that scans the Next.js output chunks and fails if any lookbehind assertions are found. Older WebViews (especially on some Android devices) don't support lookbehinds.
|
||||
|
||||
**How to apply:** When writing regex that needs to assert what comes before a match, use a non-capturing group with alternation (e.g., `(?:^|[^a-z-])`) instead of a negative lookbehind (`(?<![a-z-])`). This applies to all `.ts`/`.tsx`/`.js` files that end up in the build output.
|
||||
|
|
@ -268,6 +268,46 @@ describe('transformStylesheet', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('hardcoded pixel width clamping', () => {
|
||||
it('adds max-width and border-box when width exceeds viewport', () => {
|
||||
const css = '.calibre8 { display: block; width: 1200px; padding: 2em 0 0 1em; }';
|
||||
const result = transformStylesheet(css, VW, VH, VERTICAL);
|
||||
expect(result).toContain('max-width: calc(var(--available-width) * 1px)');
|
||||
expect(result).toContain('box-sizing: border-box');
|
||||
});
|
||||
|
||||
it('does not clamp when width is smaller than viewport', () => {
|
||||
const css = '.box { width: 450px; padding: 2em; }';
|
||||
const result = transformStylesheet(css, VW, VH, VERTICAL);
|
||||
expect(result).not.toContain('max-width: calc(var(--available-width)');
|
||||
});
|
||||
|
||||
it('does not add max-width when one already exists', () => {
|
||||
const css = '.box { width: 1200px; max-width: 100%; }';
|
||||
const result = transformStylesheet(css, VW, VH, VERTICAL);
|
||||
const matches = result.match(/max-width/g);
|
||||
expect(matches).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('does not affect max-width or min-width properties', () => {
|
||||
const css = '.box { max-width: 1200px; min-width: 200px; }';
|
||||
const result = transformStylesheet(css, VW, VH, VERTICAL);
|
||||
expect(result).not.toContain('max-width: calc(var(--available-width)');
|
||||
});
|
||||
|
||||
it('does not add max-width for non-pixel width values', () => {
|
||||
const css = '.box { width: 50%; }';
|
||||
const result = transformStylesheet(css, VW, VH, VERTICAL);
|
||||
expect(result).not.toContain('max-width: calc(var(--available-width)');
|
||||
});
|
||||
|
||||
it('does not add max-width for em width values', () => {
|
||||
const css = '.box { width: 20em; }';
|
||||
const result = transformStylesheet(css, VW, VH, VERTICAL);
|
||||
expect(result).not.toContain('max-width: calc(var(--available-width)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('preserves unrelated CSS', () => {
|
||||
it('passes through CSS without any matching patterns unchanged', () => {
|
||||
const css = '.custom { display: flex; padding: 10px; margin: 5px; }';
|
||||
|
|
|
|||
|
|
@ -764,6 +764,20 @@ export const transformStylesheet = (css: string, vw: number, vh: number, vertica
|
|||
return selector + block;
|
||||
});
|
||||
|
||||
// clip hardcoded pixel widths to available width when they exceed viewport
|
||||
css = css.replace(ruleRegex, (match, selector, block) => {
|
||||
const widthMatch = /(?:^|[^a-z-])width\s*:\s*(\d+(?:\.\d+)?)px/.exec(block);
|
||||
const pxWidth = widthMatch ? parseFloat(widthMatch[1] ?? '0') : 0;
|
||||
if (pxWidth > vw && !/max-width\s*:/.test(block)) {
|
||||
block = block.replace(
|
||||
/}$/,
|
||||
' max-width: calc(var(--available-width) * 1px); box-sizing: border-box; }',
|
||||
);
|
||||
return selector + block;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
|
||||
// replace absolute font sizes with rem units
|
||||
// replace vw and vh as they cause problems with layout
|
||||
// replace hardcoded colors
|
||||
|
|
|
|||
|
|
@ -17,20 +17,22 @@ export default defineConfig({
|
|||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'js-md5',
|
||||
'@supabase/supabase-js',
|
||||
'@tauri-apps/plugin-fs',
|
||||
'@tauri-apps/plugin-http',
|
||||
'@tauri-apps/api/path',
|
||||
'@tauri-apps/api/core',
|
||||
'@testing-library/react',
|
||||
'@zip.js/zip.js',
|
||||
'franc-min',
|
||||
'iso-639-2',
|
||||
'iso-639-3',
|
||||
'uuid',
|
||||
'js-md5',
|
||||
'jwt-decode',
|
||||
'@supabase/supabase-js',
|
||||
'uuid',
|
||||
],
|
||||
exclude: [
|
||||
'@pdfjs/pdf.min.mjs',
|
||||
'@tursodatabase/database-wasm',
|
||||
'@tursodatabase/database-wasm-common',
|
||||
'@tursodatabase/database-common',
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 9a0c1c6f5bcb3a16b659d4ee4c4ceb437170fda9
|
||||
Subproject commit fe33bb510871bd0489493c49638a5b82c1de5df5
|
||||
Loading…
Reference in a new issue