e2e.labels.test.ts
Raw
1import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
2import { chromium } from 'playwright';
3import type { Browser, BrowserContext } from 'playwright';
4import {
5 BASE,
6 ADMIN_PASS,
7 setupTestEnv,
8 spawnServer,
9 killServer,
10 login,
11 writeTempFile,
12} from './helpers.ts';
13
14let browser: Browser;
15let server: Awaited<ReturnType<typeof spawnServer>>;
16
17beforeAll(async () => {
18 await setupTestEnv();
19 server = await spawnServer();
20 browser = await chromium.launch();
21
22 // Register alice (needed for user label management)
23 const regCtx = await browser.newContext();
24 const regPage = await regCtx.newPage();
25 try {
26 await regPage.goto(`${BASE}/register`);
27 await regPage.fill('[name=username]', 'alice');
28 await regPage.fill('[name=password]', 'password123');
29 await regPage.fill('[name=password2]', 'password123');
30 await regPage.click('button[type=submit]');
31 await regPage.waitForURL(BASE + '/');
32 } finally { await regCtx.close(); }
33});
34
35afterAll(async () => {
36 await browser.close();
37 await killServer(server);
38});
39
40async function loggedInContext(username = 'admin', password = ADMIN_PASS) {
41 const ctx = await browser.newContext();
42 const page = await ctx.newPage();
43 await login(page, username, password);
44 await page.close();
45 return ctx;
46}
47
48// ─── Labels ───────────────────────────────────────────────────────────────────
49
50describe('labels', () => {
51 let adminCtx: BrowserContext;
52 let issueUrl: string;
53 let patchUrl: string;
54
55 const VALID_PATCH = [
56 'From a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 Mon Sep 17 00:00:00 2001',
57 'From: Test User <test@example.com>',
58 'Date: Mon, 01 Jan 2024 12:00:00 +0000',
59 'Subject: [PATCH] Add label-test.txt',
60 '',
61 '---',
62 'diff --git a/label-test.txt b/label-test.txt',
63 'new file mode 100644',
64 'index 0000000..9daeafb',
65 '--- /dev/null',
66 '+++ b/label-test.txt',
67 '@@ -0,0 +1 @@',
68 '+x',
69 '',
70 ].join('\n');
71
72 beforeAll(async () => {
73 adminCtx = await loggedInContext();
74
75 // Create a dedicated repo for label tests
76 const page = await adminCtx.newPage();
77 try {
78 await page.goto(`${BASE}/new`);
79 await page.fill('[name=name]', 'label-repo');
80 await page.click('form[action="/new"] button[type=submit]');
81 await page.waitForURL(`${BASE}/label-repo`);
82 } finally { await page.close(); }
83
84 // Create an issue
85 const issuePage = await adminCtx.newPage();
86 try {
87 await issuePage.goto(`${BASE}/label-repo/issues/new`);
88 await issuePage.fill('[name=title]', 'Labelled issue');
89 await issuePage.click('form[action$="/issues"] button[type=submit]');
90 await issuePage.waitForURL(/\/label-repo\/issues\/\d+/);
91 issueUrl = issuePage.url();
92 } finally { await issuePage.close(); }
93
94 // Create a patch
95 writeTempFile('/tmp/label-test.patch', VALID_PATCH);
96 const patchPage = await adminCtx.newPage();
97 try {
98 await patchPage.goto(`${BASE}/label-repo/patches/new`);
99 await patchPage.fill('[name=title]', 'Labelled patch');
100 await patchPage.locator('[name=patch_file]').setInputFiles('/tmp/label-test.patch');
101 await patchPage.click('form[action$="/patches"] button[type=submit]');
102 await patchPage.waitForURL(/\/label-repo\/patches\/\d+/);
103 patchUrl = patchPage.url();
104 } finally { await patchPage.close(); }
105 });
106
107 afterAll(async () => { await adminCtx.close(); });
108
109 test('create label in repo settings', async () => {
110 const page = await adminCtx.newPage();
111 try {
112 await page.goto(`${BASE}/label-repo/settings`);
113 await page.fill('input[name=name]', 'bug');
114 await page.locator('input[type=color][name=color]').evaluate(
115 (el: any) => { el.value = '#ff0000'; },
116 );
117 await page.click('form[action$="/settings/labels"] button[type=submit]');
118 await page.waitForURL(/\/label-repo\/settings/);
119 expect(await page.locator('.label-settings-name').allTextContents()).toContain('bug');
120 } finally { await page.close(); }
121 });
122
123 test('create a second label', async () => {
124 const page = await adminCtx.newPage();
125 try {
126 await page.goto(`${BASE}/label-repo/settings`);
127 await page.fill('input[name=name]', 'enhancement');
128 await page.locator('input[type=color][name=color]').evaluate(
129 (el: any) => { el.value = '#00aa00'; },
130 );
131 await page.click('form[action$="/settings/labels"] button[type=submit]');
132 await page.waitForURL(/\/label-repo\/settings/);
133 const badges = await page.locator('.label-settings-name').allTextContents();
134 expect(badges).toContain('bug');
135 expect(badges).toContain('enhancement');
136 } finally { await page.close(); }
137 });
138
139 test('duplicate label name is rejected', async () => {
140 const page = await adminCtx.newPage();
141 try {
142 await page.goto(`${BASE}/label-repo/settings`);
143 await page.fill('input[name=name]', 'bug');
144 await page.locator('input[type=color][name=color]').evaluate(
145 (el: any) => { el.value = '#0000ff'; },
146 );
147 await page.click('form[action$="/settings/labels"] button[type=submit]');
148 await page.waitForURL(/\/label-repo\/settings/);
149 expect(await page.locator('.form-error').isVisible()).toBe(true);
150 } finally { await page.close(); }
151 });
152
153 test('non-admin cannot create labels', async () => {
154 const ctx = await browser.newContext();
155 try {
156 const r = await ctx.request.post(`${BASE}/label-repo/settings/labels`, {
157 form: { name: 'nope', color: '#123456' },
158 maxRedirects: 0,
159 });
160 // Unauthenticated → redirected to /login
161 expect(r.status()).toBe(302);
162 expect(r.headers()['location']).toContain('/login');
163 } finally { await ctx.close(); }
164 });
165
166 test('assign label to issue', async () => {
167 const page = await adminCtx.newPage();
168 try {
169 await page.goto(issueUrl);
170 await page.selectOption('select[name=label_id]', { label: 'bug' });
171 await page.click('form[action$="/labels/add"] button[type=submit]');
172 await page.waitForURL(new RegExp(issueUrl.replace(BASE, '')));
173 expect(await page.locator('.label-badge').allTextContents()).toContain('bug');
174 } finally { await page.close(); }
175 });
176
177 test('label appears on issue list', async () => {
178 const page = await adminCtx.newPage();
179 try {
180 await page.goto(`${BASE}/label-repo/issues`);
181 const item = page.locator('.issue-item').filter({ hasText: 'Labelled issue' });
182 expect(await item.locator('.label-badge').allTextContents()).toContain('bug');
183 } finally { await page.close(); }
184 });
185
186 test('filter issues by label shows only matching issues', async () => {
187 // Create a second issue without the label
188 const createPage = await adminCtx.newPage();
189 try {
190 await createPage.goto(`${BASE}/label-repo/issues/new`);
191 await createPage.fill('[name=title]', 'Unlabelled issue');
192 await createPage.click('form[action$="/issues"] button[type=submit]');
193 await createPage.waitForURL(/\/label-repo\/issues\/\d+/);
194 } finally { await createPage.close(); }
195
196 // Open filter popup and apply label filter
197 const page = await adminCtx.newPage();
198 try {
199 await page.goto(`${BASE}/label-repo/issues`);
200 // Get the label id from the checkbox
201 const checkbox = page.locator('.label-filter-item input[name=labels]').first();
202 const labelId = await checkbox.getAttribute('value');
203 expect(labelId).toBeTruthy();
204
205 // Navigate with the filter applied via URL
206 await page.goto(`${BASE}/label-repo/issues?labels=${labelId}`);
207 const titles = await page.locator('.issue-title').allTextContents();
208 expect(titles.some(t => t.includes('Labelled issue'))).toBe(true);
209 expect(titles.some(t => t.includes('Unlabelled issue'))).toBe(false);
210 } finally { await page.close(); }
211 });
212
213 test('filter popup is visible without JS', async () => {
214 // details/summary is a native HTML element — verify it renders
215 const page = await adminCtx.newPage();
216 try {
217 await page.goto(`${BASE}/label-repo/issues`);
218 expect(await page.locator('details.label-filter').isVisible()).toBe(true);
219 expect(await page.locator('details.label-filter summary').isVisible()).toBe(true);
220 } finally { await page.close(); }
221 });
222
223 test('remove label from issue', async () => {
224 const page = await adminCtx.newPage();
225 try {
226 await page.goto(issueUrl);
227 await page.click('form[action$="/labels/remove"] button[type=submit]');
228 await page.waitForURL(new RegExp(issueUrl.replace(BASE, '')));
229 // Label badge should no longer appear in the labels row
230 const labelBadges = await page.locator('.issue-labels-row .label-badge').allTextContents();
231 expect(labelBadges).not.toContain('bug');
232 } finally { await page.close(); }
233 });
234
235 test('assign label to patch', async () => {
236 const page = await adminCtx.newPage();
237 try {
238 await page.goto(patchUrl);
239 await page.selectOption('select[name=label_id]', { label: 'enhancement' });
240 await page.click('form[action$="/labels/add"] button[type=submit]');
241 await page.waitForURL(new RegExp(patchUrl.replace(BASE, '')));
242 expect(await page.locator('.label-badge').allTextContents()).toContain('enhancement');
243 } finally { await page.close(); }
244 });
245
246 test('label appears on patch list', async () => {
247 const page = await adminCtx.newPage();
248 try {
249 await page.goto(`${BASE}/label-repo/patches`);
250 const item = page.locator('.issue-item').filter({ hasText: 'Labelled patch' });
251 expect(await item.locator('.label-badge').allTextContents()).toContain('enhancement');
252 } finally { await page.close(); }
253 });
254
255 test('filter patches by label', async () => {
256 const page = await adminCtx.newPage();
257 try {
258 await page.goto(`${BASE}/label-repo/patches`);
259 // Find the checkbox for the 'enhancement' label specifically
260 const checkbox = page.locator('.label-filter-item').filter({ hasText: 'enhancement' })
261 .locator('input[name=labels]');
262 const labelId = await checkbox.getAttribute('value');
263 expect(labelId).toBeTruthy();
264
265 await page.goto(`${BASE}/label-repo/patches?labels=${labelId}`);
266 const titles = await page.locator('.issue-title').allTextContents();
267 expect(titles.some(t => t.includes('Labelled patch'))).toBe(true);
268 } finally { await page.close(); }
269 });
270
271 test('remove label from patch', async () => {
272 const page = await adminCtx.newPage();
273 try {
274 await page.goto(patchUrl);
275 await page.click('form[action$="/labels/remove"] button[type=submit]');
276 await page.waitForURL(new RegExp(patchUrl.replace(BASE, '')));
277 const labelBadges = await page.locator('.issue-labels-row .label-badge').allTextContents();
278 expect(labelBadges).not.toContain('enhancement');
279 } finally { await page.close(); }
280 });
281
282 test('delete label removes it from settings list', async () => {
283 const page = await adminCtx.newPage();
284 try {
285 await page.goto(`${BASE}/label-repo/settings`);
286 const bugItem = page.locator('.label-settings-item').filter({ hasText: 'bug' });
287 await bugItem.locator('form[action$="/labels/delete"] button').click();
288 await page.waitForURL(/\/label-repo\/settings/);
289 const badges = await page.locator('.label-settings-name').allTextContents();
290 expect(badges).not.toContain('bug');
291 } finally { await page.close(); }
292 });
293
294 test('deleted label no longer appears in filter popup', async () => {
295 const page = await adminCtx.newPage();
296 try {
297 await page.goto(`${BASE}/label-repo/issues`);
298 const filterLabels = await page.locator('.label-filter-item').allTextContents();
299 expect(filterLabels.every(t => !t.includes('bug'))).toBe(true);
300 } finally { await page.close(); }
301 });
302});
303
304// ─── User label management ────────────────────────────────────────────────────
305
306describe('user label management', () => {
307 let adminCtx: BrowserContext;
308 let aliceCtx: BrowserContext;
309 // URL of an issue owned by alice
310 let aliceIssueUrl: string;
311 // URL of an issue owned by admin
312 let adminIssueUrl: string;
313 // label IDs, fetched from the filter inputs
314 let bugLabelId: string;
315
316 const VALID_PATCH = [
317 'From a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 Mon Sep 17 00:00:00 2001',
318 'From: Test User <test@example.com>',
319 'Date: Mon, 01 Jan 2024 12:00:00 +0000',
320 'Subject: [PATCH] Add ulm-test.txt',
321 '',
322 '---',
323 'diff --git a/ulm-test.txt b/ulm-test.txt',
324 'new file mode 100644',
325 'index 0000000..9daeafb',
326 '--- /dev/null',
327 '+++ b/ulm-test.txt',
328 '@@ -0,0 +1 @@',
329 '+x',
330 '',
331 ].join('\n');
332
333 beforeAll(async () => {
334 adminCtx = await loggedInContext();
335 aliceCtx = await loggedInContext('alice', 'password123');
336
337 // Create dedicated repo
338 const repoPage = await adminCtx.newPage();
339 try {
340 await repoPage.goto(`${BASE}/new`);
341 await repoPage.fill('[name=name]', 'ulm-repo');
342 await repoPage.click('form[action="/new"] button[type=submit]');
343 await repoPage.waitForURL(`${BASE}/ulm-repo`);
344 } finally { await repoPage.close(); }
345
346 // Create labels 'bug' and 'feature'
347 for (const name of ['bug', 'feature']) {
348 const p = await adminCtx.newPage();
349 try {
350 await p.goto(`${BASE}/ulm-repo/settings`);
351 await p.fill('input[name=name]', name);
352 await p.click('form[action$="/settings/labels"] button[type=submit]');
353 await p.waitForURL(/\/ulm-repo\/settings/);
354 } finally { await p.close(); }
355 }
356
357 // Create an issue owned by admin
358 const adminIssuePage = await adminCtx.newPage();
359 try {
360 await adminIssuePage.goto(`${BASE}/ulm-repo/issues/new`);
361 await adminIssuePage.fill('[name=title]', "Admin's issue");
362 await adminIssuePage.click('form[action$="/issues"] button[type=submit]');
363 await adminIssuePage.waitForURL(/\/ulm-repo\/issues\/\d+/);
364 adminIssueUrl = adminIssuePage.url();
365 } finally { await adminIssuePage.close(); }
366
367 // Create an issue owned by alice
368 const aliceIssuePage = await aliceCtx.newPage();
369 try {
370 await aliceIssuePage.goto(`${BASE}/ulm-repo/issues/new`);
371 await aliceIssuePage.fill('[name=title]', "Alice's issue");
372 await aliceIssuePage.click('form[action$="/issues"] button[type=submit]');
373 await aliceIssuePage.waitForURL(/\/ulm-repo\/issues\/\d+/);
374 aliceIssueUrl = aliceIssuePage.url();
375 } finally { await aliceIssuePage.close(); }
376
377 // Grab the bug label id from the filter popup
378 const filterPage = await adminCtx.newPage();
379 try {
380 await filterPage.goto(`${BASE}/ulm-repo/issues`);
381 const checkbox = filterPage.locator('.label-filter-item')
382 .filter({ hasText: 'bug' })
383 .locator('input[name=labels]');
384 bugLabelId = (await checkbox.getAttribute('value')) ?? '';
385 } finally { await filterPage.close(); }
386 });
387
388 afterAll(async () => {
389 await adminCtx.close();
390 await aliceCtx.close();
391 });
392
393 // ── Settings ──
394
395 test('allow_user_labels checkbox is present in repo settings', async () => {
396 const page = await adminCtx.newPage();
397 try {
398 await page.goto(`${BASE}/ulm-repo/settings`);
399 expect(await page.locator('input[name=allow_user_labels]').count()).toBe(1);
400 } finally { await page.close(); }
401 });
402
403 test('allow_user_labels is off by default', async () => {
404 const page = await adminCtx.newPage();
405 try {
406 await page.goto(`${BASE}/ulm-repo/settings`);
407 expect(await page.locator('input[name=allow_user_labels]').isChecked()).toBe(false);
408 } finally { await page.close(); }
409 });
410
411 // ── Label checkboxes hidden when setting is off ──
412
413 test('label checkboxes not shown to non-admin on new issue form when allow_user_labels is off', async () => {
414 const page = await aliceCtx.newPage();
415 try {
416 await page.goto(`${BASE}/ulm-repo/issues/new`);
417 expect(await page.locator('.label-checkbox-list').count()).toBe(0);
418 } finally { await page.close(); }
419 });
420
421 test('label checkboxes not shown to non-admin on new patch form when allow_user_labels is off', async () => {
422 const page = await aliceCtx.newPage();
423 try {
424 await page.goto(`${BASE}/ulm-repo/patches/new`);
425 expect(await page.locator('.label-checkbox-list').count()).toBe(0);
426 } finally { await page.close(); }
427 });
428
429 test('label checkboxes shown to admin on new issue form regardless of setting', async () => {
430 const page = await adminCtx.newPage();
431 try {
432 await page.goto(`${BASE}/ulm-repo/issues/new`);
433 expect(await page.locator('.label-checkbox-list').isVisible()).toBe(true);
434 } finally { await page.close(); }
435 });
436
437 // ── Enable the setting ──
438
439 test('admin can enable allow_user_labels', async () => {
440 const page = await adminCtx.newPage();
441 try {
442 await page.goto(`${BASE}/ulm-repo/settings`);
443 await page.check('input[name=allow_user_labels]');
444 await page.click('form[action$="/settings"] button[type=submit]');
445 await page.waitForURL(/\/ulm-repo\/settings/);
446 // Verify it persisted
447 await page.goto(`${BASE}/ulm-repo/settings`);
448 expect(await page.locator('input[name=allow_user_labels]').isChecked()).toBe(true);
449 } finally { await page.close(); }
450 });
451
452 // ── Label checkboxes visible when setting is on ──
453
454 test('label checkboxes shown to non-admin on new issue form when allow_user_labels is on', async () => {
455 const page = await aliceCtx.newPage();
456 try {
457 await page.goto(`${BASE}/ulm-repo/issues/new`);
458 expect(await page.locator('.label-checkbox-list').isVisible()).toBe(true);
459 const labels = await page.locator('.label-checkbox-list .label-badge').allTextContents();
460 expect(labels).toContain('bug');
461 expect(labels).toContain('feature');
462 } finally { await page.close(); }
463 });
464
465 test('label checkboxes shown to non-admin on new patch form when allow_user_labels is on', async () => {
466 const page = await aliceCtx.newPage();
467 try {
468 await page.goto(`${BASE}/ulm-repo/patches/new`);
469 expect(await page.locator('.label-checkbox-list').isVisible()).toBe(true);
470 } finally { await page.close(); }
471 });
472
473 // ── Creating with labels selected ──
474
475 test('non-admin can create issue with label selected', async () => {
476 const page = await aliceCtx.newPage();
477 try {
478 await page.goto(`${BASE}/ulm-repo/issues/new`);
479 await page.fill('[name=title]', 'Issue with label');
480 // Check the 'bug' label checkbox
481 await page.locator('.label-checkbox-item').filter({ hasText: 'bug' })
482 .locator('input[type=checkbox]').check();
483 await page.click('form[action$="/issues"] button[type=submit]');
484 await page.waitForURL(/\/ulm-repo\/issues\/\d+/);
485 const badges = await page.locator('.label-badge').allTextContents();
486 expect(badges).toContain('bug');
487 } finally { await page.close(); }
488 });
489
490 test('non-admin can create patch with label selected', async () => {
491 writeTempFile('/tmp/ulm-test.patch', VALID_PATCH);
492 const page = await aliceCtx.newPage();
493 try {
494 await page.goto(`${BASE}/ulm-repo/patches/new`);
495 await page.fill('[name=title]', 'Patch with label');
496 await page.locator('[name=patch_file]').setInputFiles('/tmp/ulm-test.patch');
497 await page.locator('.label-checkbox-item').filter({ hasText: 'feature' })
498 .locator('input[type=checkbox]').check();
499 await page.click('form[action$="/patches"] button[type=submit]');
500 await page.waitForURL(/\/ulm-repo\/patches\/\d+/);
501 const badges = await page.locator('.label-badge').allTextContents();
502 expect(badges).toContain('feature');
503 } finally { await page.close(); }
504 });
505
506 test('label_ids in POST are ignored for non-admin when allow_user_labels is off (no label applied)', async () => {
507 // Temporarily disable the setting, post with label_ids, re-enable
508 await adminCtx.request.post(`${BASE}/ulm-repo/settings`, {
509 form: { description: '', default_branch: 'main' }, // no allow_user_labels
510 maxRedirects: 0,
511 }).catch(() => {});
512
513 const resp = await aliceCtx.request.post(`${BASE}/ulm-repo/issues`, {
514 form: { title: 'Issue sneaking labels', label_ids: bugLabelId },
515 maxRedirects: 0,
516 }).catch(() => null);
517 // Should redirect to the new issue
518 const location = resp?.headers()['location'] ?? '';
519 const issueNum = location.split('/issues/')[1];
520
521 if (issueNum) {
522 const page = await aliceCtx.newPage();
523 try {
524 await page.goto(`${BASE}/ulm-repo/issues/${issueNum}`);
525 const badges = await page.locator('.label-badge').allTextContents();
526 expect(badges).not.toContain('bug');
527 } finally { await page.close(); }
528 }
529
530 // Re-enable for subsequent tests
531 await adminCtx.request.post(`${BASE}/ulm-repo/settings`, {
532 form: { description: '', default_branch: 'main', allow_user_labels: '1' },
533 maxRedirects: 0,
534 }).catch(() => {});
535 });
536
537 // ── Add/remove labels on existing items ──
538
539 test('non-admin can add label to their own issue', async () => {
540 const issueNum = aliceIssueUrl.split('/issues/')[1];
541 const page = await aliceCtx.newPage();
542 try {
543 await page.goto(aliceIssueUrl);
544 await page.selectOption('select[name=label_id]', { label: 'bug' });
545 await page.click('form[action$="/labels/add"] button[type=submit]');
546 await page.waitForURL(new RegExp(`/ulm-repo/issues/${issueNum}`));
547 expect(await page.locator('.label-badge').allTextContents()).toContain('bug');
548 } finally { await page.close(); }
549 });
550
551 test('non-admin can remove label from their own issue', async () => {
552 const issueNum = aliceIssueUrl.split('/issues/')[1];
553 const page = await aliceCtx.newPage();
554 try {
555 await page.goto(aliceIssueUrl);
556 await page.click('form[action$="/labels/remove"] button[type=submit]');
557 await page.waitForURL(new RegExp(`/ulm-repo/issues/${issueNum}`));
558 const badges = await page.locator('.issue-labels-row .label-badge').allTextContents();
559 expect(badges).not.toContain('bug');
560 } finally { await page.close(); }
561 });
562
563 test('non-admin cannot add label to another user\'s issue', async () => {
564 const issueNum = adminIssueUrl.split('/issues/')[1];
565 const resp = await aliceCtx.request.post(
566 `${BASE}/ulm-repo/issues/${issueNum}/labels/add`,
567 { form: { label_id: bugLabelId }, maxRedirects: 0 },
568 );
569 expect(resp.status()).toBe(403);
570 });
571
572 test('unauthenticated user gets 401 adding a label', async () => {
573 const issueNum = aliceIssueUrl.split('/issues/')[1];
574 const ctx = await browser.newContext();
575 try {
576 const resp = await ctx.request.post(
577 `${BASE}/ulm-repo/issues/${issueNum}/labels/add`,
578 { form: { label_id: bugLabelId }, maxRedirects: 0 },
579 );
580 expect(resp.status()).toBe(401);
581 } finally { await ctx.close(); }
582 });
583
584 // ── Disable setting and verify enforcement ──
585
586 test('admin can disable allow_user_labels', async () => {
587 const page = await adminCtx.newPage();
588 try {
589 await page.goto(`${BASE}/ulm-repo/settings`);
590 await page.uncheck('input[name=allow_user_labels]');
591 await page.click('form[action$="/settings"] button[type=submit]');
592 await page.waitForURL(/\/ulm-repo\/settings/);
593 await page.goto(`${BASE}/ulm-repo/settings`);
594 expect(await page.locator('input[name=allow_user_labels]').isChecked()).toBe(false);
595 } finally { await page.close(); }
596 });
597
598 test('non-admin gets 403 adding label to own issue when allow_user_labels is off', async () => {
599 const issueNum = aliceIssueUrl.split('/issues/')[1];
600 const resp = await aliceCtx.request.post(
601 `${BASE}/ulm-repo/issues/${issueNum}/labels/add`,
602 { form: { label_id: bugLabelId }, maxRedirects: 0 },
603 );
604 expect(resp.status()).toBe(403);
605 });
606
607 test('label checkboxes hidden on new issue form after allow_user_labels disabled', async () => {
608 const page = await aliceCtx.newPage();
609 try {
610 await page.goto(`${BASE}/ulm-repo/issues/new`);
611 expect(await page.locator('.label-checkbox-list').count()).toBe(0);
612 } finally { await page.close(); }
613 });
614});
615