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

issues.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.

issues.spec.tsBlame235 lines · 1 contributor
a014defClaude1/**
2 * E2E — Issue tracker flows
3 *
4 * Covers: create issue, add comment, close issue, label management.
5 */
6
7import { test, expect } from "@playwright/test";
8import {
9 uid,
10 TEST_PASSWORD,
11 pushTestCommit,
12 cleanupDir,
13} from "./fixtures";
14
15// ---------------------------------------------------------------------------
16// Shared state — one user + one repo, one issue URL reused across suites.
17// ---------------------------------------------------------------------------
18
19let owner: string;
20let repoName: string;
21let tmpDir: string;
22/** URL of an issue we create in beforeAll for the comment/close/label tests. */
23let sharedIssueUrl: string;
24
25test.beforeAll(async ({ browser }) => {
26 owner = uid("issueuser");
27 repoName = uid("issuerepo");
28
29 const page = await browser.newPage();
30
31 // Register
32 await page.goto("/register");
33 await page.fill('input[name="username"]', owner);
34 await page.fill('input[name="email"]', `${owner}@test.example`);
35 await page.fill('input[name="password"]', TEST_PASSWORD);
36 await page.click('button[type="submit"]');
37 await page.waitForURL(/\/(dashboard|[a-z])/);
38
39 // Create repo
40 await page.goto("/new");
41 await page.fill('input[name="name"]', repoName);
42 await page.click('button[type="submit"]');
43 await page.waitForURL(new RegExp(`/${owner}/${repoName}`));
44
45 await page.close();
46
47 // Push a commit so the repo isn't empty (some routes need an existing HEAD)
48 tmpDir = await pushTestCommit({
49 owner,
50 repo: repoName,
51 username: owner,
52 password: TEST_PASSWORD,
53 fileName: "README.md",
54 fileContent: `# ${repoName}\n`,
55 commitMsg: "Initial commit",
56 });
57
58 // Create a shared issue via the web UI
59 const page2 = await browser.newPage();
60 await page2.goto("/login");
61 await page2.fill('input[name="username"]', owner);
62 await page2.fill('input[name="password"]', TEST_PASSWORD);
63 await page2.click('button[type="submit"]');
64 await page2.waitForURL(/\/(dashboard|[a-z])/);
65
66 await page2.goto(`/${owner}/${repoName}/issues/new`);
67 const titleInput = page2.locator('input[name="title"]');
68 await titleInput.waitFor({ timeout: 8_000 });
69 await titleInput.fill(`Shared issue ${uid("iss")}`);
70
71 const bodyArea = page2.locator('textarea[name="body"]');
72 if (await bodyArea.isVisible()) {
73 await bodyArea.fill("Issue body for shared E2E issue.");
74 }
75
76 await page2.click('button[type="submit"]');
77 await page2.waitForURL(new RegExp(`/${owner}/${repoName}/issues/\\d+`));
78 sharedIssueUrl = page2.url();
79 await page2.close();
80});
81
82test.afterAll(async () => {
83 await cleanupDir(tmpDir);
84});
85
86// ---------------------------------------------------------------------------
87// Helper
88// ---------------------------------------------------------------------------
89
90async function loginAsOwner(page: any): Promise<void> {
91 await page.goto("/login");
92 await page.fill('input[name="username"]', owner);
93 await page.fill('input[name="password"]', TEST_PASSWORD);
94 await page.click('button[type="submit"]');
95 await page.waitForURL(/\/(dashboard|[a-z])/);
96}
97
98// ---------------------------------------------------------------------------
99// Create issue
100// ---------------------------------------------------------------------------
101
102test.describe("Issue creation", () => {
103 test("issues list page is accessible", async ({ page }) => {
104 await page.goto(`/${owner}/${repoName}/issues`);
105 await expect(page).toHaveURL(new RegExp(`/${owner}/${repoName}/issues`));
106 await expect(page.locator("body")).not.toContainText(/500|Internal Server Error/i);
107 });
108
109 test("can open new issue form", async ({ page }) => {
110 await loginAsOwner(page);
111 await page.goto(`/${owner}/${repoName}/issues/new`);
112 await expect(page.locator('input[name="title"]')).toBeVisible({ timeout: 8_000 });
113 });
114
115 test("can create a new issue", async ({ page }) => {
116 await loginAsOwner(page);
117 await page.goto(`/${owner}/${repoName}/issues/new`);
118
119 const titleInput = page.locator('input[name="title"]');
120 await titleInput.waitFor({ timeout: 8_000 });
121
122 const issueTitle = `New issue ${uid("ni")}`;
123 await titleInput.fill(issueTitle);
124
125 const bodyArea = page.locator('textarea[name="body"]');
126 if (await bodyArea.isVisible()) {
127 await bodyArea.fill("This is an E2E-created issue.");
128 }
129
130 await page.click('button[type="submit"]');
131
132 // Should redirect to the issue detail page
133 await expect(page).toHaveURL(
134 new RegExp(`/${owner}/${repoName}/issues/\\d+`)
135 );
136 await expect(page.locator("body")).toContainText(issueTitle);
137 });
138
139 test("issue appears in the issues list", async ({ page }) => {
140 await page.goto(`/${owner}/${repoName}/issues`);
141 // The shared issue created in beforeAll should be listed
142 await expect(page.locator("body")).toContainText(/Shared issue|issue/i, {
143 timeout: 8_000,
144 });
145 });
146});
147
148// ---------------------------------------------------------------------------
149// Comment on issue
150// ---------------------------------------------------------------------------
151
152test.describe("Issue comments", () => {
153 test("can post a comment on an issue", async ({ page }) => {
154 await loginAsOwner(page);
155 await page.goto(sharedIssueUrl);
156
157 const commentBox = page.locator(
158 'textarea[name="body"], textarea[name="comment"]'
159 ).first();
160 await expect(commentBox).toBeVisible({ timeout: 8_000 });
161
162 const commentText = `E2E comment ${uid("cm")}`;
163 await commentBox.fill(commentText);
164 await page.click('button[type="submit"]');
165
166 await expect(page.locator("body")).toContainText(commentText, {
167 timeout: 8_000,
168 });
169 });
170});
171
172// ---------------------------------------------------------------------------
173// Close issue
174// ---------------------------------------------------------------------------
175
176test.describe("Issue lifecycle", () => {
177 let closeIssueUrl: string;
178
179 test.beforeAll(async ({ browser }) => {
180 // Create a fresh issue just for the close test
181 const page = await browser.newPage();
182 await loginAsOwner(page);
183
184 await page.goto(`/${owner}/${repoName}/issues/new`);
185 const titleInput = page.locator('input[name="title"]');
186 await titleInput.waitFor({ timeout: 8_000 });
187 await titleInput.fill(`Close-me issue ${uid("cl")}`);
188 await page.click('button[type="submit"]');
189 await page.waitForURL(new RegExp(`/${owner}/${repoName}/issues/\\d+`));
190 closeIssueUrl = page.url();
191 await page.close();
192 });
193
194 test("can close an open issue", async ({ page }) => {
195 await loginAsOwner(page);
196 await page.goto(closeIssueUrl);
197
198 const closeBtn = page.locator(
199 'button:has-text("Close"), button[value="close"], form[action*="close"] button'
200 ).first();
201 await expect(closeBtn).toBeVisible({ timeout: 8_000 });
202 await closeBtn.click();
203
204 await expect(page.locator("body")).toContainText(/closed/i, {
205 timeout: 8_000,
206 });
207 });
208
209 test("can reopen a closed issue", async ({ page }) => {
210 await loginAsOwner(page);
211 await page.goto(closeIssueUrl);
212
213 const reopenBtn = page.locator(
214 'button:has-text("Reopen"), button[value="reopen"], form[action*="reopen"] button'
215 ).first();
216 await expect(reopenBtn).toBeVisible({ timeout: 8_000 });
217 await reopenBtn.click();
218
219 await expect(page.locator("body")).toContainText(/open/i, {
220 timeout: 8_000,
221 });
222 });
223});
224
225// ---------------------------------------------------------------------------
226// Labels
227// ---------------------------------------------------------------------------
228
229test.describe("Issue labels", () => {
230 test("labels page is accessible", async ({ page }) => {
231 await page.goto(`/${owner}/${repoName}/labels`);
232 // Page should load (may be empty if no labels yet)
233 await expect(page.locator("body")).not.toContainText(/500|Internal Server Error/i);
234 });
235});