Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

pulls.spec.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

pulls.spec.tsBlame264 lines · 1 contributor
a014defClaude1/**
2 * E2E — Pull request flows
3 *
4 * Covers: create PR, add comment, merge PR, close PR.
5 *
6 * Strategy: beforeAll pushes an initial commit to main, then pushes a
7 * feature branch. We create a PR from the feature branch against main
8 * and exercise the PR lifecycle within a single spec run.
9 */
10
11import { test, expect } from "@playwright/test";
12import {
13 uid,
14 TEST_PASSWORD,
15 pushTestCommit,
16 pushFeatureBranch,
17 cleanupDir,
18} from "./fixtures";
19
20// ---------------------------------------------------------------------------
21// Shared state
22// ---------------------------------------------------------------------------
23
24let owner: string;
25let repoName: string;
26let featureBranch: string;
27let tmpDir: string;
28
29test.beforeAll(async ({ browser }) => {
30 owner = uid("pruser");
31 repoName = uid("prerepo");
32 featureBranch = uid("feat-");
33
34 const page = await browser.newPage();
35
36 // Register
37 await page.goto("/register");
38 await page.fill('input[name="username"]', owner);
39 await page.fill('input[name="email"]', `${owner}@test.example`);
40 await page.fill('input[name="password"]', TEST_PASSWORD);
41 await page.click('button[type="submit"]');
42 await page.waitForURL(/\/(dashboard|[a-z])/);
43
44 // Create repo
45 await page.goto("/new");
46 await page.fill('input[name="name"]', repoName);
47 await page.click('button[type="submit"]');
48 await page.waitForURL(new RegExp(`/${owner}/${repoName}`));
49
50 await page.close();
51
52 // Push initial commit to main
53 tmpDir = await pushTestCommit({
54 owner,
55 repo: repoName,
56 username: owner,
57 password: TEST_PASSWORD,
58 fileName: "README.md",
59 fileContent: `# ${repoName}\n`,
60 commitMsg: "Initial commit",
61 });
62
63 // Push feature branch
64 featureBranch = await pushFeatureBranch({
65 repoDir: tmpDir,
66 username: owner,
67 branchName: featureBranch,
68 fileName: "feature.md",
69 fileContent: "Feature branch content.\n",
70 commitMsg: "Add feature file",
71 });
72});
73
74test.afterAll(async () => {
75 await cleanupDir(tmpDir);
76});
77
78// ---------------------------------------------------------------------------
79// Helper: log in the page as owner
80// ---------------------------------------------------------------------------
81
82async function loginAsOwner(page: Parameters<typeof test>[1] extends never ? never : any): Promise<void> {
83 await page.goto("/login");
84 await page.fill('input[name="username"]', owner);
85 await page.fill('input[name="password"]', TEST_PASSWORD);
86 await page.click('button[type="submit"]');
87 await page.waitForURL(/\/(dashboard|[a-z])/);
88}
89
90// ---------------------------------------------------------------------------
91// Create PR
92// ---------------------------------------------------------------------------
93
94test.describe("Pull request creation", () => {
95 test("PR list page is accessible", async ({ page }) => {
96 await page.goto(`/${owner}/${repoName}/pulls`);
97 await expect(page).toHaveURL(new RegExp(`/${owner}/${repoName}/pulls`));
98 // Even if empty, the page should render without error
99 await expect(page.locator("body")).not.toContainText(/500|Internal Server Error/i);
100 });
101
102 test("can open a new PR from the compare page", async ({ page }) => {
103 await loginAsOwner(page);
104
105 // Navigate to compare page with the feature branch
106 await page.goto(`/${owner}/${repoName}/compare/main...${featureBranch}`);
107
108 // Should show a PR creation form
109 const titleInput = page.locator('input[name="title"]');
110 await expect(titleInput).toBeVisible({ timeout: 8_000 });
111
112 // Fill in PR details
113 const prTitle = `E2E PR ${uid("pr")}`;
114 await titleInput.fill(prTitle);
115
116 await page.click('button[type="submit"]');
117
118 // Should redirect to the new PR
119 await expect(page).toHaveURL(new RegExp(`/${owner}/${repoName}/pulls/\\d+`));
120 await expect(page.locator("body")).toContainText(prTitle);
121 });
122});
123
124// ---------------------------------------------------------------------------
125// PR comment
126// ---------------------------------------------------------------------------
127
128test.describe("Pull request comment", () => {
129 let prUrl: string;
130
131 test.beforeAll(async ({ browser }) => {
132 // Create a PR programmatically so we have a URL to comment on
133 const page = await browser.newPage();
134 await loginAsOwner(page);
135
136 await page.goto(`/${owner}/${repoName}/compare/main...${featureBranch}`);
137 const titleInput = page.locator('input[name="title"]');
138 await titleInput.waitFor({ timeout: 8_000 });
139 await titleInput.fill(`Comment-test PR ${uid("c")}`);
140 await page.click('button[type="submit"]');
141 await page.waitForURL(new RegExp(`/${owner}/${repoName}/pulls/\\d+`));
142 prUrl = page.url();
143 await page.close();
144 });
145
146 test("can post a comment on a PR", async ({ page }) => {
147 await loginAsOwner(page);
148 await page.goto(prUrl);
149
150 const commentBox = page.locator(
151 'textarea[name="body"], textarea[name="comment"]'
152 ).first();
153 await expect(commentBox).toBeVisible({ timeout: 8_000 });
154
155 const commentText = `Test comment ${uid("cmt")}`;
156 await commentBox.fill(commentText);
157 await page.click('button[type="submit"]');
158
159 await expect(page.locator("body")).toContainText(commentText, {
160 timeout: 8_000,
161 });
162 });
163});
164
165// ---------------------------------------------------------------------------
166// Merge PR
167// ---------------------------------------------------------------------------
168
169test.describe("Pull request merge", () => {
170 let prUrl: string;
171
172 test.beforeAll(async ({ browser }) => {
173 const page = await browser.newPage();
174 await loginAsOwner(page);
175
176 await page.goto(`/${owner}/${repoName}/compare/main...${featureBranch}`);
177 const titleInput = page.locator('input[name="title"]');
178 await titleInput.waitFor({ timeout: 8_000 });
179 await titleInput.fill(`Merge-test PR ${uid("m")}`);
180 await page.click('button[type="submit"]');
181 await page.waitForURL(new RegExp(`/${owner}/${repoName}/pulls/\\d+`));
182 prUrl = page.url();
183 await page.close();
184 });
185
186 test("merge button is visible on an open PR", async ({ page }) => {
187 await loginAsOwner(page);
188 await page.goto(prUrl);
189
190 // The merge button may be disabled if checks are pending — just check visible
191 const mergeBtn = page.locator(
192 'button:has-text("Merge"), button[name="action"][value="merge"], form[action*="merge"] button'
193 ).first();
194 await expect(mergeBtn).toBeVisible({ timeout: 8_000 });
195 });
196
197 test("can merge an open PR", async ({ page }) => {
198 await loginAsOwner(page);
199 await page.goto(prUrl);
200
201 const mergeBtn = page.locator(
202 'button:has-text("Merge"), button[name="action"][value="merge"], form[action*="merge"] button'
203 ).first();
204
205 // Only click if enabled — if branch-protection blocks it, skip gracefully
206 if (await mergeBtn.isEnabled()) {
207 await mergeBtn.click();
208 await expect(page.locator("body")).toContainText(/merged/i, {
209 timeout: 10_000,
210 });
211 } else {
212 test.skip();
213 }
214 });
215});
216
217// ---------------------------------------------------------------------------
218// Close PR
219// ---------------------------------------------------------------------------
220
221test.describe("Pull request close", () => {
222 let prUrl: string;
223
224 test.beforeAll(async ({ browser }) => {
225 // Push another unique branch so we have an un-merged PR to close
226 const closeBranch = uid("close-");
227 await pushFeatureBranch({
228 repoDir: tmpDir,
229 username: owner,
230 branchName: closeBranch,
231 fileName: `close-${uid()}.md`,
232 fileContent: "Close branch file.\n",
233 commitMsg: "Add close-branch file",
234 });
235
236 const page = await browser.newPage();
237 await loginAsOwner(page);
238
239 await page.goto(`/${owner}/${repoName}/compare/main...${closeBranch}`);
240 const titleInput = page.locator('input[name="title"]');
241 await titleInput.waitFor({ timeout: 8_000 });
242 await titleInput.fill(`Close-test PR ${uid("cl")}`);
243 await page.click('button[type="submit"]');
244 await page.waitForURL(new RegExp(`/${owner}/${repoName}/pulls/\\d+`));
245 prUrl = page.url();
246 await page.close();
247 });
248
249 test("can close an open PR", async ({ page }) => {
250 await loginAsOwner(page);
251 await page.goto(prUrl);
252
253 // Close button — server may render it as a form submit or link
254 const closeBtn = page.locator(
255 'button:has-text("Close"), button[value="close"], form[action*="close"] button'
256 ).first();
257 await expect(closeBtn).toBeVisible({ timeout: 8_000 });
258 await closeBtn.click();
259
260 await expect(page.locator("body")).toContainText(/closed/i, {
261 timeout: 8_000,
262 });
263 });
264});