diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5b19a1..101c279 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,36 @@ permissions: contents: read jobs: + forms: + name: Shared forms browser QA + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Verify exact tested source + env: + EXPECTED_TEST_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$EXPECTED_TEST_SHA" + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Install isolated browser tooling + run: | + npm install --prefix "$RUNNER_TEMP/cms-ui" --no-audit --no-fund --save-exact playwright@1.61.1 + "$RUNNER_TEMP/cms-ui/node_modules/.bin/playwright" install --with-deps chromium + - name: Exercise actual editor and platform forms + env: + CMS_PLAYWRIGHT_MODULE: ${{ runner.temp }}/cms-ui/package.json + CMS_UI_ARTIFACTS: ${{ runner.temp }}/cms-form-results + run: node adminui/testdata/form-usability.cjs + - uses: actions/upload-artifact@v4 + if: always() + with: + name: cms-form-browser-results + path: ${{ runner.temp }}/cms-form-results + retention-days: 7 test: runs-on: ubuntu-latest services: diff --git a/README.md b/README.md index ed1cb2e..b0661da 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,19 @@ The module path must resolve an installed Playwright package; optionally set and a test cookie auth hook. It does not exercise production authorization or PostgreSQL restart durability. +The shared form layout/interaction regression needs no Go build or live service: + +```sh +CMS_PLAYWRIGHT_MODULE=/path/to/playwright/package.json \ + CMS_UI_ARTIFACTS=/tmp/cms-forms node adminui/testdata/form-usability.cjs +``` + +It drives the actual editor/platform UI at desktop, tablet and narrow mobile +widths against synthetic loopback APIs, including raw source resize/preservation, +draft/save/reload/conflict behavior, required fields, dates and site creation. +Optionally set `CMS_BROWSER_CHANNEL=msedge` to reuse an installed browser. +Hosted CI runs this harness on the exact source head and retains screenshots. + > ⚠️ **Experimental** — This plugin compiles and passes its unit tests but has not been validated in any active GoCodeAlone-internal production deployment. Use with caution. Please [open an issue](https://github.com/GoCodeAlone/workflow-plugin-cms/issues/new) if you adopt it so we can promote it to **verified** status. [](LICENSE) diff --git a/adminui/static/admin.css b/adminui/static/admin.css index ef0fe02..59168a5 100644 --- a/adminui/static/admin.css +++ b/adminui/static/admin.css @@ -1,5 +1,8 @@ :root { --control-border: #768697; } * { box-sizing: border-box; } +button { min-height:44px; padding:.6rem .9rem; border:1px solid var(--control-border); border-radius:4px; background:white; color:#192638; font:inherit; cursor:pointer; } +button:disabled { cursor:default; } +input,select,textarea { min-width:0; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; @@ -128,7 +131,7 @@ form > button:hover { background: #155bb5; } border-color: #1a73e8; } -button.danger { background: #dc3545; } +button.danger { background: #b42332; color:white; border-color:#b42332; } button.danger:hover { background: #b02a37; } button.link { @@ -149,10 +152,20 @@ button.link { color: white; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); - max-width: 24rem; -} -#toast.error { background: #dc3545; } + max-width: min(32rem, calc(100% - 2rem)); + max-height:calc(100vh - 2rem); + overflow:auto; + display:flex; + align-items:flex-start; + gap:1rem; + overflow-wrap:anywhere; + z-index:10; +} +#toast.error { background: #b42332; } #toast.ok { background: #198754; } +#toast > span { min-width:0; } +#toast button { flex:none; color:inherit; background:transparent; border-color:currentColor; padding:.35rem .55rem; } +#toast button:focus-visible { outline-color:white; } details { margin-top: 1rem; } details summary { cursor: pointer; padding: 0.5rem 0; } @@ -164,9 +177,10 @@ details summary { cursor: pointer; padding: 0.5rem 0; } background: white; } -[data-event-tool] { display:grid; gap:.6rem; padding:1rem; border:1px solid var(--control-border); } +[data-event-tool] { display:grid; min-width:0; gap:.6rem; padding:1rem; border:1px solid var(--control-border); } [data-event-tool][hidden] { display:none; } -[data-event-tool] label { display:grid; gap:.2rem; } +[data-event-tool] label { display:grid; grid-template-columns:minmax(0,1fr); gap:.4rem; } +[data-event-tool] label > input,[data-event-tool] label > select { grid-column:auto; min-height:44px; } [data-event-tool] input { font:inherit; padding:.5rem; width:100%; } button:focus-visible, a:focus-visible { outline:3px solid #154f9d; outline-offset:3px; } @media(max-width:640px) { body{padding:.6rem} section{padding:.8rem} form{display:block} form label{display:block;margin:.8rem 0} form .form-row{display:block} table{font-size:.85rem} th,td{padding:.4rem} header{gap:.5rem;flex-wrap:wrap} } @@ -187,19 +201,22 @@ h1 { font-size:1.6rem; } h2 { margin-top:0; font-size:1.45rem; } h3 { margin-top #page-tabs a,.page-tabs a { padding:.65rem .85rem; border-radius:6px; text-decoration:none; color:#3c526d; font-weight:550; } #page-tabs a[aria-current],.page-tabs a[aria-current] { background:#e7eefb; color:#174985; } #page-editor-form { display:block; margin:0; } -.settings-pane { max-width:640px; display:block; } +.settings-pane { width:100%; max-width:640px; display:block; } .settings-pane label { display:block; font-weight:550; margin:1rem 0; } -.settings-pane label input,.settings-pane label select { display:block; margin-top:.4rem; padding:.65rem; } +.settings-pane label input,.settings-pane label select { display:block; margin-top:.4rem; padding:.65rem; min-height:44px; } +.field-help { font-size:.875rem; font-weight:400; color:#4c6076; line-height:1.5; margin:.5rem 0 1rem; } .content-layout { display:grid; grid-template-columns:210px minmax(0,1fr); gap:1.5rem; } #section-tabs { display:flex; flex-direction:column; gap:.25rem; border-right:1px solid #dce2ea; padding-right:.8rem; } #section-tabs a { padding:.65rem .75rem; border-radius:6px; text-decoration:none; line-height:1.4; overflow-wrap:anywhere; } #section-tabs a[aria-current] { background:#e7eefb; color:#174985; font-weight:600; } .content-editor { min-width:0; }.content-editor .form-row { display:block; }.content-editor .rich-editor { display:grid; } +#section-tabs[hidden] + .content-editor { grid-column:1 / -1; } .editor-surface { min-height:20rem; border-color:var(--control-border); padding:1rem; line-height:1.6; } .editor-surface section { padding:0; box-shadow:none; border:0; margin:0; background:none; } .editor-surface img { max-height:330px; object-fit:contain; } -[data-editor-source] { width:100%; min-height:24rem; font:13px/1.55 ui-monospace,monospace; padding:1rem; border:1px solid var(--control-border); border-radius:6px; } +[data-editor-source] { width:100%; max-width:100%; min-height:24rem; height:clamp(24rem,60vh,48rem); resize:vertical; overflow:auto; font:14px/1.55 ui-monospace,monospace; padding:1rem; border:1px solid var(--control-border); border-radius:6px; } [data-source-notice] { font-size:.875rem; color:#664300; background:#fff5db; padding:.8rem; border-radius:6px; } .table-wrap { overflow-x:auto; } th,td { padding:.8rem; }.sr-only { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); } #page-preview { width:100%; min-height:650px; border:1px solid #dce2ea; margin-top:1rem; background:white; } @media(max-width:700px) { body { padding:1rem; }.view { padding:1rem; }.editor-heading,.view-heading { align-items:flex-start; flex-direction:column; }.content-layout { grid-template-columns:1fr; }#section-tabs { flex-direction:row; flex-wrap:wrap; border-right:0; border-bottom:1px solid #dce2ea; padding:0 0 .8rem; }#section-tabs a { font-size:.85rem; }.editor-surface { min-height:16rem; }#page-preview { min-height:550px; } } +@media(max-width:700px) { input,select,textarea,[data-editor-source] { font-size:16px; } } diff --git a/adminui/static/admin.js b/adminui/static/admin.js index 7499f7f..da25028 100644 --- a/adminui/static/admin.js +++ b/adminui/static/admin.js @@ -70,7 +70,15 @@ function pagePayload(form) { } function dateTimeLocalToISO(value){if(!value)return "";const d=new Date(value);return Number.isNaN(d.getTime())?"":d.toISOString()} function isoToDateTimeLocal(value){if(!value)return "";const d=new Date(value);if(Number.isNaN(d.getTime()))return "";const pad=n=>String(n).padStart(2,"0");return d.getFullYear()+"-"+pad(d.getMonth()+1)+"-"+pad(d.getDate())+"T"+pad(d.getHours())+":"+pad(d.getMinutes())} -function toast(msg,kind){const t=$("#toast");t.textContent=msg;t.className=kind||"";t.hidden=false;setTimeout(()=>{t.hidden=true},3500)} +let toastTimer; +function toast(msg,kind){ + const t=$("#toast"),previousFocus=document.activeElement;clearTimeout(toastTimer); + const message=document.createElement("span");message.textContent=msg; + const dismiss=document.createElement("button");dismiss.type="button";dismiss.textContent="Dismiss";dismiss.setAttribute("aria-label","Dismiss notification"); + dismiss.onclick=()=>{const focused=t.contains(document.activeElement);t.hidden=true;clearTimeout(toastTimer);if(focused&&previousFocus?.isConnected)previousFocus.focus()}; + t.replaceChildren(message,dismiss);t.className=kind||"";t.hidden=false; + if(kind!=="error")toastTimer=setTimeout(()=>{if(!t.contains(document.activeElement))t.hidden=true},5000); +} function fail(err){console.error(err);toast(err.message,"error")} function escapeHTML(s){return String(s||"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")} function canonicalID(s){return /^[1-9][0-9]*$/.test(s)&&Number.isSafeInteger(Number(s))?Number(s):0} @@ -151,7 +159,7 @@ async function showPagePane(route,generation){ $$("[data-page-pane]",form).forEach(el=>el.hidden=el.dataset.pagePane!==(route.view==="html"?"content":route.view)); if(route.view==="content"||route.view==="html"){ const isHTML=route.view==="html",advanced=editor.dataset.advanced==="true"; - source.hidden=!isHTML;surface.hidden=isHTML;$(".editor-toolbar",editor).hidden=isHTML||advanced||!(route.pid==="new"?pagePermissions.create:pagePermissions.edit); + source.hidden=!isHTML;$(":scope > #page-source-help",editor).hidden=!isHTML;surface.hidden=isHTML;$(".editor-toolbar",editor).hidden=isHTML||advanced||!(route.pid==="new"?pagePermissions.create:pagePermissions.edit); surface.contentEditable=String(!advanced&&(route.pid==="new"?pagePermissions.create:pagePermissions.edit)); const sections=$("#section-tabs");sections.hidden=isHTML;sections.replaceChildren(); const children=[...surface.children],plain=[...surface.childNodes].some(n=>n.nodeType===Node.TEXT_NODE&&n.textContent.trim()); diff --git a/adminui/static/index.html b/adminui/static/index.html index 46bee4b..eb7af73 100644 --- a/adminui/static/index.html +++ b/adminui/static/index.html @@ -9,10 +9,10 @@