Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
name: Playwright

on:
push:
pull_request:
branches:
- main
paths:
types:
- opened
- synchronize
paths: &playwright_paths
- 'src/**'
- 'playwright/**'
- 'package-lock.json'
- 'playwright.config.ts'
- '.github/workflows/playwright.yml'
push:
branches:
- main
paths: *playwright_paths
workflow_dispatch:

concurrency:
group: playwright-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
e2e:
name: ${{ matrix.jobName }}
Expand Down
135 changes: 132 additions & 3 deletions playwright/github-pr-drawer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
waitForAppReady,
} from './helpers/app-test-helpers.js'

const defaultCommitMessage = 'chore: sync editor updates from @knighted/develop'

const decodeGitHubFileBodyContent = (body: Record<string, unknown>) => {
const encoded = typeof body.content === 'string' ? body.content : ''
return Buffer.from(encoded, 'base64').toString('utf8')
Expand Down Expand Up @@ -80,6 +82,7 @@ const expectOpenPrConfirmationPrompt = async (page: Page) => {
test('Open PR drawer confirms and submits component/styles filepaths', async ({
page,
}) => {
const customCommitMessage = 'chore: sync develop editor outputs'
let createdRefBody: CreateRefRequestBody | null = null
const upsertRequests: Array<{ path: string; body: Record<string, unknown> }> = []
let pullRequestBody: PullRequestCreateBody | null = null
Expand Down Expand Up @@ -184,6 +187,7 @@ test('Open PR drawer confirms and submits component/styles filepaths', async ({
await page
.getByLabel('PR description')
.fill('Generated from editor content in @knighted/develop.')
await page.getByLabel('Commit message').fill(customCommitMessage)

Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new tests cover passing a custom commit message for both Open PR and Push Commit flows, but they don't verify the repository-scoped persistence requirement (commit message restored after closing/reopening the drawer and after a reload). Add a Playwright assertion that sets a commit message, reloads (or reopens the drawer), and confirms the value is prefilled as expected.

Suggested change
// Verify repository-scoped persistence of the commit message after a reload.
await page.reload()
await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)
await connectByotWithSingleRepo(page)
await ensureOpenPrDrawerOpen(page)
await expect(page.getByLabel('Commit message')).toHaveValue(customCommitMessage)

Copilot uses AI. Check for mistakes.
await submitOpenPrAndConfirm(page, {
expectedSummaryLines: [
Expand All @@ -208,6 +212,8 @@ test('Open PR drawer confirms and submits component/styles filepaths', async ({
expect(upsertRequests).toHaveLength(2)
expect(upsertRequests[0]?.path).toBe('examples/component/App.tsx')
expect(upsertRequests[1]?.path).toBe('examples/styles/app.css')
expect(upsertRequests[0]?.body.message).toBe(customCommitMessage)
expect(upsertRequests[1]?.body.message).toBe(customCommitMessage)
expect(pullRequestPayload?.head).toBe('Develop/Open-Pr-Test')
expect(pullRequestPayload?.base).toBe('main')

Expand All @@ -221,9 +227,9 @@ test('Open PR drawer confirms and submits component/styles filepaths', async ({
await expect(page.getByLabel('PR title')).toHaveValue(
'Apply editor updates from develop',
)
await expect(page.getByLabel('PR description')).toHaveValue(
'Generated from editor content in @knighted/develop.',
)
await expect(page.getByLabel('PR description')).toBeHidden()
await expect(page.getByLabel('Commit message')).toBeVisible()
await expect(page.getByLabel('Commit message')).toHaveValue(customCommitMessage)
await expect(
page.getByRole('button', { name: 'Push commit to active pull request branch' }),
).toBeVisible()
Expand All @@ -232,6 +238,19 @@ test('Open PR drawer confirms and submits component/styles filepaths', async ({
).toBeVisible()
})

test('Open PR drawer starts with empty title/description and short default head', async ({
page,
}) => {
await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)
await connectByotWithSingleRepo(page)
await ensureOpenPrDrawerOpen(page)

const headValue = await page.getByLabel('Head').inputValue()
expect(headValue).toMatch(/^feat\/component-[a-z0-9]{4}$/)
await expect(page.getByLabel('PR title')).toHaveValue('')
await expect(page.getByLabel('PR description')).toHaveValue('')
})

test('Open PR drawer base dropdown updates from mocked repo branches', async ({
page,
}) => {
Expand Down Expand Up @@ -678,6 +697,79 @@ test('Active PR context is disabled on load when pull request is closed', async
expect(isActivePr).toBe(false)
})

test('Active PR context recovers when saved head branch is missing but PR metadata exists', async ({
page,
}) => {
await page.route('https://api.github.com/user/repos**', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{
id: 11,
owner: { login: 'knightedcodemonkey' },
name: 'develop',
full_name: 'knightedcodemonkey/develop',
default_branch: 'main',
permissions: { push: true },
},
]),
})
})

await mockRepositoryBranches(page, {
'knightedcodemonkey/develop': ['main', 'release', 'develop/open-pr-test'],
})

await page.route(
'https://api.github.com/repos/knightedcodemonkey/develop/pulls/2',
async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
number: 2,
state: 'open',
title: 'Recovered PR context title',
html_url: 'https://github.com/knightedcodemonkey/develop/pull/2',
head: { ref: 'develop/open-pr-test' },
base: { ref: 'main' },
}),
})
},
)

await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)

await page.evaluate(() => {
localStorage.setItem(
'knighted:develop:github-pr-config:knightedcodemonkey/develop',
JSON.stringify({
componentFilePath: 'examples/component/App.tsx',
stylesFilePath: 'examples/styles/app.css',
renderMode: 'react',
baseBranch: 'main',
headBranch: '',
prTitle: 'Recovered PR context title',
prBody: 'Saved body',
isActivePr: true,
pullRequestNumber: 2,
pullRequestUrl: 'https://github.com/knightedcodemonkey/develop/pull/2',
}),
)
})

await connectByotWithSingleRepo(page)

await expect(
page.getByRole('button', { name: 'Push commit to active pull request branch' }),
).toBeVisible()

await ensureOpenPrDrawerOpen(page)
await expect(page.getByRole('button', { name: 'Push commit' }).last()).toBeVisible()
await expect(page.getByLabel('Head')).toHaveValue('develop/open-pr-test')
})

test('Active PR context uses Push commit flow without creating a new pull request', async ({
page,
}) => {
Expand Down Expand Up @@ -815,8 +907,34 @@ test('Active PR context uses Push commit flow without creating a new pull reques
await connectByotWithSingleRepo(page)
await ensureOpenPrDrawerOpen(page)

await expect(page.getByLabel('Pull request repository')).toBeDisabled()
await expect(page.getByLabel('Pull request base branch')).toBeDisabled()
await expect(page.getByLabel('Head')).toHaveJSProperty('readOnly', true)
await expect(page.getByLabel('Component filename')).toHaveJSProperty('readOnly', true)
await expect(page.getByLabel('Styles filename')).toHaveJSProperty('readOnly', true)
await expect(page.getByLabel('PR title')).toHaveJSProperty('readOnly', true)
await expect(
page.getByLabel('Include App wrapper in committed component source'),
).toBeEnabled()
await expect(page.getByLabel('Commit message')).toBeEditable()

await expect(page.getByLabel('PR description')).toBeHidden()
await expect(page.getByLabel('Commit message')).toBeVisible()

const includeWrapperToggle = page.getByLabel(
'Include App wrapper in committed component source',
)
await expect(includeWrapperToggle).toBeEnabled()
await includeWrapperToggle.check()
await expect(includeWrapperToggle).toBeChecked()
await expect(page.getByRole('button', { name: 'Push commit' }).last()).toBeVisible()
await expect(page.getByLabel('PR description')).toBeHidden()
await expect(page.getByLabel('Commit message')).toBeVisible()

await setComponentEditorSource(page, 'const commitMarker = 1')
await setStylesEditorSource(page, '.commit-marker { color: red; }')
const pushCommitMessage = 'chore: push active context sync'
await page.getByLabel('Commit message').fill(pushCommitMessage)

await page.getByRole('button', { name: 'Push commit' }).last().click()

Expand Down Expand Up @@ -846,6 +964,8 @@ test('Active PR context uses Push commit flow without creating a new pull reques
expect(upsertRequests).toHaveLength(2)
expect(upsertRequests[0]?.path).toBe('examples/component/App.tsx')
expect(upsertRequests[1]?.path).toBe('examples/styles/app.css')
expect(upsertRequests[0]?.body.message).toBe(pushCommitMessage)
expect(upsertRequests[1]?.body.message).toBe(pushCommitMessage)
})

test('Reloaded active PR context from URL metadata keeps Push mode and status reference', async ({
Expand Down Expand Up @@ -988,6 +1108,8 @@ test('Reloaded active PR context from URL metadata keeps Push mode and status re
await ensureOpenPrDrawerOpen(page)
await expect(page.getByRole('button', { name: 'Push commit' }).last()).toBeVisible()
await expect(page.getByLabel('Head')).toHaveValue('develop/open-pr-test')
await expect(page.getByLabel('PR description')).toBeHidden()
await expect(page.getByLabel('Commit message')).toBeVisible()

await setComponentEditorSource(page, 'const commitMarker = 1')
await setStylesEditorSource(page, '.commit-marker { color: red; }')
Expand All @@ -1007,6 +1129,8 @@ test('Reloaded active PR context from URL metadata keeps Push mode and status re
expect(upsertRequests).toHaveLength(2)
expect(upsertRequests[0]?.path).toBe('examples/component/App.tsx')
expect(upsertRequests[1]?.path).toBe('examples/styles/app.css')
expect(upsertRequests[0]?.body.message).toBe(defaultCommitMessage)
expect(upsertRequests[1]?.body.message).toBe(defaultCommitMessage)
})

test('Reloaded active PR context syncs editor content from GitHub branch', async ({
Expand Down Expand Up @@ -1151,6 +1275,7 @@ test('Open PR drawer validates unsafe filepaths', async ({ page }) => {
await ensureOpenPrDrawerOpen(page)

const componentPath = page.getByLabel('Component filename')
await page.getByLabel('PR title').fill('Validate unsafe paths')
await componentPath.fill('../outside/App.tsx')
await expect(componentPath).toHaveValue('../outside/App.tsx')
await componentPath.blur()
Expand All @@ -1176,6 +1301,7 @@ test('Open PR drawer allows dotted file segments that are not traversal', async
await stylesPath.fill('styles/foo..bar.css')
await expect(componentPath).toHaveValue('docs/v1.0..v1.1/App.tsx')
await expect(stylesPath).toHaveValue('styles/foo..bar.css')
await page.getByLabel('PR title').fill('Allow dotted file segments')
await stylesPath.blur()

await expectOpenPrConfirmationPrompt(page)
Expand All @@ -1190,6 +1316,7 @@ test('Open PR drawer rejects trailing slash file paths', async ({ page }) => {
await ensureOpenPrDrawerOpen(page)

await page.getByLabel('Component filename').fill('src/components/')
await page.getByLabel('PR title').fill('Reject trailing slash path')
await clickOpenPrDrawerSubmit(page)

await expect(
Expand Down Expand Up @@ -1325,6 +1452,7 @@ test('Open PR drawer strips App wrapper from committed component source by defau
await ensureOpenPrDrawerOpen(page)

await page.getByLabel('Head').fill('develop/repo/editor-sync-without-app')
await page.getByLabel('PR title').fill('Strip App wrapper by default')
await submitOpenPrAndConfirm(page)

await expect(
Expand Down Expand Up @@ -1455,6 +1583,7 @@ test('Open PR drawer includes App wrapper in committed source when toggled on',
await includeWrapperToggle.check()

await page.getByLabel('Head').fill('develop/repo/editor-sync-with-app')
await page.getByLabel('PR title').fill('Include App wrapper in commit')
await submitOpenPrAndConfirm(page)

await expect(
Expand Down
18 changes: 14 additions & 4 deletions playwright/helpers/app-test-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,19 @@ export type BranchesByRepo = Record<string, string[]>
export const waitForAppReady = async (page: Page, path = appEntryPath) => {
await page.goto(path)
await expect(page.getByRole('heading', { name: '@knighted/develop' })).toBeVisible()
await expect(page.locator('#cdn-loading')).toHaveAttribute('hidden', '')
await expect
.poll(() => page.getByRole('status', { name: 'App status' }).textContent())
.not.toBe('Idle')
.poll(async () => {
const statusText = (
await page.getByRole('status', { name: 'App status' }).textContent()
)?.trim()

return (
statusText === 'Rendered' ||
statusText?.startsWith('Rendered (Type errors:') ||
statusText === 'Error'
)
})
.toBe(true)
}

export const waitForInitialRender = async (page: Page) => {
Expand Down Expand Up @@ -215,7 +224,8 @@ export const connectByotWithSingleRepo = async (page: Page) => {
await page.getByRole('button', { name: 'Add GitHub token' }).click()

const repoSelect = page.getByLabel('Pull request repository')
await expect(repoSelect).toBeEnabled({ timeout: 60_000 })
await expect(repoSelect).toHaveValue('knightedcodemonkey/develop')

await expect(repoSelect).toHaveValue('knightedcodemonkey/develop')

await expect(
Expand Down
2 changes: 2 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const githubPrComponentPath = document.getElementById('github-pr-component-path'
const githubPrStylesPath = document.getElementById('github-pr-styles-path')
const githubPrTitle = document.getElementById('github-pr-title')
const githubPrBody = document.getElementById('github-pr-body')
const githubPrCommitMessage = document.getElementById('github-pr-commit-message')
const githubPrIncludeAppWrapper = document.getElementById('github-pr-include-app-wrapper')
const githubPrSubmit = document.getElementById('github-pr-submit')
const componentPrSyncIcon = document.getElementById('component-pr-sync-icon')
Expand Down Expand Up @@ -801,6 +802,7 @@ prDrawerController = createGitHubPrDrawer({
stylesPathInput: githubPrStylesPath,
prTitleInput: githubPrTitle,
prBodyInput: githubPrBody,
commitMessageInput: githubPrCommitMessage,
includeAppWrapperToggle: githubPrIncludeAppWrapper,
submitButton: githubPrSubmit,
titleNode: openPrTitle,
Expand Down
18 changes: 16 additions & 2 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ <h2 id="open-pr-title">Open Pull Request</h2>
type="text"
autocomplete="off"
spellcheck="false"
placeholder="develop/repo/editor-sync-yyyymmdd"
placeholder="feat/component-ab12"
/>
</label>

Expand Down Expand Up @@ -857,6 +857,20 @@ <h2 id="open-pr-title">Open Pull Request</h2>
placeholder="Describe the generated editor updates."
></textarea>
</label>

<label
class="github-pr-field github-pr-field--full"
for="github-pr-commit-message"
>
<span>Commit message</span>
<input
id="github-pr-commit-message"
type="text"
autocomplete="off"
spellcheck="false"
placeholder="chore: sync editor updates from @knighted/develop"
/>
</label>
</div>

<div class="github-pr-drawer__actions">
Expand All @@ -881,7 +895,7 @@ <h2 id="open-pr-title">Open Pull Request</h2>
<div class="cdn-loading" id="cdn-loading" role="status" aria-live="polite">
<div class="cdn-loading-card">
<div class="cdn-loading-spinner" aria-hidden="true"></div>
<p class="cdn-loading-title">Loading playground assets…</p>
<p class="cdn-loading-title">Loading IDE assets…</p>
<p class="cdn-loading-copy">
Fetching runtimes and compilers from CDN. This can take a moment.
</p>
Expand Down
Loading
Loading