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