From 4ecb052bd1398ef1b4b394eb849c3bcf8f9b3e0d Mon Sep 17 00:00:00 2001 From: KCM Date: Sat, 1 Aug 2026 14:29:19 -0500 Subject: [PATCH 1/5] docs: update article. --- docs/article.md | 118 ++++++++++++++++++++++++++++++------------------ 1 file changed, 74 insertions(+), 44 deletions(-) diff --git a/docs/article.md b/docs/article.md index e049e50..6d45ad5 100644 --- a/docs/article.md +++ b/docs/article.md @@ -1,90 +1,120 @@ -# Forget The Build Step: A Browser-Native Editor For JSX + CSS +--- +title: 'Forget The Build Step: A Browser-Native Editor For JSX + CSS' +published: true +description: '@knighted/develop is a browser-native editor for JSX and CSS — compile, lint, typecheck, and sync to GitHub without a local build step.' +tags: javascript, webdev, showdev, react +--- Frontend tooling is incredibly capable. It is also often front-loaded. -For many UI ideas, the first thing you do is not write code. You install, -configure, and wait. The creative loop starts late. +For many UI ideas, the first thing you do is not write code. You install, configure, and wait. The creative loop starts late. -[@knighted/develop](https://github.com/knightedcodemonkey/develop) is built -for a different default: fast prototyping from anywhere you can open a browser. +[@knighted/develop](https://github.com/knightedcodemonkey/develop) is built for a different default: fast prototyping from anywhere you can open a browser. -It is a browser-native editor/workbench for -[@knighted/jsx](https://github.com/knightedcodemonkey/jsx) and -[@knighted/css](https://github.com/knightedcodemonkey/css), delivered through -CDN ESM with mode-aware loading. +It is a browser-native editor/workbench for [@knighted/jsx](https://github.com/knightedcodemonkey/jsx) and [@knighted/css](https://github.com/knightedcodemonkey/css), delivered through CDN ESM with mode-aware loading. ## The Loop, In Practice -Open the app, edit multiple files in dynamic tabs, switch render/style modes, -run lint/type diagnostics, and preview instantly. +Open the app, spin up isolated workspaces, edit multiple files in dynamic tabs, switch render/style modes, run lint/type diagnostics, and preview instantly. No local bundler is required for that inner loop. +In DOM mode, JSX expressions resolve to real DOM nodes — no virtual DOM, no diffing, no reconciler. Here is a countdown timer that offloads its tick to a Web Worker and mutates the element JSX returned directly: + +```tsx +export const App = () => { + //
resolves to a real HTMLDivElement; the cast reflects the runtime type + const counterEl = ( +
100
+ ) as HTMLDivElement + + const workerCode = ` + let startTime = performance.now(); + let ticks = 0; + function tick() { + ticks++; + self.postMessage(100 - ticks); + if (ticks < 100) { + setTimeout(tick, 1000 - ((performance.now() - startTime) - (ticks * 1000))); + } + } + tick(); + ` + + const worker = new Worker( + URL.createObjectURL(new Blob([workerCode], { type: 'application/javascript' })), + ) + + worker.onmessage = e => { + counterEl.textContent = String(e.data) + if (e.data <= 0) worker.terminate() + } + + return ( +
+ {counterEl} +
+ ) +} +``` + ## What The App Gives You -- Dynamic tabbed editing (add, rename, remove, and protect required entry tabs) -- Render mode switch: DOM or React -- Style mode switch: CSS, CSS Modules, Less, Sass -- Live preview with iframe-isolated style encapsulation -- In-browser lint and type diagnostics with jump-to-source navigation -- GitHub-connected workflows for Open PR and Push Commit -- AI chat with tab-aware edit proposals and explicit apply/undo controls +- **Multiple Workspaces:** Create and switch between isolated projects without losing your current files. +- **Dynamic Tabbed Editing:** Add, rename, remove, and protect required entry tabs within any workspace. +- **Instant Share URLs:** Encode the current workspace state into a URL you can share or bookmark. +- **Direct GitHub Synchronization:** Open pull requests and push commits from the browser. +- **Render Mode Switch:** Toggle instantly between DOM or React runtimes. +- **Style Mode Switch:** Support for CSS, CSS Modules, Less, and Sass. +- **Live Preview:** Real-time updates with iframe-isolated style encapsulation. +- **In-Browser Diagnostics:** Full lint and type diagnostics with jump-to-source navigation. +- **AI Integration:** Chat with tab-aware edit proposals and explicit apply/undo controls. -This is not only "can this compile?" It is about shipping the whole iteration -loop in one place: edit, validate, preview, sync, and refine. +The goal is a complete iteration loop without switching tools: edit, check, preview, and sync. ## Why `@knighted/jsx` + `@knighted/css` Matter Here The app demonstrates both libraries in realistic authoring conditions: -- `@knighted/jsx` provides a direct path from JSX to rendered output, - including DOM-first workflows. -- `@knighted/css` handles modern browser-side style compilation, - including Modules/Less/Sass modes. +- `@knighted/jsx` provides a direct path from JSX to rendered output, including DOM-first workflows. +- `@knighted/css` handles modern browser-side style compilation, including Modules/Less/Sass modes. -Together they show how much of the authoring cycle modern browsers can run -directly. +Together they show what a browser can handle natively when you stop routing everything through a local build tool. ## "Compiler-as-a-Service" Without A Build Farm In this project, Compiler-as-a-Service means: - CDN delivers modules and WASM artifacts. -- The browser session performs compile, lint, typecheck, render, and editor - interactions locally. - -It is service-oriented distribution with local execution. +- The browser session performs compile, lint, typecheck, render, and editor interactions locally. -Mode-aware loading keeps costs aligned to usage: if you do not use Sass, -Sass does not load. +It is service-oriented distribution with local execution. Mode-aware loading means you only download what you use: skip Sass mode and the Sass bundle never loads. ## Why This Matters -This does not replace production pipelines. +This does not replace production pipelines — it removes the setup cost for exploratory work where a full project scaffold is overkill. -It lowers the cost of exploration while preserving enough workflow surface to -be useful for real component work. +The loop is complete enough for real component work: write, render, diagnose, push. If you can share a URL or open a PR from the same tab you are editing in, the feedback cycle gets shorter. -When setup friction drops, teams try more ideas. When feedback is immediate, -they converge faster. When browser-native workspaces can sync to GitHub and -carry chat-assisted edit proposals, collaboration is lighter too. - -For prototyping and component development, that is a meaningful shift. +For prototyping and focused component work, that is worth something. ## Try It +> **Note:** The app loads its compiler and runtime from CDN on first visit. If something fails to initialize, a hard reload (`Cmd/Ctrl + Shift + R`) is usually enough to recover. + - Live workbench: https://knightedcodemonkey.github.io/develop/ - Source: https://github.com/knightedcodemonkey/develop If you want a fast product tour, try this sequence: -1. Add a new tab, rename it, and make an edit. +1. Spin up a new workspace or add a new file tab, rename it, and make an edit. 2. Toggle DOM -> React render mode. 3. Toggle CSS -> Modules -> Less -> Sass style mode. 4. Open diagnostics and jump to a reported line. -5. Connect BYOT (Bring Your Own Token) by adding a GitHub personal access token, then run Open PR / Push Commit. -6. Ask chat for a targeted tab update, then apply it. +5. Copy a Share URL to send your current workspace state to someone else. +6. Connect your GitHub personal access token (BYOT) to run an Open PR or Push Commit. +7. Ask chat for a targeted tab update, then apply it. -That flow tells the product story better than any architecture diagram. +That covers most of what the app does. From 440fd79d1d555d7d78270f43bba87995799eac48 Mon Sep 17 00:00:00 2001 From: KCM Date: Sat, 1 Aug 2026 14:35:22 -0500 Subject: [PATCH 2/5] ci: skip playwright on unrelated. --- .github/workflows/playwright.yml | 35 +++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 62f58c0..27a5c9b 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -7,16 +7,15 @@ on: types: - opened - synchronize - paths: &playwright_paths + push: + branches: + - main + paths: - 'src/**' - 'playwright/**' - 'package-lock.json' - 'playwright.config.ts' - '.github/workflows/playwright.yml' - push: - branches: - - main - paths: *playwright_paths workflow_dispatch: concurrency: @@ -62,19 +61,35 @@ jobs: - name: Checkout uses: actions/checkout@v6.0.2 + - name: Detect Playwright-impacting changes + id: changes + uses: dorny/paths-filter@v4.0.2 + with: + filters: | + playwright: + - 'src/**' + - 'playwright/**' + - 'package-lock.json' + - 'playwright.config.ts' + - '.github/workflows/playwright.yml' + - name: Setup Node + if: ${{ steps.changes.outputs.playwright == 'true' }} uses: actions/setup-node@v6.4.0 with: node-version: '24.14.0' cache: npm - name: Install Dependencies + if: ${{ steps.changes.outputs.playwright == 'true' }} run: npm ci - name: Check Types + if: ${{ steps.changes.outputs.playwright == 'true' }} run: npm run check-types - name: Cache Playwright Browsers + if: ${{ steps.changes.outputs.playwright == 'true' }} uses: actions/cache@v5.0.5 with: path: ~/.cache/ms-playwright @@ -83,9 +98,11 @@ jobs: ${{ runner.os }}-playwright-${{ matrix.browser }}- - name: Install Browsers + if: ${{ steps.changes.outputs.playwright == 'true' }} run: npx playwright install --with-deps ${{ matrix.browser }} - name: Run Playwright Tests + if: ${{ steps.changes.outputs.playwright == 'true' }} env: CI: 'true' run: | @@ -96,17 +113,21 @@ jobs: npm run test:e2e -- --project=${{ matrix.browser }} --workers=${{ matrix.workers }} $SHARD_ARGS - name: Upload Playwright report + if: ${{ failure() && steps.changes.outputs.playwright == 'true' }} uses: actions/upload-artifact@v7.0.1 - if: ${{ failure() }} with: name: playwright-report-${{ matrix.browser }}-${{ matrix.shardIndex }}of${{ matrix.shardTotal }} path: playwright-report if-no-files-found: ignore - name: Upload Playwright test results + if: ${{ failure() && steps.changes.outputs.playwright == 'true' }} uses: actions/upload-artifact@v7.0.1 - if: ${{ failure() }} with: name: test-results-${{ matrix.browser }}-${{ matrix.shardIndex }}of${{ matrix.shardTotal }} path: test-results if-no-files-found: ignore + + - name: Skip Playwright for unrelated changes + if: ${{ steps.changes.outputs.playwright != 'true' }} + run: echo 'No Playwright-impacting files changed; marking required check as successful.' From ff541d18fe60ad60cc7dab9fefabd41268f8cf8c Mon Sep 17 00:00:00 2001 From: KCM Date: Sat, 1 Aug 2026 15:00:17 -0500 Subject: [PATCH 3/5] refactor: default and article. --- .github/workflows/playwright.yml | 35 ++++------------- docs/article.md | 67 +++++++++++++++++--------------- src/app.js | 13 ++++++- src/modules/app-core/defaults.js | 35 ++++++++++------- 4 files changed, 74 insertions(+), 76 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 27a5c9b..62f58c0 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -7,15 +7,16 @@ on: types: - opened - synchronize - push: - branches: - - main - paths: + paths: &playwright_paths - 'src/**' - 'playwright/**' - 'package-lock.json' - 'playwright.config.ts' - '.github/workflows/playwright.yml' + push: + branches: + - main + paths: *playwright_paths workflow_dispatch: concurrency: @@ -61,35 +62,19 @@ jobs: - name: Checkout uses: actions/checkout@v6.0.2 - - name: Detect Playwright-impacting changes - id: changes - uses: dorny/paths-filter@v4.0.2 - with: - filters: | - playwright: - - 'src/**' - - 'playwright/**' - - 'package-lock.json' - - 'playwright.config.ts' - - '.github/workflows/playwright.yml' - - name: Setup Node - if: ${{ steps.changes.outputs.playwright == 'true' }} uses: actions/setup-node@v6.4.0 with: node-version: '24.14.0' cache: npm - name: Install Dependencies - if: ${{ steps.changes.outputs.playwright == 'true' }} run: npm ci - name: Check Types - if: ${{ steps.changes.outputs.playwright == 'true' }} run: npm run check-types - name: Cache Playwright Browsers - if: ${{ steps.changes.outputs.playwright == 'true' }} uses: actions/cache@v5.0.5 with: path: ~/.cache/ms-playwright @@ -98,11 +83,9 @@ jobs: ${{ runner.os }}-playwright-${{ matrix.browser }}- - name: Install Browsers - if: ${{ steps.changes.outputs.playwright == 'true' }} run: npx playwright install --with-deps ${{ matrix.browser }} - name: Run Playwright Tests - if: ${{ steps.changes.outputs.playwright == 'true' }} env: CI: 'true' run: | @@ -113,21 +96,17 @@ jobs: npm run test:e2e -- --project=${{ matrix.browser }} --workers=${{ matrix.workers }} $SHARD_ARGS - name: Upload Playwright report - if: ${{ failure() && steps.changes.outputs.playwright == 'true' }} uses: actions/upload-artifact@v7.0.1 + if: ${{ failure() }} with: name: playwright-report-${{ matrix.browser }}-${{ matrix.shardIndex }}of${{ matrix.shardTotal }} path: playwright-report if-no-files-found: ignore - name: Upload Playwright test results - if: ${{ failure() && steps.changes.outputs.playwright == 'true' }} uses: actions/upload-artifact@v7.0.1 + if: ${{ failure() }} with: name: test-results-${{ matrix.browser }}-${{ matrix.shardIndex }}of${{ matrix.shardTotal }} path: test-results if-no-files-found: ignore - - - name: Skip Playwright for unrelated changes - if: ${{ steps.changes.outputs.playwright != 'true' }} - run: echo 'No Playwright-impacting files changed; marking required check as successful.' diff --git a/docs/article.md b/docs/article.md index 6d45ad5..6a93939 100644 --- a/docs/article.md +++ b/docs/article.md @@ -21,42 +21,44 @@ Open the app, spin up isolated workspaces, edit multiple files in dynamic tabs, No local bundler is required for that inner loop. -In DOM mode, JSX expressions resolve to real DOM nodes — no virtual DOM, no diffing, no reconciler. Here is a countdown timer that offloads its tick to a Web Worker and mutates the element JSX returned directly: +In DOM mode, JSX expressions resolve to real DOM nodes — no virtual DOM, no diffing, no reconciler. Tabs are standard ESM modules too, so your entry tab can import from sibling tabs with relative paths: + +`App.tsx` ```tsx -export const App = () => { - //
resolves to a real HTMLDivElement; the cast reflects the runtime type - const counterEl = ( -
100
- ) as HTMLDivElement - - const workerCode = ` - let startTime = performance.now(); - let ticks = 0; - function tick() { - ticks++; - self.postMessage(100 - ticks); - if (ticks < 100) { - setTimeout(tick, 1000 - ((performance.now() - startTime) - (ticks * 1000))); - } - } - tick(); - ` - - const worker = new Worker( - URL.createObjectURL(new Blob([workerCode], { type: 'application/javascript' })), - ) - - worker.onmessage = e => { - counterEl.textContent = String(e.data) - if (e.data <= 0) worker.terminate() +import { Counter } from './Counter.js' + +export const App = () => ( +
+ +
+) +``` + +`Counter.tsx` + +```tsx +import '../styles/app.css' + +type CounterProps = { + label: string +} + +export const Counter = ({ label }: CounterProps) => { + const el = ( + + ) as HTMLButtonElement + let count = 0 + + el.onclick = () => { + count += 1 + el.textContent = `${label}: ${count}` + el.classList.toggle('is-even', count % 2 === 0) } - return ( -
- {counterEl} -
- ) + return el } ``` @@ -106,6 +108,7 @@ For prototyping and focused component work, that is worth something. - Live workbench: https://knightedcodemonkey.github.io/develop/ - Source: https://github.com/knightedcodemonkey/develop +- Worker DOM snapshot (Share URL): [Open workspace example](https://knightedcodemonkey.github.io/develop/?sws=H4sIAAAAAAAAE4VWYW_bNhD9Kwd2m-1VkqXUdVM5SpumHVYsWYskxT7UxUJTJ5kJRQokbdc1_N8HklacpN1mwLZ4vHs8vnt39oYsURuuJMmziDDVtBpNWJP6G29JRJhGarE8sSTPXhw-H2ejLB2n6bOIGElbM1eW5BvCS5KTlfm7epaVmLGX8fj5GOMRKw_il-noMD4o8RDT6vBFOUYSEbNoURsssXzvAklEVkrfmpYyvGSqRZIToRgV9zf-wHVnzvMKqY1dvkqitHFZ1i9iJkaGRERjqwLmjBqH1FAuSUTmSN1ZLnL448hW_7loZqhJLhdCuPUVt8JB_KX0LWp4--Hcu50qafGrvbTUul0uKbN8if5wWaI-V6Wzl6ohEamUtKfGfNKC5GRubWvy4dAZTVIrVQukLTcJU82QGXPwqqINF-vivbSoc26piFRrvkWrem5fp1E2SpJnB1GWpknyMk0n2WPLL7v4CzVTVgWALvZeUOddctMKui7MirpiWzozJP-8qydKq9ckIpI27jonbZtY89UxQO2c5MRotmfSDPf7gsp6QWsXdEOX1DDNWxvf-D2tPKMdNjcngbvc6gVGxDCthCB56uQoLUpLcvdkLJy0LRTQH0BxDJupBAhmphaOq3cCCjgq-RKMXQsspp742PBvmMNzjc0EvGGFvJ7bHGZKlJMpOc7S9GhY8uUxUAO_X52fveXLdwIblHayP2TlBXCqSoQCrp0dQKAFY6m2V7xx5hZ1pXRDJcNEqlV_MNn7Wc5uDRSQ7mzVQjLLlfQb_UG4j3t5x6dPJ93aoKiSVhl7jsbQGvtZmkIc3AZ3XryCfjjiCLI0vYfnEHyCamG9S-QcHES__13CEO8v5BY7zF99yGCwP28bHnZf4Q5-8_o7zqAAiSsIHdT_dHGWhInyYXaDzH66OOu77TdCzfqf9yx_iWADdt1iDj3atoIz6uga7uXUg23IyB0YAhMlm8CS0wnuhQJ7kSSucU-DsqCAS6u5rPuYlNTS7n6OzWCBowLSQQdvUTdcUou7y279p3trtAstH8pv11p5JfDrBG4WxvJqHe9EnTN0-UxgRtltrdVClvmT7CAbZWwCTAml8ydVVU1gHuSapelyPgEqeC1jbrExHcKUHG_ubrcNUp5M5Xbi21nXaD_q37jAj__Ts9y85dqu7_pwLVmY-mEWCmrspbddYKMsXs5ptxVcT--6lTet0hZ6STL0XJghbduEGdNzxXJFhdOQ8JuFtUp-1Kp1veFLJegMRQ7G18UZlDwVnN3m0MelIw7O1cLgO_fsC7xUvJzKrYMOsnuA7YSwCaBRBwXb_AcJeLC-O_FoFkKZoMYUU7JjNw7mKfG6LKbkbr3DLTa7h-1x0NHGn-ub5GgYvI-ncrBP9buJ5kaFP86Nin0nzaksBYbki38j4r7S4WkBWbd2CLOODR-bsIXWKO2V10c3-AIZu9kXgkPYo5659omYHH4KwtteP3B2fWPQJuE3EYpdRj_DARRFASm8gl5FhcEe5NBzaus9iPe0n3FjE6vqWmC_x03s0u5Fj6EGvgvDCOha8GH5fQWKXpdx2tsX6x6pWxgeew2RIPRzVfKKY_noL8_2S0TCra7o7P39X8jHf5LS8Xh0cHh4-N9w238AZi93_38JAAA) If you want a fast product tour, try this sequence: diff --git a/src/app.js b/src/app.js index 990ffcc..9ba30b4 100644 --- a/src/app.js +++ b/src/app.js @@ -36,7 +36,7 @@ import { createLayoutDiagnosticsSetup } from './modules/app-core/layout-diagnost import { createWorkspaceControllersSetup } from './modules/app-core/workspace-controllers-setup.js' import { createWorkspaceScopeForkActions } from './modules/app-core/workspace-scope-fork-actions.js' import { createGitHubWorkflowsSetup } from './modules/app-core/github-workflows-setup.js' -import { defaultCss, defaultJsx } from './modules/app-core/defaults.js' +import { defaultCss, defaultJsx, defaultModuleJsx } from './modules/app-core/defaults.js' import { createGitHubPrContextUiController } from './modules/app-core/github-pr-context-ui.js' import { createGitHubTokenInfoUiController } from './modules/app-core/github-token-info-ui.js' import { @@ -227,8 +227,10 @@ const clearConfirmCopy = document.getElementById('clear-confirm-copy') const clearConfirmButton = clearConfirmDialog?.querySelector('button[value="confirm"]') const defaultComponentTabPath = 'src/components/App.tsx' +const defaultModuleTabPath = 'src/components/Counter.tsx' const defaultStylesTabPath = 'src/styles/app.css' const defaultComponentTabName = 'App.tsx' +const defaultModuleTabName = 'Counter.tsx' const defaultStylesTabName = 'app.css' const editorKinds = ['component', 'styles'] const editorPanelsByKind = { @@ -279,6 +281,15 @@ const workspaceTabsState = createWorkspaceTabsState({ isActive: true, content: defaultJsx, }, + { + id: 'counter', + name: defaultModuleTabName, + path: defaultModuleTabPath, + language: 'javascript-jsx', + role: 'module', + isActive: false, + content: defaultModuleJsx, + }, { id: 'styles', name: defaultStylesTabName, diff --git a/src/modules/app-core/defaults.js b/src/modules/app-core/defaults.js index ddff06f..eb8026d 100644 --- a/src/modules/app-core/defaults.js +++ b/src/modules/app-core/defaults.js @@ -1,28 +1,33 @@ export const defaultJsx = [ + "import { Counter } from './Counter.js'", + '', + 'export const App = () => (', + '
', + " ", + '
', + ')', + '', +].join('\n') + +export const defaultModuleJsx = [ "import '../styles/app.css'", '', - 'type CounterButtonProps = {', + 'type CounterProps = {', ' label: string', - ' onClick: (event: MouseEvent) => void', '}', '', - 'const CounterButton = ({ label, onClick }: CounterButtonProps) => (', - ' ', - ')', - '', - 'const App = () => {', + 'export const Counter = ({ label }: CounterProps) => {', + " const el = as HTMLButtonElement", ' let count = 0', - ' const handleClick = (event: MouseEvent) => {', + '', + ' el.onclick = () => {', ' count += 1', - ' const button = event.currentTarget as HTMLButtonElement', - ' button.textContent = `Clicks: ${count}`', - " button.dataset.active = count % 2 === 0 ? 'false' : 'true'", - " button.classList.toggle('is-even', count % 2 === 0)", + ' el.textContent = `${label}: ${count}`', + " el.dataset.active = count % 2 === 0 ? 'false' : 'true'", + " el.classList.toggle('is-even', count % 2 === 0)", ' }', '', - " return ", + ' return el', '}', '', ].join('\n') From ff15a3c2c8617ae2effcaaee815f1e13b0ce4a56 Mon Sep 17 00:00:00 2001 From: KCM Date: Sat, 1 Aug 2026 15:15:21 -0500 Subject: [PATCH 4/5] test: address failing specs. --- .../github-pr-drawer/open-pr-create.spec.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/playwright/github-pr-drawer/open-pr-create.spec.ts b/playwright/github-pr-drawer/open-pr-create.spec.ts index 8ed3c74..5aa0206 100644 --- a/playwright/github-pr-drawer/open-pr-create.spec.ts +++ b/playwright/github-pr-drawer/open-pr-create.spec.ts @@ -25,7 +25,7 @@ import { waitForAppReady, } from './github-pr-drawer.helpers.js' -test('Open PR drawer confirms and submits component/styles filepaths', async ({ +test('Open PR drawer confirms and submits default workspace filepaths', async ({ page, }) => { const customCommitMessage = 'chore: sync develop editor outputs' @@ -191,7 +191,14 @@ test('Open PR drawer confirms and submits component/styles filepaths', async ({ expect(createdRefPayload?.ref).toBe('refs/heads/Develop/Open-Pr-Test') expect(createdRefPayload?.sha).toBe('abc123mainsha') expect(treeRequests).toHaveLength(1) - expect((treeRequests[0]?.tree as Array>)?.length).toBe(2) + const submittedPaths = (treeRequests[0]?.tree as Array>).map( + entry => entry.path, + ) + expect(submittedPaths).toEqual([ + 'src/components/App.tsx', + 'src/components/Counter.tsx', + 'src/styles/app.css', + ]) expect(commitRequests).toHaveLength(1) expect(commitRequests[0]?.message).toBe(customCommitMessage) expect(updateRefRequests).toHaveLength(1) @@ -2083,7 +2090,14 @@ test('Open PR drawer uses Git Database API atomic commit path by default', async ) expect(treeRequests).toHaveLength(1) - expect((treeRequests[0]?.tree as Array>)?.length).toBe(2) + const submittedPaths = (treeRequests[0]?.tree as Array>).map( + entry => entry.path, + ) + expect(submittedPaths).toEqual([ + 'src/components/App.tsx', + 'src/components/Counter.tsx', + 'src/styles/app.css', + ]) expect(commitRequests).toHaveLength(1) expect(updateRefRequests).toHaveLength(1) expect(updateRefRequests[0]?.sha).toBe('new-commit-sha') From da975300403693c391b8755afae664f8f7db6922 Mon Sep 17 00:00:00 2001 From: KCM Date: Sat, 1 Aug 2026 15:24:45 -0500 Subject: [PATCH 5/5] test: address comments, add agents. --- playwright/AGENTS.md | 49 +++++++++++++++++++ .../github-pr-drawer/open-pr-create.spec.ts | 34 ++++++++----- src/modules/app-core/defaults.js | 6 ++- 3 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 playwright/AGENTS.md diff --git a/playwright/AGENTS.md b/playwright/AGENTS.md new file mode 100644 index 0000000..2787662 --- /dev/null +++ b/playwright/AGENTS.md @@ -0,0 +1,49 @@ +--- +name: knighted-develop-playwright-agent +description: Focused guidance for Playwright E2E authoring and triage in @knighted/develop. +--- + +You are working in @knighted/develop Playwright E2E tests. Keep feedback loops short and avoid full-suite reruns unless explicitly requested. + +## Scope + +- Folder: playwright/ +- Focus: test behavior, selectors, test stability, and fixture/setup correctness +- Keep changes minimal and localized to the failing behavior + +## Fast Failure Loop + +- Start with one browser at a time: Chromium first. +- Run one spec file before running broader groups. +- When possible, run only the failing test name(s). +- Do not run full Playwright shards locally unless explicitly requested. + +## Flake Triage + +- Check failure output for network/CDN/API timing flakes first. +- Retry flaky failures once. +- If the same assertion fails again, treat it as deterministic and fix code/tests. +- Prefer fixing stale expectations when product behavior intentionally changed. + +## Test Authoring Rules + +- Prefer semantic selectors: getByRole, getByLabel, getByText. +- Use explicit accessible names for interactive controls. +- Use locator() only when semantic selectors are not reliable. +- For known WebKit dialog issues, prefer a stable dialog id and evaluate-based click for dialog confirmation controls. + +## PR and Workspace Assertions + +- When asserting Git payloads, prefer verifying exact file paths/content over fragile counts when defaults may evolve. +- Keep assertions aligned with default workspace/tab contracts. + +## Validation Commands + +- Lint after JS/TS edits: npm run lint +- For Playwright changes, prefer targeted execution first. + +## Boundaries + +- Do not change build/import-map scripts unless required by the test task. +- Do not broaden CI scope or shard counts unless explicitly requested. +- Do not modify generated outputs or lockfiles unless explicitly requested. diff --git a/playwright/github-pr-drawer/open-pr-create.spec.ts b/playwright/github-pr-drawer/open-pr-create.spec.ts index 5aa0206..a05091b 100644 --- a/playwright/github-pr-drawer/open-pr-create.spec.ts +++ b/playwright/github-pr-drawer/open-pr-create.spec.ts @@ -191,14 +191,19 @@ test('Open PR drawer confirms and submits default workspace filepaths', async ({ expect(createdRefPayload?.ref).toBe('refs/heads/Develop/Open-Pr-Test') expect(createdRefPayload?.sha).toBe('abc123mainsha') expect(treeRequests).toHaveLength(1) - const submittedPaths = (treeRequests[0]?.tree as Array>).map( + const submittedTree = treeRequests[0]?.tree + expect(Array.isArray(submittedTree)).toBe(true) + const submittedPaths = (submittedTree as Array>).map( entry => entry.path, ) - expect(submittedPaths).toEqual([ - 'src/components/App.tsx', - 'src/components/Counter.tsx', - 'src/styles/app.css', - ]) + expect(submittedPaths).toHaveLength(3) + expect(submittedPaths).toEqual( + expect.arrayContaining([ + 'src/components/App.tsx', + 'src/components/Counter.tsx', + 'src/styles/app.css', + ]), + ) expect(commitRequests).toHaveLength(1) expect(commitRequests[0]?.message).toBe(customCommitMessage) expect(updateRefRequests).toHaveLength(1) @@ -2090,14 +2095,19 @@ test('Open PR drawer uses Git Database API atomic commit path by default', async ) expect(treeRequests).toHaveLength(1) - const submittedPaths = (treeRequests[0]?.tree as Array>).map( + const submittedTree = treeRequests[0]?.tree + expect(Array.isArray(submittedTree)).toBe(true) + const submittedPaths = (submittedTree as Array>).map( entry => entry.path, ) - expect(submittedPaths).toEqual([ - 'src/components/App.tsx', - 'src/components/Counter.tsx', - 'src/styles/app.css', - ]) + expect(submittedPaths).toHaveLength(3) + expect(submittedPaths).toEqual( + expect.arrayContaining([ + 'src/components/App.tsx', + 'src/components/Counter.tsx', + 'src/styles/app.css', + ]), + ) expect(commitRequests).toHaveLength(1) expect(updateRefRequests).toHaveLength(1) expect(updateRefRequests[0]?.sha).toBe('new-commit-sha') diff --git a/src/modules/app-core/defaults.js b/src/modules/app-core/defaults.js index eb8026d..e28f981 100644 --- a/src/modules/app-core/defaults.js +++ b/src/modules/app-core/defaults.js @@ -17,7 +17,11 @@ export const defaultModuleJsx = [ '}', '', 'export const Counter = ({ label }: CounterProps) => {', - " const el = as HTMLButtonElement", + ' const el = (', + " ', + ' ) as HTMLButtonElement', ' let count = 0', '', ' el.onclick = () => {',