diff --git a/app/src/components/Archive.js b/app/src/components/Archive.js index ec49bfe1..5a1edfc1 100644 --- a/app/src/components/Archive.js +++ b/app/src/components/Archive.js @@ -498,8 +498,9 @@ export function DomainsPage() { } }, {enableOnFormTags: false}); - // Header section matching ChannelsPage pattern - const header =
+ // Header section matching ChannelsPage pattern. No `marginBottom`: this is a top-level block + // of the page, and the page's stack spaces it -- its own margin was added to that gap. + const header =
navigator.clipboard.writeText('url')} />; - return + /* + * A fragment, not a `PageContainer`. `ArchiveRoute` already wraps its `` in one, and + * a second applies its `margin-top: 1em` and `padding: 1em` again -- so this page sat 1em + * lower and 1em further in on all four sides than /archives/domains beside it. As a fragment + * these panels become blocks of the route's own page stack, which spaces them. + */ + return <>
Archive Downloader Config
@@ -1081,7 +1088,7 @@ function ArchiveSettingsPage() { action={urlFieldNameClipboardButton} />
-
+ } function ArchivesPage() { diff --git a/app/src/components/Channels.js b/app/src/components/Channels.js index 0a9ed3df..b31f3c00 100644 --- a/app/src/components/Channels.js +++ b/app/src/components/Channels.js @@ -499,8 +499,9 @@ export function ChannelsPage() { } }, {enableOnFormTags: false}); - // Header section matching DomainsPage pattern - const header = + // Header section matching DomainsPage pattern. No `mb`: this is a top-level block of the + // page, and the page's stack spaces it -- its own margin was added to that gap. + const header = { expect(stacks).toHaveLength(2); stacks.forEach(stack => expect(stack.textContent).toBe('Body')); }); + + it('is never rendered by a page that a PageContainer already wraps', () => { + /* + * `PageContainer` carries `margin-top: 1em` and `padding: 1em`, so nesting one inside + * another applies both twice: the page is pushed 1em further down and inset 1em further + * in on all four sides than every other page in the app. `/archives/settings` did this + * -- `ArchiveRoute` wraps its `` in one, and `ArchiveSettingsPage` wrapped itself + * in another -- and read as "more margin than /archives/domains", which is exactly what + * it was. + * + * Nothing about a doubled margin throws, so this is a source scan. It reads each route + * wrapper (a `PageContainer` holding `` or ``), takes the components it + * routes to, and checks the ones defined in the same file -- which is where a route and + * its pages normally live together. + */ + const src = path.join(__dirname, '..'); + const files = (dir) => fs.readdirSync(dir, {withFileTypes: true}).flatMap(entry => { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) return files(full); + if (!/\.jsx?$/.test(entry.name)) return []; + if (/\.(test|cy)\.jsx?$/.test(entry.name)) return []; + return [full]; + }); + + const offenders = []; + for (const file of files(src)) { + const text = fs.readFileSync(file, 'utf8'); + + /* + * EVERY `` in the file, not just the first. Checking only the first + * is what let this bug through a version of this test: in Archive.js the first one + * is `ArchiveSettingsPage`'s -- the offender -- and its block holds no ``, so + * the file was skipped before the route wrapper 400 lines below was ever looked at. + */ + for (const block of routeWrapperBlocks(text)) { + const routed = [...block.matchAll(/element=\{<(\w+)\s*\/>\}/g)].map(m => m[1]); + for (const name of routed) { + // The component's body, bounded by the next top-level declaration. + const declaration = new RegExp(`^(?:export\\s+)?(?:function|const)\\s+${name}\\b`, 'm'); + const found = declaration.exec(text); + if (!found) continue; // Defined elsewhere; out of this scan's reach. + const after = text.slice(found.index + 1); + const next = after.search(/^(?:export\s+)?(?:function|const|class)\s/m); + const body = next === -1 ? after : after.slice(0, next); + if (body.includes('')) { + offenders.push(`${path.relative(src, file)}: ${name}`); + } + } + } + } + + /* + * The premise: a scan that found no route wrappers, or routed to no components, would + * report no offenders and read exactly like a clean codebase. + */ + const wrappers = files(src).flatMap(file => routeWrapperBlocks(fs.readFileSync(file, 'utf8'))); + expect(wrappers.length).toBeGreaterThan(2); + expect(wrappers.flatMap(b => [...b.matchAll(/element=\{<(\w+)\s*\/>\}/g)]).length) + .toBeGreaterThan(8); + + expect(offenders).toEqual([]); + }); }); + +/** + * Every `...` in `text` that wraps a router -- so, the ones that + * wrap a whole section of the app rather than one page. All of them, because a file may hold both + * a route wrapper and the pages it routes to, in either order. + */ +const routeWrapperBlocks = (text) => { + const blocks = []; + let from = 0; + for (;;) { + const start = text.indexOf('', from); + if (start === -1) return blocks; + const end = text.indexOf('', start); + if (end === -1) return blocks; + const block = text.slice(start, end); + if (/|/.test(block)) blocks.push(block); + from = end + 1; + } +};