GitHub workflows (#428)
Fixes #348 Fixes #274 Fixes #149 - Connect to existing repos - Push to other branches on GitHub besides main - Allows force push (with confirmation) dialog --------- Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
This commit is contained in:
145
e2e-tests/github.spec.ts
Normal file
145
e2e-tests/github.spec.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { test } from "./helpers/test_helper";
|
||||
|
||||
test("should connect to GitHub using device flow", async ({ po }) => {
|
||||
await po.setUp();
|
||||
await po.sendPrompt("tc=basic");
|
||||
|
||||
await po.getTitleBarAppNameButton().click();
|
||||
await po.githubConnector.connect();
|
||||
|
||||
// Wait for device flow to start and show the code
|
||||
await expect(po.page.locator("text=FAKE-CODE")).toBeVisible();
|
||||
|
||||
// Verify the verification URI is displayed
|
||||
await expect(
|
||||
po.page.locator("text=https://github.com/login/device"),
|
||||
).toBeVisible();
|
||||
|
||||
// Verify the "Set up your GitHub repo" section appears
|
||||
await expect(po.githubConnector.getSetupYourGitHubRepoButton()).toBeVisible();
|
||||
});
|
||||
|
||||
test("create and sync to new repo", async ({ po }) => {
|
||||
await po.setUp();
|
||||
await po.sendPrompt("tc=basic");
|
||||
|
||||
await po.getTitleBarAppNameButton().click();
|
||||
await po.githubConnector.connect();
|
||||
|
||||
// Verify "Create new repo" is selected by default
|
||||
await expect(po.githubConnector.getCreateNewRepoModeButton()).toHaveClass(
|
||||
/bg-primary/,
|
||||
);
|
||||
|
||||
await po.githubConnector.fillCreateRepoName("test-new-repo");
|
||||
|
||||
// Wait for availability check
|
||||
await po.page.waitForSelector("text=Repository name is available!", {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
// Click create repo button
|
||||
await po.githubConnector.clickCreateRepoButton();
|
||||
|
||||
// Snapshot post-creation state
|
||||
await po.githubConnector.snapshotConnectedRepo();
|
||||
|
||||
// Sync: capture success message
|
||||
await po.githubConnector.clickSyncToGithubButton();
|
||||
|
||||
await po.githubConnector.snapshotConnectedRepo();
|
||||
// Verify the push was received for the default branch (main)
|
||||
await po.githubConnector.verifyPushEvent({
|
||||
repo: "test-new-repo",
|
||||
branch: "main",
|
||||
operation: "create",
|
||||
});
|
||||
});
|
||||
|
||||
test("create and sync to new repo - custom branch", async ({ po }) => {
|
||||
await po.setUp();
|
||||
await po.sendPrompt("tc=basic");
|
||||
|
||||
await po.getTitleBarAppNameButton().click();
|
||||
await po.githubConnector.connect();
|
||||
|
||||
await po.githubConnector.fillCreateRepoName("test-new-repo");
|
||||
await po.githubConnector.fillNewRepoBranchName("new-branch");
|
||||
|
||||
// Click create repo button
|
||||
await po.githubConnector.clickCreateRepoButton();
|
||||
|
||||
// Sync to GitHub
|
||||
await po.githubConnector.clickSyncToGithubButton();
|
||||
|
||||
// Snapshot post-creation state
|
||||
await po.githubConnector.snapshotConnectedRepo();
|
||||
|
||||
// Verify the push was received for the correct custom branch
|
||||
await po.githubConnector.verifyPushEvent({
|
||||
repo: "test-new-repo",
|
||||
branch: "new-branch",
|
||||
operation: "create",
|
||||
});
|
||||
});
|
||||
|
||||
test("disconnect from repo", async ({ po }) => {
|
||||
await po.setUp();
|
||||
await po.sendPrompt("tc=basic");
|
||||
|
||||
await po.getTitleBarAppNameButton().click();
|
||||
await po.githubConnector.connect();
|
||||
|
||||
await po.githubConnector.fillCreateRepoName("test-new-repo");
|
||||
await po.githubConnector.clickCreateRepoButton();
|
||||
|
||||
await po.githubConnector.clickDisconnectRepoButton();
|
||||
await po.githubConnector.getSetupYourGitHubRepoButton().click();
|
||||
// Make this deterministic
|
||||
await po.githubConnector.fillCreateRepoName("[scrubbed]");
|
||||
await po.githubConnector.snapshotSetupRepo();
|
||||
});
|
||||
|
||||
test("create and sync to existing repo", async ({ po }) => {
|
||||
await po.setUp();
|
||||
await po.sendPrompt("tc=basic");
|
||||
|
||||
await po.getTitleBarAppNameButton().click();
|
||||
await po.githubConnector.connect();
|
||||
|
||||
await po.githubConnector.getConnectToExistingRepoModeButton().click();
|
||||
|
||||
await po.githubConnector.selectRepo("testuser/existing-app");
|
||||
await po.githubConnector.selectBranch("main");
|
||||
await po.githubConnector.clickConnectToRepoButton();
|
||||
|
||||
await po.githubConnector.snapshotConnectedRepo();
|
||||
});
|
||||
|
||||
test("create and sync to existing repo - custom branch", async ({ po }) => {
|
||||
// Clear any previous push events
|
||||
await po.githubConnector.clearPushEvents();
|
||||
|
||||
await po.setUp();
|
||||
await po.sendPrompt("tc=basic");
|
||||
|
||||
await po.getTitleBarAppNameButton().click();
|
||||
await po.githubConnector.connect();
|
||||
|
||||
await po.githubConnector.getConnectToExistingRepoModeButton().click();
|
||||
|
||||
await po.githubConnector.selectRepo("testuser/existing-app");
|
||||
await po.githubConnector.selectCustomBranch("new-branch");
|
||||
await po.githubConnector.clickConnectToRepoButton();
|
||||
|
||||
// Sync to GitHub to trigger a push
|
||||
await po.githubConnector.clickSyncToGithubButton();
|
||||
await po.githubConnector.snapshotConnectedRepo();
|
||||
// Verify the push was received for the correct custom branch
|
||||
await po.githubConnector.verifyPushEvent({
|
||||
repo: "existing-app",
|
||||
branch: "new-branch",
|
||||
operation: "create",
|
||||
});
|
||||
});
|
||||
@@ -64,21 +64,149 @@ class ProModesDialog {
|
||||
}
|
||||
}
|
||||
|
||||
class GitHubConnector {
|
||||
constructor(public page: Page) {}
|
||||
|
||||
async connect() {
|
||||
await this.page.getByRole("button", { name: "Connect to GitHub" }).click();
|
||||
}
|
||||
|
||||
getSetupYourGitHubRepoButton() {
|
||||
return this.page.getByText("Set up your GitHub repo");
|
||||
}
|
||||
|
||||
getCreateNewRepoModeButton() {
|
||||
return this.page.getByRole("button", { name: "Create new repo" });
|
||||
}
|
||||
|
||||
getConnectToExistingRepoModeButton() {
|
||||
return this.page.getByRole("button", { name: "Connect to existing repo" });
|
||||
}
|
||||
|
||||
async clickCreateRepoButton() {
|
||||
await this.page.getByRole("button", { name: "Create Repo" }).click();
|
||||
}
|
||||
|
||||
async fillCreateRepoName(name: string) {
|
||||
await this.page.getByTestId("github-create-repo-name-input").fill(name);
|
||||
}
|
||||
|
||||
async fillNewRepoBranchName(name: string) {
|
||||
await this.page.getByTestId("github-new-repo-branch-input").fill(name);
|
||||
}
|
||||
|
||||
async selectRepo(repo: string) {
|
||||
await this.page.getByTestId("github-repo-select").click();
|
||||
await this.page.getByRole("option", { name: repo }).click();
|
||||
}
|
||||
|
||||
async selectBranch(branch: string) {
|
||||
await this.page.getByTestId("github-branch-select").click();
|
||||
await this.page.getByRole("option", { name: branch }).click();
|
||||
}
|
||||
|
||||
async selectCustomBranch(branch: string) {
|
||||
await this.page.getByTestId("github-branch-select").click();
|
||||
await this.page
|
||||
.getByRole("option", { name: "✏️ Type custom branch name" })
|
||||
.click();
|
||||
await this.page.getByTestId("github-custom-branch-input").click();
|
||||
await this.page.getByTestId("github-custom-branch-input").fill(branch);
|
||||
}
|
||||
|
||||
async clickConnectToRepoButton() {
|
||||
await this.page.getByRole("button", { name: "Connect to repo" }).click();
|
||||
}
|
||||
|
||||
async snapshotConnectedRepo() {
|
||||
await expect(
|
||||
this.page.getByTestId("github-connected-repo"),
|
||||
).toMatchAriaSnapshot();
|
||||
}
|
||||
|
||||
async snapshotSetupRepo() {
|
||||
await expect(
|
||||
this.page.getByTestId("github-setup-repo"),
|
||||
).toMatchAriaSnapshot();
|
||||
}
|
||||
|
||||
async snapshotUnconnectedRepo() {
|
||||
await expect(
|
||||
this.page.getByTestId("github-unconnected-repo"),
|
||||
).toMatchAriaSnapshot();
|
||||
}
|
||||
|
||||
async clickSyncToGithubButton() {
|
||||
await this.page.getByRole("button", { name: "Sync to GitHub" }).click();
|
||||
}
|
||||
|
||||
async clickDisconnectRepoButton() {
|
||||
await this.page
|
||||
.getByRole("button", { name: "Disconnect from repo" })
|
||||
.click();
|
||||
}
|
||||
|
||||
async clearPushEvents() {
|
||||
const response = await this.page.request.post(
|
||||
"http://localhost:3500/github/api/test/clear-push-events",
|
||||
);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async getPushEvents(repo?: string) {
|
||||
const url = repo
|
||||
? `http://localhost:3500/github/api/test/push-events?repo=${repo}`
|
||||
: "http://localhost:3500/github/api/test/push-events";
|
||||
const response = await this.page.request.get(url);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async verifyPushEvent(expectedEvent: {
|
||||
repo: string;
|
||||
branch: string;
|
||||
operation?: "push" | "create" | "delete";
|
||||
}) {
|
||||
const pushEvents = await this.getPushEvents(expectedEvent.repo);
|
||||
const matchingEvent = pushEvents.find(
|
||||
(event: any) =>
|
||||
event.repo === expectedEvent.repo &&
|
||||
event.branch === expectedEvent.branch &&
|
||||
(!expectedEvent.operation ||
|
||||
event.operation === expectedEvent.operation),
|
||||
);
|
||||
|
||||
if (!matchingEvent) {
|
||||
throw new Error(
|
||||
`Expected push event not found. Expected: ${JSON.stringify(expectedEvent)}. ` +
|
||||
`Actual events: ${JSON.stringify(pushEvents)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return matchingEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export class PageObject {
|
||||
private userDataDir: string;
|
||||
|
||||
public githubConnector: GitHubConnector;
|
||||
constructor(
|
||||
public electronApp: ElectronApplication,
|
||||
public page: Page,
|
||||
{ userDataDir }: { userDataDir: string },
|
||||
) {
|
||||
this.userDataDir = userDataDir;
|
||||
this.githubConnector = new GitHubConnector(this.page);
|
||||
}
|
||||
|
||||
private async baseSetup() {
|
||||
await this.githubConnector.clearPushEvents();
|
||||
}
|
||||
|
||||
async setUp({
|
||||
autoApprove = false,
|
||||
nativeGit = false,
|
||||
}: { autoApprove?: boolean; nativeGit?: boolean } = {}) {
|
||||
await this.baseSetup();
|
||||
await this.goToSettingsTab();
|
||||
if (autoApprove) {
|
||||
await this.toggleAutoApprove();
|
||||
@@ -93,16 +221,8 @@ export class PageObject {
|
||||
await this.selectTestModel();
|
||||
}
|
||||
|
||||
async importApp(appDir: string) {
|
||||
await this.page.getByRole("button", { name: "Import App" }).click();
|
||||
await eph.stubDialog(this.electronApp, "showOpenDialog", {
|
||||
filePaths: [path.join(__dirname, "..", "fixtures", "import-app", appDir)],
|
||||
});
|
||||
await this.page.getByRole("button", { name: "Select Folder" }).click();
|
||||
await this.page.getByRole("button", { name: "Import" }).click();
|
||||
}
|
||||
|
||||
async setUpDyadPro({ autoApprove = false }: { autoApprove?: boolean } = {}) {
|
||||
await this.baseSetup();
|
||||
await this.goToSettingsTab();
|
||||
if (autoApprove) {
|
||||
await this.toggleAutoApprove();
|
||||
@@ -112,7 +232,6 @@ export class PageObject {
|
||||
}
|
||||
|
||||
async setUpDyadProvider() {
|
||||
// await page.getByRole('link', { name: 'Settings' }).click();
|
||||
await this.page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^DyadNeeds Setup$/ })
|
||||
@@ -123,12 +242,15 @@ export class PageObject {
|
||||
.getByRole("textbox", { name: "Set Dyad API Key" })
|
||||
.fill("testdyadkey");
|
||||
await this.page.getByRole("button", { name: "Save Key" }).click();
|
||||
// await page.getByRole('link', { name: 'Apps' }).click();
|
||||
// await page.getByTestId('home-chat-input-container').getByRole('button', { name: 'Pro' }).click();
|
||||
// await page.getByRole('switch', { name: 'Turbo Edits' }).click();
|
||||
// await page.getByRole('switch', { name: 'Turbo Edits' }).click();
|
||||
// await page.locator('div').filter({ hasText: /^Import App$/ }).click();
|
||||
// await page.getByRole('button', { name: 'Select Folder' }).press('Escape');
|
||||
}
|
||||
|
||||
async importApp(appDir: string) {
|
||||
await this.page.getByRole("button", { name: "Import App" }).click();
|
||||
await eph.stubDialog(this.electronApp, "showOpenDialog", {
|
||||
filePaths: [path.join(__dirname, "..", "fixtures", "import-app", appDir)],
|
||||
});
|
||||
await this.page.getByRole("button", { name: "Select Folder" }).click();
|
||||
await this.page.getByRole("button", { name: "Import" }).click();
|
||||
}
|
||||
|
||||
async openContextFilesPicker() {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
- paragraph: "Connected to GitHub Repo:"
|
||||
- text: testuser/existing-app
|
||||
- paragraph: "Branch: new-branch"
|
||||
- button "Sync to GitHub"
|
||||
- button "Disconnect from repo"
|
||||
@@ -0,0 +1,5 @@
|
||||
- paragraph: "Connected to GitHub Repo:"
|
||||
- text: testuser/existing-app
|
||||
- paragraph: "Branch: main"
|
||||
- button "Sync to GitHub"
|
||||
- button "Disconnect from repo"
|
||||
@@ -0,0 +1,5 @@
|
||||
- paragraph: "Connected to GitHub Repo:"
|
||||
- text: testuser/test-new-repo
|
||||
- paragraph: "Branch: new-branch"
|
||||
- button "Sync to GitHub"
|
||||
- button "Disconnect from repo"
|
||||
@@ -0,0 +1,5 @@
|
||||
- paragraph: "Connected to GitHub Repo:"
|
||||
- text: testuser/test-new-repo
|
||||
- paragraph: "Branch: main"
|
||||
- button "Sync to GitHub"
|
||||
- button "Disconnect from repo"
|
||||
@@ -0,0 +1,6 @@
|
||||
- paragraph: "Connected to GitHub Repo:"
|
||||
- text: testuser/test-new-repo
|
||||
- paragraph: "Branch: main"
|
||||
- button "Sync to GitHub"
|
||||
- button "Disconnect from repo"
|
||||
- paragraph: Successfully pushed to GitHub!
|
||||
@@ -0,0 +1,10 @@
|
||||
- button "Set up your GitHub repo":
|
||||
- img
|
||||
- button "Create new repo"
|
||||
- button "Connect to existing repo"
|
||||
- text: Repository Name
|
||||
- textbox: "[scrubbed]"
|
||||
- paragraph: Repository name is available!
|
||||
- text: Branch
|
||||
- textbox "main"
|
||||
- button "Create Repo"
|
||||
@@ -0,0 +1,5 @@
|
||||
- paragraph: "Connected to GitHub Repo:"
|
||||
- text: testuser/test-new-repo
|
||||
- paragraph: "Branch: main"
|
||||
- button "Sync to GitHub"
|
||||
- button "Disconnect from repo"
|
||||
@@ -0,0 +1,6 @@
|
||||
- paragraph: "Connected to GitHub Repo:"
|
||||
- text: testuser/test-new-repo
|
||||
- paragraph: "Branch: main"
|
||||
- button "Sync to GitHub"
|
||||
- button "Disconnect from repo"
|
||||
- paragraph: Successfully pushed to GitHub!
|
||||
Reference in New Issue
Block a user