From dba09f8e09201a147f19d5d50ecec9bcf39e1826 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:54:44 +0000 Subject: [PATCH] redesign right SQL panel: responsive layout, larger query editor, scroll isolation, Playwright tests Agent-Logs-Url: https://github.com/Devn913/SQL_Chess/sessions/eb41956a-9637-482d-a220-c795c0d325b3 Co-authored-by: Devn913 <56478595+Devn913@users.noreply.github.com> --- .github/workflows/deploy.yml | 33 ++++- css/style.css | 165 +++++++++++++++++++--- index.html | 2 +- package.json | 7 + playwright.config.js | 61 +++++++++ test-results/.last-run.json | 4 + tests/responsive.spec.js | 259 +++++++++++++++++++++++++++++++++++ 7 files changed, 512 insertions(+), 19 deletions(-) create mode 100644 playwright.config.js create mode 100644 test-results/.last-run.json create mode 100644 tests/responsive.spec.js diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index dcec89a..2c90d4e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,10 +29,40 @@ jobs: - name: Validate HTML run: html-validate index.html + # ── Playwright responsive / layout tests ───────────────────── + test: + name: Responsive Layout Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install chromium --with-deps + + - name: Run Playwright tests + run: npx playwright test --reporter=list + + - name: Upload Playwright report on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 + # ── Deploy (only on push to main) ──────────────────────────── deploy: name: Deploy to GitHub Pages - needs: validate + needs: [validate, test] if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest @@ -61,3 +91,4 @@ jobs: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 + diff --git a/css/style.css b/css/style.css index 393b315..d0d67a7 100644 --- a/css/style.css +++ b/css/style.css @@ -12,10 +12,10 @@ --chess-panel-padding: 4rem; /* total horizontal padding within chess panel (2 × 1rem + margins) */ - /* Board size: fills the 70 % chess panel minus padding, capped at 720 px. + /* Board size: fills the 68 % chess panel minus padding, capped at 720 px. Also constrained by viewport height (subtracting ~280px for header 56px + player bars ~80px + controls ~40px + move history ~100px + gaps ~4px). */ - --board-sz: clamp(300px, min(calc(70vw - var(--chess-panel-padding) * 2), calc(100vh - 280px)), 720px); + --board-sz: clamp(300px, min(calc(68vw - var(--chess-panel-padding) * 2), calc(100vh - 280px)), 720px); --text-primary: #e6edf3; --text-secondary: #8b949e; @@ -166,8 +166,8 @@ a { color: var(--accent-blue); } align-items: center; padding: 1rem; gap: .6rem; - flex: 0 0 70%; - width: 70%; + flex: 0 0 68%; + width: 68%; min-width: 0; overflow-y: auto; scrollbar-width: thin; @@ -405,22 +405,25 @@ a { color: var(--accent-blue); } /* ── SQL Panel ──────────────────────────────────────────────── */ .sql-panel { - flex: 0 0 30%; - width: 30%; + flex: 0 0 32%; + width: 32%; + min-width: 300px; display: flex; flex-direction: column; border-left: 1px solid var(--bg-border); background: var(--sql-bg); overflow: hidden; - min-width: 0; transition: width .25s ease, flex .25s ease, opacity .2s; } .sql-panel.hidden-panel { flex: 0 0 0; width: 0; + /* Override the base min-width (300 px) so the panel collapses fully */ + min-width: 0; opacity: 0; pointer-events: none; border-left: none; + overflow: hidden; } .sql-panel-header { @@ -459,13 +462,16 @@ a { color: var(--accent-blue); } /* SQL content area */ .sql-content { flex: 1; + min-height: 0; overflow-y: auto; + overflow-x: hidden; padding: .75rem 1rem; display: flex; flex-direction: column; gap: .75rem; scroll-behavior: smooth; scrollbar-width: thin; + scrollbar-color: var(--bg-border) transparent; } .sql-placeholder { @@ -782,7 +788,9 @@ input:checked + .slider::before { transform: translateX(18px); } font-size: .78rem; line-height: 1.6; padding: .6rem .75rem; - resize: none; + resize: vertical; + min-height: 7.5rem; + max-height: 20rem; outline: none; transition: background .15s; } @@ -845,41 +853,164 @@ input:checked + .slider::before { transform: translateX(18px); } .sql-input-section { display: flex; } } +/* ── Responsive: Tablet (≤ 1024 px) ────────────────────────── */ +@media (max-width: 1024px) { + .sql-panel { + flex: 0 0 36%; + width: 36%; + min-width: 260px; + } + .chess-panel { + flex: 0 0 64%; + width: 64%; + } + :root { + --board-sz: clamp(280px, + min(calc(64vw - var(--chess-panel-padding) * 2), calc(100vh - 280px)), + 640px); + } +} + +/* ── Responsive: Mobile landscape / narrow (≤ 900 px) ──────── */ @media (max-width: 900px) { - .app-main { flex-direction: column; overflow-y: auto; overflow-x: hidden; } + .app-main { + flex-direction: column; + overflow: hidden; + } .chess-panel { - flex: none; + flex: 0 0 auto; width: 100%; + max-height: 58vh; min-height: auto; - overflow-y: visible; + overflow-y: auto; + scrollbar-width: thin; } .sql-panel { - flex: none; + flex: 1 1 auto; width: 100%; + min-width: 0; + min-height: 40vh; border-left: none; border-top: 1px solid var(--bg-border); - height: 45vh; - min-height: 260px; + overflow: hidden; } .sql-panel.hidden-panel { - height: 0; + flex: 0 0 0; min-height: 0; + height: 0; border-top: none; + overflow: hidden; + } + + .sql-move-input { + min-height: 5.5rem; } /* Mobile: limit by both viewport width and height to handle short screens */ :root { --board-sz: min(90vw, 440px, calc(55vh)); } } +/* ── Responsive: Mobile portrait (≤ 600 px) ────────────────── */ +@media (max-width: 600px) { + .chess-panel { + max-height: 54vh; + padding: .6rem; + } + + .sql-panel { + min-height: 44vh; + } + + .sql-move-input { + min-height: 5rem; + font-size: .75rem; + } + + .sql-panel-header { + padding: .4rem .75rem; + } + + .sql-input-bar { + padding: .45rem .75rem; + } + + :root { --board-sz: min(90vw, 380px, calc(50vh)); } +} + +/* ── Responsive: Small phone (≤ 480 px) ────────────────────── */ @media (max-width: 480px) { .app-header { padding: 0 .75rem; } .header-logo .logo-text { display: none; } - .chess-panel { padding: .5rem; } + .chess-panel { padding: .5rem; gap: .4rem; max-height: 52vh; } .header-controls { gap: .3rem; } - :root { --board-sz: min(92vw, 360px); } + .sql-panel { min-height: 46vh; } + + .sql-move-input { min-height: 4.5rem; } + + :root { --board-sz: min(92vw, 340px, calc(48vh)); } #btnToggleSQL #sqlToggleLabel { display: none; } } + +/* ── Responsive: Wide screens (≥ 1440 px) ──────────────────── */ +@media (min-width: 1440px) { + .sql-panel { + flex: 0 0 28%; + width: 28%; + min-width: 340px; + } + .chess-panel { + flex: 0 0 72%; + width: 72%; + } + :root { + --board-sz: clamp(400px, + min(calc(72vw - var(--chess-panel-padding) * 2), calc(100vh - 280px)), + 760px); + } +} + +/* ── Responsive: Very wide screens (≥ 1920 px) ─────────────── */ +@media (min-width: 1920px) { + .sql-panel { + flex: 0 0 26%; + width: 26%; + min-width: 400px; + } + .chess-panel { + flex: 0 0 74%; + width: 74%; + } + .sql-move-input { + min-height: 9rem; + } +} + +/* ── Responsive: Short landscape (height < 500 px) ─────────── */ +/* e.g. phones in landscape orientation */ +@media (max-height: 500px) { + .chess-panel { + max-height: 50vh; + } + .sql-panel { + min-height: 48vh; + } + .sql-move-input { + min-height: 3rem; + max-height: 6rem; + } + .sql-input-bar { + min-height: 36px; + padding: .25rem .75rem; + } + .sql-input-actions { + padding: .25rem .5rem; + } + .sql-panel-header { + padding: .35rem .75rem; + } +} + diff --git a/index.html b/index.html index 5539676..ded5f90 100644 --- a/index.html +++ b/index.html @@ -101,7 +101,7 @@

id="sqlMoveInput" class="sql-move-input" spellcheck="false" - rows="3" + rows="6" placeholder="UPDATE chess_piece SET position = 'e4' WHERE position = 'e2'; -- or shorthand: e2 e4">
diff --git a/package.json b/package.json index 2542c90..74ccad6 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,12 @@ { "dependencies": { "chess.js": "^0.10.3" + }, + "devDependencies": { + "@playwright/test": "^1.59.1" + }, + "scripts": { + "test": "playwright test", + "test:report": "playwright show-report" } } diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..4ada4f2 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,61 @@ +// @ts-check +const { defineConfig, devices } = require('@playwright/test'); +const path = require('path'); + +module.exports = defineConfig({ + testDir: './tests', + timeout: 30_000, + retries: 0, + reporter: [['list'], ['html', { open: 'never' }]], + + use: { + /* Serve index.html directly via file:// so no server is needed */ + baseURL: 'file://' + path.resolve(__dirname, 'index.html'), + headless: true, + screenshot: 'only-on-failure', + video: 'off', + }, + + projects: [ + /* Desktop sizes */ + { + name: 'desktop-1920x1080', + use: { ...devices['Desktop Chrome'], viewport: { width: 1920, height: 1080 } }, + }, + { + name: 'desktop-1440x900', + use: { ...devices['Desktop Chrome'], viewport: { width: 1440, height: 900 } }, + }, + { + name: 'desktop-1280x800', + use: { ...devices['Desktop Chrome'], viewport: { width: 1280, height: 800 } }, + }, + /* Tablet sizes */ + { + name: 'tablet-1024x768', + use: { ...devices['Desktop Chrome'], viewport: { width: 1024, height: 768 } }, + }, + { + name: 'tablet-portrait-768x1024', + use: { ...devices['Desktop Chrome'], viewport: { width: 768, height: 1024 } }, + }, + /* Mobile sizes */ + { + name: 'mobile-portrait-390x844', + use: { ...devices['Desktop Chrome'], viewport: { width: 390, height: 844 } }, + }, + { + name: 'mobile-landscape-844x390', + use: { ...devices['Desktop Chrome'], viewport: { width: 844, height: 390 } }, + }, + { + name: 'mobile-small-360x640', + use: { ...devices['Desktop Chrome'], viewport: { width: 360, height: 640 } }, + }, + /* Ultrawide */ + { + name: 'ultrawide-2560x1080', + use: { ...devices['Desktop Chrome'], viewport: { width: 2560, height: 1080 } }, + }, + ], +}); diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..cbcc1fb --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file diff --git a/tests/responsive.spec.js b/tests/responsive.spec.js new file mode 100644 index 0000000..6c5da90 --- /dev/null +++ b/tests/responsive.spec.js @@ -0,0 +1,259 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); +const path = require('path'); + +const PAGE_URL = 'file://' + path.resolve(__dirname, '..', 'index.html'); + +/** + * Helper: open the page, dismiss the setup modal by playing as guest, + * then return the page for further assertions. + */ +async function openAndStartGame(page) { + await page.goto(PAGE_URL); + // Dismiss setup modal + await page.click('#btnPlayAsGuest'); +} + +// ── SQL Panel visibility ──────────────────────────────────────────────────── + +test.describe('SQL panel — visible on page load', () => { + test('SQL panel is rendered and not hidden', async ({ page }) => { + await openAndStartGame(page); + const sqlPanel = page.locator('#sqlPanel'); + await expect(sqlPanel).toBeVisible(); + await expect(sqlPanel).not.toHaveClass(/hidden-panel/); + }); + + test('SQL panel header is visible', async ({ page }) => { + await openAndStartGame(page); + await expect(page.locator('.sql-panel-header')).toBeVisible(); + }); + + test('SQL panel contains at least one SQL block after game start', async ({ page }) => { + await openAndStartGame(page); + const blocks = page.locator('.sql-block'); + await expect(blocks).toHaveCount(1); + }); +}); + +// ── Query textarea ────────────────────────────────────────────────────────── + +test.describe('SQL query textarea', () => { + test('textarea is visible', async ({ page }) => { + await openAndStartGame(page); + await expect(page.locator('#sqlMoveInput')).toBeVisible(); + }); + + test('textarea has rows=6', async ({ page }) => { + await openAndStartGame(page); + const rows = await page.locator('#sqlMoveInput').getAttribute('rows'); + expect(Number(rows)).toBeGreaterThanOrEqual(6); + }); + + test('textarea has sufficient rendered height (≥ 80 px)', async ({ page }) => { + await openAndStartGame(page); + const height = await page.locator('#sqlMoveInput').evaluate( + (el) => el.getBoundingClientRect().height + ); + expect(height).toBeGreaterThanOrEqual(80); + }); + + test('textarea is pre-filled with the default SQL sample', async ({ page }) => { + await openAndStartGame(page); + const value = await page.locator('#sqlMoveInput').inputValue(); + expect(value.toLowerCase()).toContain('update chess_piece'); + }); + + test('textarea allows vertical resize (resize != none)', async ({ page }) => { + await openAndStartGame(page); + const resize = await page.locator('#sqlMoveInput').evaluate( + (el) => window.getComputedStyle(el).resize + ); + expect(resize).not.toBe('none'); + }); +}); + +// ── SQL content area scrollability ───────────────────────────────────────── + +test.describe('SQL content area — scrollability', () => { + test('sql-content has overflow-y scroll or auto', async ({ page }) => { + await openAndStartGame(page); + const overflow = await page.locator('#sqlContent').evaluate( + (el) => window.getComputedStyle(el).overflowY + ); + expect(['auto', 'scroll']).toContain(overflow); + }); + + test('body does NOT scroll (overflow hidden)', async ({ page }) => { + await openAndStartGame(page); + const overflow = await page.locator('body').evaluate( + (el) => window.getComputedStyle(el).overflow + ); + expect(overflow).toBe('hidden'); + }); + + test('sql-content has positive scrollHeight (content is present)', async ({ page }) => { + await openAndStartGame(page); + const sh = await page.locator('#sqlContent').evaluate((el) => el.scrollHeight); + expect(sh).toBeGreaterThan(0); + }); +}); + +// ── Layout correctness at various viewports ───────────────────────────────── + +test.describe('Layout at current viewport', () => { + test('chess panel and SQL panel are side-by-side on wide screens', async ({ page, viewport }) => { + if (!viewport || viewport.width < 1024) test.skip(); + await openAndStartGame(page); + + const chessBox = await page.locator('#chessPanel').boundingBox(); + const sqlBox = await page.locator('#sqlPanel').boundingBox(); + expect(chessBox).not.toBeNull(); + expect(sqlBox).not.toBeNull(); + + // On wide screens both panels should be in the same row (similar top values) + expect(Math.abs((chessBox?.y ?? 0) - (sqlBox?.y ?? 0))).toBeLessThan(10); + // SQL panel should be to the right of chess panel + expect((sqlBox?.x ?? 0)).toBeGreaterThan((chessBox?.x ?? 0)); + }); + + test('SQL panel stacks below chess panel on narrow screens', async ({ page, viewport }) => { + if (!viewport || viewport.width >= 900) test.skip(); + await openAndStartGame(page); + + const chessBox = await page.locator('#chessPanel').boundingBox(); + const sqlBox = await page.locator('#sqlPanel').boundingBox(); + expect(chessBox).not.toBeNull(); + expect(sqlBox).not.toBeNull(); + + // SQL panel should be below chess panel (larger Y) + expect((sqlBox?.y ?? 0)).toBeGreaterThan((chessBox?.y ?? 0)); + }); + + test('no horizontal page overflow', async ({ page }) => { + await openAndStartGame(page); + const bodyWidth = await page.evaluate(() => document.body.scrollWidth); + const viewWidth = await page.evaluate(() => window.innerWidth); + // Allow up to 1 px rounding difference + expect(bodyWidth).toBeLessThanOrEqual(viewWidth + 1); + }); + + test('chess board is visible and square', async ({ page }) => { + await openAndStartGame(page); + const box = await page.locator('#board').boundingBox(); + expect(box).not.toBeNull(); + if (box) { + expect(box.width).toBeGreaterThan(100); + // Width and height should be within 2 px of each other + expect(Math.abs(box.width - box.height)).toBeLessThanOrEqual(2); + } + }); + + test('SQL panel header does not overflow its container', async ({ page }) => { + await openAndStartGame(page); + const panelBox = await page.locator('#sqlPanel').boundingBox(); + const headerBox = await page.locator('.sql-panel-header').boundingBox(); + if (panelBox && headerBox) { + expect(headerBox.width).toBeLessThanOrEqual(panelBox.width + 1); + } + }); + + test('SQL input section is fully visible within the SQL panel', async ({ page, viewport }) => { + // On very short landscape screens (height < 500 px) the panel is compact by design; + // the user can still interact — skip strict bounds check in that scenario. + if (viewport && viewport.height < 500) test.skip(); + await openAndStartGame(page); + const panelBox = await page.locator('#sqlPanel').boundingBox(); + const inputBox = await page.locator('#sqlInputSection').boundingBox(); + if (panelBox && inputBox) { + // Input section top should be within the panel + expect(inputBox.y).toBeGreaterThanOrEqual(panelBox.y - 1); + // Input section bottom should be within the panel + expect(inputBox.y + inputBox.height).toBeLessThanOrEqual(panelBox.y + panelBox.height + 1); + } + }); +}); + +// ── Toggle SQL panel ──────────────────────────────────────────────────────── + +test.describe('Toggle SQL panel', () => { + test('SQL panel hides when toggle button is clicked', async ({ page }) => { + await openAndStartGame(page); + await page.click('#btnToggleSQL'); + await expect(page.locator('#sqlPanel')).toHaveClass(/hidden-panel/); + }); + + test('SQL panel reappears when toggle button is clicked again', async ({ page }) => { + await openAndStartGame(page); + await page.click('#btnToggleSQL'); + await page.click('#btnToggleSQL'); + await expect(page.locator('#sqlPanel')).not.toHaveClass(/hidden-panel/); + }); + + test('chess panel expands when SQL panel is hidden', async ({ page, viewport }) => { + if (!viewport || viewport.width < 1024) test.skip(); + await openAndStartGame(page); + + const widthBefore = await page.locator('#chessPanel').evaluate( + (el) => el.getBoundingClientRect().width + ); + await page.click('#btnToggleSQL'); + // Wait for the CSS transition to finish (width change detectable via polling) + await expect.poll(async () => + page.locator('#chessPanel').evaluate((el) => el.getBoundingClientRect().width) + ).toBeGreaterThan(widthBefore); + }); +}); + +// ── SQL block content ─────────────────────────────────────────────────────── + +test.describe('SQL block content', () => { + test('initial SQL block contains CREATE TABLE statements', async ({ page }) => { + await openAndStartGame(page); + const codeText = await page.locator('.sql-block .sql-code').first().textContent(); + expect(codeText?.toUpperCase()).toContain('CREATE TABLE'); + }); + + test('a chess move appends a new SQL block', async ({ page }) => { + await openAndStartGame(page); + const before = await page.locator('.sql-block').count(); + + // Click e2 then e4 to make a move + await page.locator('.sq[data-square="e2"]').click(); + await page.locator('.sq[data-square="e4"]').click(); + + const after = await page.locator('.sql-block').count(); + expect(after).toBeGreaterThan(before); + }); + + test('Clear SQL button removes all SQL blocks', async ({ page }) => { + await openAndStartGame(page); + // Make a move first + await page.locator('.sq[data-square="e2"]').click(); + await page.locator('.sq[data-square="e4"]').click(); + await page.click('#btnClearSQL'); + await expect(page.locator('.sql-block')).toHaveCount(0); + }); +}); + +// ── Run SQL via textarea ──────────────────────────────────────────────────── + +test.describe('Execute SQL move via textarea', () => { + test('typing a shorthand move and running it executes the move', async ({ page }) => { + await openAndStartGame(page); + await page.locator('#sqlMoveInput').fill('e2 e4'); + await page.click('#btnRunSQL'); + // After a successful move the error should be hidden + await expect(page.locator('#sqlRunError')).toHaveClass(/hidden/); + }); + + test('invalid SQL shows an error message', async ({ page }) => { + await openAndStartGame(page); + await page.locator('#sqlMoveInput').fill('not valid sql'); + await page.click('#btnRunSQL'); + const errorEl = page.locator('#sqlRunError'); + await expect(errorEl).not.toHaveClass(/hidden/); + const text = await errorEl.textContent(); + expect(text?.trim().length).toBeGreaterThan(0); + }); +});