diff --git a/web/src/shellResponsiveCss.test.ts b/web/src/shellResponsiveCss.test.ts new file mode 100644 index 0000000..5ae45f7 --- /dev/null +++ b/web/src/shellResponsiveCss.test.ts @@ -0,0 +1,75 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const css = readFileSync(resolve(__dirname, 'index.css'), 'utf8'); + +const mediaBlocks = (() => { + const blocks: Array<{ query: string; body: string }> = []; + const mediaPattern = /@media\s*([^{]+)\{/g; + let match: RegExpExecArray | null; + + while ((match = mediaPattern.exec(css))) { + let depth = 1; + let cursor = mediaPattern.lastIndex; + for (; cursor < css.length; cursor += 1) { + if (css[cursor] === '{') depth += 1; + if (css[cursor] === '}') depth -= 1; + if (depth === 0) break; + } + blocks.push({ + query: match[1].trim(), + body: css.slice(mediaPattern.lastIndex, cursor), + }); + mediaPattern.lastIndex = cursor + 1; + } + + return blocks; +})(); + +const blocksFor = (queryPart: string) => + mediaBlocks.filter((block) => block.query.includes(queryPart)); + +const ruleBodies = (source: string, selector: string) => { + const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return [...source.matchAll(new RegExp(`${escapedSelector}\\s*\\{([^}]*)\\}`, 'g'))] + .map((match) => match[1]); +}; + +describe('responsive shell CSS contract', () => { + it('keeps desktop navigation as a left rail by default', () => { + expect(css).toMatch(/\.dashboard-main\s*\{[\s\S]*?margin-left:\s*248px;/); + expect(css).toMatch(/\.trading-sidebar\s*\{[\s\S]*?width:\s*248px;/); + expect(css).toMatch(/\.dashboard-right-panel\s*\{[\s\S]*?width:\s*340px;/); + }); + + it('keeps tablet navigation on the side instead of switching to footer nav', () => { + const tabletBlocks = blocksFor('min-width: 561px'); + expect(tabletBlocks.length).toBeGreaterThan(0); + + const tabletCss = tabletBlocks.map((block) => block.body).join('\n'); + const tabletSidebarRules = ruleBodies(tabletCss, '.trading-sidebar').join('\n'); + expect(tabletCss).toContain('margin-left: 88px'); + expect(tabletSidebarRules).toContain('width: 88px'); + expect(tabletSidebarRules).toContain('flex-direction: column'); + expect(tabletSidebarRules).toContain('border-right: 1px solid var(--border)'); + expect(tabletSidebarRules).not.toContain('width: 100%'); + expect(tabletSidebarRules).not.toContain('border-top: 1px solid var(--border)'); + }); + + it('uses bottom navigation only at phone width', () => { + const phoneCss = blocksFor('max-width: 560px').map((block) => block.body).join('\n'); + const phoneSidebarRules = ruleBodies(phoneCss, '.trading-sidebar').join('\n'); + expect(phoneCss).toContain('margin-left: 0'); + expect(phoneSidebarRules).toContain('width: 100%'); + expect(phoneSidebarRules).toContain('flex-direction: row'); + expect(phoneSidebarRules).toContain('border-top: 1px solid var(--border)'); + + const wideResponsiveCss = mediaBlocks + .filter((block) => !block.query.includes('max-width: 560px')) + .map((block) => block.body) + .join('\n'); + const wideSidebarRules = ruleBodies(wideResponsiveCss, '.trading-sidebar').join('\n'); + expect(wideSidebarRules).not.toContain('width: 100%'); + }); +});