snapshot of https://github.com/go-gitea/gitea.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
cache-seeder / prune (push) Has been skipped
cache-seeder / frontend (push) Failing after 3s
cache-seeder / python (push) Failing after 3s
cache-seeder / gobuild (push) Failing after 4s
cache-seeder / lint (bindata, lint-backend) (push) Failing after 3s
giteabot backport / giteabot (push) Has been skipped
giteabot / giteabot (push) Has been skipped
cache-prune / prune (push) Has been skipped
release-nightly-snapcraft / build-and-publish (ubuntu-24.04) (push) Has been cancelled
release-nightly-snapcraft / build-and-publish (ubuntu-24.04-arm) (push) Has been cancelled
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
cron-renovate / cron-renovate (push) Has been skipped
Some checks failed
cache-seeder / prune (push) Has been skipped
cache-seeder / frontend (push) Failing after 3s
cache-seeder / python (push) Failing after 3s
cache-seeder / gobuild (push) Failing after 4s
cache-seeder / lint (bindata, lint-backend) (push) Failing after 3s
giteabot backport / giteabot (push) Has been skipped
giteabot / giteabot (push) Has been skipped
cache-prune / prune (push) Has been skipped
release-nightly-snapcraft / build-and-publish (ubuntu-24.04) (push) Has been cancelled
release-nightly-snapcraft / build-and-publish (ubuntu-24.04-arm) (push) Has been cancelled
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
cron-renovate / cron-renovate (push) Has been skipped
This commit is contained in:
16
tests/e2e/codeeditor.test.ts
Normal file
16
tests/e2e/codeeditor.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {env} from 'node:process';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {login, apiCreateRepo, randomString} from './utils.ts';
|
||||
|
||||
test('codeeditor textarea updates correctly', async ({page, request}) => {
|
||||
const repoName = `e2e-codeeditor-${randomString(8)}`;
|
||||
await Promise.all([apiCreateRepo(request, {name: repoName}), login(page)]);
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/_new/main`);
|
||||
await page.getByPlaceholder('Name your file…').fill('test.js');
|
||||
await expect(page.locator('[data-tab="write"] .editor-loading')).toBeHidden();
|
||||
const editor = page.locator('.cm-content[role="textbox"]');
|
||||
await expect(editor).toBeVisible();
|
||||
await editor.click();
|
||||
await page.keyboard.type('const hello = "world";');
|
||||
await expect(page.locator('textarea[name="content"]')).toHaveValue('const hello = "world";');
|
||||
});
|
||||
9
tests/e2e/env.d.ts
vendored
Normal file
9
tests/e2e/env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
GITEA_TEST_E2E_DOMAIN: string;
|
||||
GITEA_TEST_E2E_USER: string;
|
||||
GITEA_TEST_E2E_EMAIL: string;
|
||||
GITEA_TEST_E2E_PASSWORD: string;
|
||||
GITEA_TEST_E2E_URL: string;
|
||||
}
|
||||
}
|
||||
138
tests/e2e/events.test.ts
Normal file
138
tests/e2e/events.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {loginUser, baseUrl, apiUserHeaders, apiCreateUser, apiCreateRepo, apiCreateIssue, apiStartStopwatch, apiCancelStopwatch, apiCloseIssue, randomString} from './utils.ts';
|
||||
|
||||
// The /-/ws WebSocket pipeline is push-only: every event is fired by the server
|
||||
// immediately on the DB write. These tests exercise that each event type
|
||||
// (notification-count, stopwatches, logout) reaches a connected tab.
|
||||
test.describe('events', () => {
|
||||
test('notification count increases on new notification', async ({page, request}) => {
|
||||
const owner = `ev-notif-owner-${randomString(8)}`;
|
||||
const commenter = `ev-notif-commenter-${randomString(8)}`;
|
||||
const repoName = `ev-notif-${randomString(8)}`;
|
||||
|
||||
await Promise.all([apiCreateUser(request, owner), apiCreateUser(request, commenter)]);
|
||||
|
||||
await Promise.all([
|
||||
apiCreateRepo(request, {name: repoName, autoInit: false, headers: apiUserHeaders(owner)}),
|
||||
loginUser(page, owner),
|
||||
]);
|
||||
await page.goto('/');
|
||||
const badge = page.locator('a.not-mobile .notification_count');
|
||||
await expect(badge).toBeHidden();
|
||||
|
||||
await expect(page.locator('html[data-user-events-connected]')).toBeAttached();
|
||||
|
||||
await apiCreateIssue(request, {owner, repo: repoName, title: 'events-notif', headers: apiUserHeaders(commenter)});
|
||||
await expect(badge).toBeVisible();
|
||||
});
|
||||
|
||||
test('stopwatch appears and hides via real-time push', async ({page, request}) => {
|
||||
const name = `ev-sw-push-${randomString(8)}`;
|
||||
const headers = apiUserHeaders(name);
|
||||
|
||||
await apiCreateUser(request, name);
|
||||
await Promise.all([
|
||||
loginUser(page, name),
|
||||
(async () => {
|
||||
await apiCreateRepo(request, {name, headers});
|
||||
await apiCreateIssue(request, {owner: name, repo: name, title: 'events stopwatch push test', headers});
|
||||
})(),
|
||||
]);
|
||||
// Page loads before the stopwatch starts — the icon is hidden in the rendered HTML
|
||||
await page.goto('/');
|
||||
const stopwatch = page.locator('.active-stopwatch.not-mobile');
|
||||
// Element must exist in the DOM (just hidden); otherwise the push has nothing to reveal.
|
||||
await expect(stopwatch).toHaveCount(1);
|
||||
await expect(stopwatch).toBeHidden();
|
||||
|
||||
await expect(page.locator('html[data-user-events-connected]')).toBeAttached();
|
||||
|
||||
// Drive both directions from outside this tab; each push must reach it
|
||||
await apiStartStopwatch(request, name, name, 1, {headers});
|
||||
await expect(stopwatch).toBeVisible();
|
||||
|
||||
await apiCancelStopwatch(request, name, name, 1, {headers});
|
||||
await expect(stopwatch).toBeHidden();
|
||||
});
|
||||
|
||||
// Closing an issue stops the timer away from any stopwatch route handler.
|
||||
test('stopwatch renders when already active and hides when the issue is closed', async ({page, request}) => {
|
||||
const name = `ev-sw-close-${randomString(8)}`;
|
||||
const headers = apiUserHeaders(name);
|
||||
|
||||
await apiCreateUser(request, name);
|
||||
await Promise.all([
|
||||
loginUser(page, name),
|
||||
(async () => {
|
||||
await apiCreateRepo(request, {name, autoInit: false, headers});
|
||||
await apiCreateIssue(request, {owner: name, repo: name, title: 'events stopwatch close test', headers});
|
||||
await apiStartStopwatch(request, name, name, 1, {headers});
|
||||
})(),
|
||||
]);
|
||||
await page.goto('/');
|
||||
const stopwatch = page.locator('.active-stopwatch.not-mobile');
|
||||
await expect(stopwatch).toBeVisible();
|
||||
await expect(page.locator('html[data-user-events-connected]')).toBeAttached();
|
||||
|
||||
await apiCloseIssue(request, name, name, 1, {headers});
|
||||
await expect(stopwatch).toBeHidden();
|
||||
});
|
||||
|
||||
// Repro for https://github.com/go-gitea/gitea/pull/36965#issuecomment-4321282667:
|
||||
// clicking the sidebar "Start timer" button reportedly produced a blank page.
|
||||
// Drives the actual UI button (not the API) so the link-action → JSONRedirect("")
|
||||
// → fetchActionDoRedirect("") path is exercised end-to-end.
|
||||
test('sidebar start timer button starts stopwatch without blanking the page', async ({page, request}) => {
|
||||
const name = `ev-sw-ui-${randomString(8)}`;
|
||||
const headers = apiUserHeaders(name);
|
||||
|
||||
await apiCreateUser(request, name);
|
||||
await Promise.all([
|
||||
loginUser(page, name),
|
||||
(async () => {
|
||||
await apiCreateRepo(request, {name, headers});
|
||||
await apiCreateIssue(request, {owner: name, repo: name, title: 'sidebar start timer test', headers});
|
||||
})(),
|
||||
]);
|
||||
await page.goto(`/${name}/${name}/issues/1`);
|
||||
|
||||
await page.getByRole('button', {name: 'Start timer'}).click();
|
||||
|
||||
// After the click the page reloads; the sidebar should now show the Stop/Discard
|
||||
// controls and the navbar stopwatch icon should appear. If the page blanked,
|
||||
// neither of these would be present.
|
||||
await expect(page.getByRole('button', {name: 'Stop timer'})).toBeVisible();
|
||||
await expect(page.getByRole('button', {name: 'Discard timer'})).toBeVisible();
|
||||
await expect(page.locator('.active-stopwatch.not-mobile')).toBeVisible();
|
||||
});
|
||||
|
||||
test('logout propagation', async ({browser, request}) => {
|
||||
const name = `ev-logout-${randomString(8)}`;
|
||||
|
||||
await apiCreateUser(request, name);
|
||||
|
||||
// Use a single context so both pages share the same session and SharedWorker
|
||||
const context = await browser.newContext({baseURL: baseUrl()});
|
||||
const page1 = await context.newPage();
|
||||
const page2 = await context.newPage();
|
||||
|
||||
await loginUser(page1, name);
|
||||
|
||||
// Navigate page2 so it connects to the shared event stream
|
||||
await page2.goto('/');
|
||||
|
||||
// Verify page2 is logged in
|
||||
await expect(page2.getByRole('link', {name: 'Sign In'})).toBeHidden();
|
||||
// Wait until page2's event stream is connected, otherwise the logout event
|
||||
// can race the connection and be silently dropped.
|
||||
await expect(page2.locator('html[data-user-events-connected]')).toBeAttached();
|
||||
|
||||
// Logout from page1 — this sends a logout event to all tabs
|
||||
await page1.goto('/user/logout');
|
||||
|
||||
// page2 should be redirected via the logout event
|
||||
await expect(page2.getByRole('link', {name: 'Sign In'})).toBeVisible();
|
||||
|
||||
await context.close();
|
||||
});
|
||||
});
|
||||
17
tests/e2e/explore.test.ts
Normal file
17
tests/e2e/explore.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
|
||||
test('explore repositories', async ({page}) => {
|
||||
await page.goto('/explore/repos');
|
||||
await expect(page.getByPlaceholder('Search repos…')).toBeVisible();
|
||||
await expect(page.getByRole('link', {name: 'Repositories'})).toBeVisible();
|
||||
});
|
||||
|
||||
test('explore users', async ({page}) => {
|
||||
await page.goto('/explore/users');
|
||||
await expect(page.getByPlaceholder('Search users…')).toBeVisible();
|
||||
});
|
||||
|
||||
test('explore organizations', async ({page}) => {
|
||||
await page.goto('/explore/organizations');
|
||||
await expect(page.getByPlaceholder('Search orgs…')).toBeVisible();
|
||||
});
|
||||
53
tests/e2e/external-render.test.ts
Normal file
53
tests/e2e/external-render.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {env} from 'node:process';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiCreateFiles, assertFlushWithParent, assertNoJsError, randomString} from './utils.ts';
|
||||
|
||||
test('external file', async ({page, request}) => {
|
||||
const repoName = `e2e-external-render-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
await Promise.all([
|
||||
apiCreateRepo(request, {name: repoName}),
|
||||
login(page),
|
||||
]);
|
||||
await apiCreateFiles(request, owner, repoName, [{path: 'test.external', content: '<p>rendered content</p>'}]);
|
||||
await page.goto(`/${owner}/${repoName}/src/branch/main/test.external`);
|
||||
const iframe = page.locator('iframe.external-render-iframe');
|
||||
await expect(iframe).toBeVisible();
|
||||
await expect(iframe).toHaveAttribute('data-src', new RegExp(`/${owner}/${repoName}/render/branch/main/test\\.external`));
|
||||
const frame = page.frameLocator('iframe.external-render-iframe');
|
||||
await expect(frame.locator('p')).toContainText('rendered content');
|
||||
await assertFlushWithParent(iframe, page.locator('.file-view'));
|
||||
await assertNoJsError(page);
|
||||
});
|
||||
|
||||
test('openapi file', async ({page, request}) => {
|
||||
const repoName = `e2e-openapi-render-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
await Promise.all([
|
||||
apiCreateRepo(request, {name: repoName}),
|
||||
login(page),
|
||||
]);
|
||||
const title = 'Test <API> & "quoted"';
|
||||
const spec = JSON.stringify({
|
||||
openapi: '3.0.0',
|
||||
info: {title, version: '1.0'},
|
||||
paths: {'/pets': {get: {responses: {'200': {description: 'OK', content: {'application/json': {schema: {$ref: '#/components/schemas/Pet'}}}}}}}},
|
||||
components: {schemas: {Pet: {type: 'object', properties: {children: {type: 'array', items: {$ref: '#/components/schemas/Pet'}}}}}},
|
||||
});
|
||||
await apiCreateFiles(request, owner, repoName, [{path: 'openapi.json', content: spec}]);
|
||||
await page.goto(`/${owner}/${repoName}/src/branch/main/openapi.json`);
|
||||
const iframe = page.locator('iframe.external-render-iframe');
|
||||
await expect(iframe).toBeVisible();
|
||||
const viewer = page.frameLocator('iframe.external-render-iframe').locator('#frontend-render-viewer');
|
||||
await expect(viewer.locator('.swagger-ui')).toBeVisible();
|
||||
await expect(viewer.locator('.info .title')).toContainText(title);
|
||||
// expanding the operation triggers swagger-ui's $ref resolver, which fetches window.location
|
||||
// (about:srcdoc since the iframe is loaded via srcdoc); failure surfaces as "Could not resolve reference"
|
||||
await viewer.locator('.opblock-tag').first().click();
|
||||
await viewer.locator('.opblock').first().click();
|
||||
await expect(viewer.getByText('Could not resolve reference')).toHaveCount(0);
|
||||
// poll: postMessage resize may not have settled yet when the visibility checks pass
|
||||
await expect.poll(async () => (await iframe.boundingBox())!.height).toBeGreaterThan(300);
|
||||
await assertFlushWithParent(iframe, page.locator('.file-view'));
|
||||
await assertNoJsError(page);
|
||||
});
|
||||
63
tests/e2e/file-view-render.test.ts
Normal file
63
tests/e2e/file-view-render.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {env} from 'node:process';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {apiCreateRepo, apiCreateFiles, assertFlushWithParent, assertNoJsError, login, randomString} from './utils.ts';
|
||||
|
||||
test('3d model file', async ({page, request, browserName}) => {
|
||||
test.skip(browserName === 'firefox', 'unclear firefox-only CI-only failure'); // eslint-disable-line playwright/no-skipped-test -- conditional skip, the reason is in the message
|
||||
const repoName = `e2e-3d-render-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
await apiCreateRepo(request, {name: repoName});
|
||||
const stl = 'solid test\nfacet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid test\n';
|
||||
await apiCreateFiles(request, owner, repoName, [{path: 'test.stl', content: stl}]);
|
||||
await page.goto(`/${owner}/${repoName}/src/branch/main/test.stl?display=rendered`);
|
||||
const iframe = page.locator('iframe.external-render-iframe');
|
||||
await expect(iframe).toBeVisible();
|
||||
const frame = page.frameLocator('iframe.external-render-iframe');
|
||||
const viewer = frame.locator('#frontend-render-viewer');
|
||||
await expect(viewer.locator('canvas')).toBeVisible(); // unclear firefox-only CI-only failure
|
||||
expect((await viewer.boundingBox())!.height).toBeGreaterThan(300);
|
||||
await assertFlushWithParent(iframe, page.locator('.file-view'));
|
||||
// bgcolor passed via gitea-iframe-bgcolor; 3D viewer reads it from body bgcolor — must match parent
|
||||
const [parentBg, iframeBg] = await Promise.all([
|
||||
page.evaluate(() => getComputedStyle(document.body).backgroundColor),
|
||||
frame.locator('body').evaluate((el) => getComputedStyle(el).backgroundColor),
|
||||
]);
|
||||
expect(iframeBg).toBe(parentBg);
|
||||
await assertNoJsError(page);
|
||||
});
|
||||
|
||||
test('pdf file', async ({page, request}) => {
|
||||
// headless playwright cannot render PDFs (PDFObject.embed returns false), so this is a limited test
|
||||
const repoName = `e2e-pdf-render-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
await apiCreateRepo(request, {name: repoName});
|
||||
await apiCreateFiles(request, owner, repoName, [{path: 'test.pdf', content: '%PDF-1.0\n%%EOF\n'}]);
|
||||
await page.goto(`/${owner}/${repoName}/src/branch/main/test.pdf`);
|
||||
const container = page.locator('.file-view-render-container');
|
||||
await expect(container).toHaveAttribute('data-render-name', 'pdf-viewer');
|
||||
await expect.poll(async () => (await container.boundingBox())!.height).toBeGreaterThan(300);
|
||||
await assertFlushWithParent(container, page.locator('.file-view'));
|
||||
});
|
||||
|
||||
test('asciicast file', async ({page, request}) => {
|
||||
const repoName = `e2e-asciicast-render-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
const branch = '日本語-branch';
|
||||
const branchEnc = encodeURIComponent(branch);
|
||||
await Promise.all([apiCreateRepo(request, {name: repoName, autoInit: false}), login(page)]);
|
||||
const cast = '{"version": 2, "width": 80, "height": 24}\n[0.0, "o", "test-content"]\n';
|
||||
// on an empty repo, apiCreateFile with newBranch creates that branch as the initial commit
|
||||
await apiCreateFiles(request, owner, repoName, [{path: 'test.cast', content: cast}], {newBranch: branch});
|
||||
await page.goto(`/${owner}/${repoName}/src/branch/${branchEnc}/test.cast`);
|
||||
const iframe = page.locator('iframe.external-render-iframe');
|
||||
const frame = iframe.contentFrame();
|
||||
const viewer = frame.locator('#frontend-render-viewer[data-frontend-render-name]');
|
||||
await expect(viewer).toHaveAttribute('data-frontend-render-name', 'asciicast'); // render succeeded
|
||||
await expect(viewer).toHaveAttribute('data-window-origin', 'null'); // no same-origin, avoid XSS
|
||||
const wrapper = frame.locator('.ap-wrapper');
|
||||
await expect(wrapper).toBeVisible();
|
||||
await expect(wrapper).toContainText('test-content');
|
||||
await expect.poll(async () => (await iframe.boundingBox())!.height).toBeGreaterThan(300);
|
||||
await assertFlushWithParent(iframe, page.locator('.file-view'));
|
||||
await assertNoJsError(page);
|
||||
});
|
||||
18
tests/e2e/fork.test.ts
Normal file
18
tests/e2e/fork.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiCreateUser, apiUserHeaders, randomString} from './utils.ts';
|
||||
|
||||
test('fork a repository', async ({page, request}) => {
|
||||
const upstream = `fork-owner-${randomString(8)}`;
|
||||
const repoName = `e2e-fork-${randomString(8)}`;
|
||||
await apiCreateUser(request, upstream);
|
||||
await Promise.all([
|
||||
apiCreateRepo(request, {name: repoName, headers: apiUserHeaders(upstream)}),
|
||||
login(page),
|
||||
]);
|
||||
await page.goto(`/${upstream}/${repoName}/fork`);
|
||||
|
||||
await page.getByRole('button', {name: 'Fork Repository'}).click();
|
||||
await page.waitForURL(new RegExp(`/${env.GITEA_TEST_E2E_USER}/${repoName}$`));
|
||||
await expect(page.getByRole('link', {name: `${upstream}/${repoName}`})).toBeVisible();
|
||||
});
|
||||
9
tests/e2e/heatmap.test.ts
Normal file
9
tests/e2e/heatmap.test.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login} from './utils.ts';
|
||||
|
||||
test('heatmap tooltip shows on hover', async ({page}) => {
|
||||
await login(page);
|
||||
await page.goto('/');
|
||||
await page.locator('.heatmap-day').first().hover();
|
||||
await expect(page.locator('.tippy-box[data-state="visible"]')).toBeVisible();
|
||||
});
|
||||
38
tests/e2e/issue-comment.test.ts
Normal file
38
tests/e2e/issue-comment.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiCreateIssue, randomString} from './utils.ts';
|
||||
|
||||
test('comment on and close an issue', async ({page, request}) => {
|
||||
const repoName = `e2e-issue-comment-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
await apiCreateRepo(request, {name: repoName, autoInit: false});
|
||||
await Promise.all([
|
||||
apiCreateIssue(request, {owner, repo: repoName, title: 'Comment test'}),
|
||||
login(page),
|
||||
]);
|
||||
await page.goto(`/${owner}/${repoName}/issues/1`);
|
||||
|
||||
const body = `e2e-comment-${randomString(8)}`;
|
||||
await page.getByPlaceholder('Leave a comment').fill(body);
|
||||
// exact match: the status button reads "Close with Comment" while the box has content, which substring-matches "Comment"
|
||||
await page.getByRole('button', {name: 'Comment', exact: true}).click();
|
||||
await expect(page.locator('.comment-body').filter({hasText: body})).toBeVisible();
|
||||
|
||||
// wait for the form to re-initialize (the empty box disables the comment button); a close click
|
||||
// before that does a native submit which lands on a raw JSON page instead of reloading the issue
|
||||
await expect(page.getByRole('button', {name: 'Comment', exact: true})).toBeDisabled();
|
||||
await page.getByRole('button', {name: 'Close Issue'}).click();
|
||||
await expect(page.getByRole('button', {name: 'Reopen Issue'})).toBeVisible();
|
||||
});
|
||||
|
||||
test('unsaved issue description prompts before leaving', async ({page, request}) => {
|
||||
const repoName = `e2e-are-you-sure-${randomString(8)}`;
|
||||
await Promise.all([apiCreateRepo(request, {name: repoName, autoInit: false}), login(page)]);
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/issues/new`);
|
||||
await page.getByPlaceholder('Leave a comment').press('a');
|
||||
const dialogPromise = page.waitForEvent('dialog');
|
||||
page.once('dialog', (dialog) => dialog.dismiss());
|
||||
await page.getByRole('link', {name: 'Dashboard'}).click();
|
||||
expect((await dialogPromise).type()).toBe('beforeunload');
|
||||
await expect(page).toHaveURL(/\/issues\/new$/);
|
||||
});
|
||||
443
tests/e2e/issue-project.test.ts
Normal file
443
tests/e2e/issue-project.test.ts
Normal file
@@ -0,0 +1,443 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiCreateIssue, apiDeleteRepo, createProject, createProjectColumn, randomString} from './utils.ts';
|
||||
|
||||
test('assign issue to project and change column', async ({page}) => {
|
||||
const repoName = `e2e-issue-project-${randomString(8)}`;
|
||||
const user = env.GITEA_TEST_E2E_USER;
|
||||
await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName, autoInit: false})]);
|
||||
await page.goto(`/${user}/${repoName}/projects/new`);
|
||||
await page.locator('input[name="title"]').fill('Kanban Board');
|
||||
await page.getByRole('button', {name: 'Create Project'}).click();
|
||||
const projectLink = page.locator('.milestone-list a', {hasText: 'Kanban Board'}).first();
|
||||
await expect(projectLink).toBeVisible();
|
||||
const href = await projectLink.getAttribute('href');
|
||||
const projectID = href!.split('/').pop()!;
|
||||
// columns created via POST because the web UI uses modals that are hard to drive
|
||||
await Promise.all([
|
||||
...['Backlog', 'In Progress', 'Done'].map((title) => createProjectColumn(page.request, user, repoName, projectID, title)),
|
||||
apiCreateIssue(page.request, {owner: user, repo: repoName, title: 'Column picker test'}),
|
||||
]);
|
||||
await page.goto(`/${user}/${repoName}/issues/1`);
|
||||
await page.locator('.sidebar-project-combo > .ui.dropdown').click();
|
||||
await page.locator('.sidebar-project-combo > .ui.dropdown .item:has-text("Kanban Board")').click();
|
||||
await page.locator('.sidebar-project-combo > .ui.dropdown').click();
|
||||
await page.locator('.sidebar-project-column-combo .ui.dropdown').click();
|
||||
await page.locator('.sidebar-project-column-combo .ui.dropdown .item:has-text("In Progress")').click();
|
||||
await expect(page.locator('.sidebar-project-column-combo .ui.dropdown .fixed-text')).toHaveText('In Progress');
|
||||
await apiDeleteRepo(page.request, user, repoName);
|
||||
});
|
||||
|
||||
test('create a project', async ({page}) => {
|
||||
const repoName = `e2e-project-repo-${Date.now()}`;
|
||||
const projectTitle = 'Test Project';
|
||||
|
||||
await login(page);
|
||||
await apiCreateRepo(page.request, {name: repoName, autoInit: false});
|
||||
|
||||
try {
|
||||
// Navigate to new project page
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/projects/new`);
|
||||
|
||||
// Fill in project details
|
||||
await page.getByLabel('Title').fill(projectTitle);
|
||||
|
||||
// Submit the form
|
||||
await page.getByRole('button', {name: 'Create Project'}).click();
|
||||
|
||||
// Verify project was created by checking we're redirected to the projects list
|
||||
await expect(page).toHaveURL(new RegExp(`/${env.GITEA_TEST_E2E_USER}/${repoName}/projects$`));
|
||||
|
||||
// Verify the project appears in the list
|
||||
await expect(page.locator('.milestone-list')).toContainText(projectTitle);
|
||||
} finally {
|
||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
||||
}
|
||||
});
|
||||
|
||||
test('assign issue to multiple projects via sidebar', async ({page}) => {
|
||||
const repoName = `e2e-multi-project-${Date.now()}`;
|
||||
const project1Title = 'Project Alpha';
|
||||
const project2Title = 'Project Beta';
|
||||
const issueTitle = 'Test issue for multiple projects';
|
||||
|
||||
await login(page);
|
||||
await apiCreateRepo(page.request, {name: repoName, autoInit: false});
|
||||
|
||||
try {
|
||||
// Create two projects via UI
|
||||
const project1 = await createProject(page, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: project1Title,
|
||||
});
|
||||
const project2 = await createProject(page, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: project2Title,
|
||||
});
|
||||
|
||||
// Create an issue without any project
|
||||
const issue = await apiCreateIssue(page.request, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: issueTitle,
|
||||
});
|
||||
|
||||
// Navigate to the issue page
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/issues/${issue.index}`);
|
||||
|
||||
// Open the projects dropdown in the sidebar
|
||||
await page.locator('.sidebar-project-combo > .ui.dropdown').click();
|
||||
|
||||
// Select both projects
|
||||
await page.locator(`.sidebar-project-combo > .ui.dropdown .item[data-value="${project1.id}"]`).click();
|
||||
await page.locator(`.sidebar-project-combo > .ui.dropdown .item[data-value="${project2.id}"]`).click();
|
||||
|
||||
// Click outside to close the dropdown and trigger the update
|
||||
await page.locator('.issue-content-left').click();
|
||||
|
||||
// Verify both projects are shown in the sidebar
|
||||
await expect(page.locator(`.item.sidebar-project-card:has-text("${project1Title}")`)).toBeVisible();
|
||||
await expect(page.locator(`.item.sidebar-project-card:has-text("${project2Title}")`)).toBeVisible();
|
||||
} finally {
|
||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
||||
}
|
||||
});
|
||||
|
||||
test('create issue with multiple projects pre-selected', async ({page}) => {
|
||||
const repoName = `e2e-issue-multi-proj-${Date.now()}`;
|
||||
const project1Title = 'Project One';
|
||||
const project2Title = 'Project Two';
|
||||
const issueTitle = 'Issue with multiple projects';
|
||||
|
||||
await login(page);
|
||||
await apiCreateRepo(page.request, {name: repoName, autoInit: false});
|
||||
|
||||
try {
|
||||
// Create two projects via UI
|
||||
const project1 = await createProject(page, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: project1Title,
|
||||
});
|
||||
const project2 = await createProject(page, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: project2Title,
|
||||
});
|
||||
|
||||
// Navigate to new issue page
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/issues/new`);
|
||||
|
||||
// Fill in the issue title
|
||||
await page.locator('input[name="title"]').fill(issueTitle);
|
||||
|
||||
// Open the projects dropdown
|
||||
await page.locator('.sidebar-project-combo > .ui.dropdown').click();
|
||||
|
||||
// Select both projects
|
||||
await page.locator(`.sidebar-project-combo > .ui.dropdown .item[data-value="${project1.id}"]`).click();
|
||||
await page.locator(`.sidebar-project-combo > .ui.dropdown .item[data-value="${project2.id}"]`).click();
|
||||
|
||||
// Click outside to close the dropdown
|
||||
await page.locator('.issue-content-left').click();
|
||||
|
||||
// Submit the form
|
||||
await page.getByRole('button', {name: 'Create Issue'}).click();
|
||||
|
||||
// Wait for issue to be created and page to redirect
|
||||
await page.waitForURL(new RegExp(`/${env.GITEA_TEST_E2E_USER}/${repoName}/issues/\\d+`));
|
||||
|
||||
// Verify both projects are shown in the sidebar
|
||||
await expect(page.locator(`.item.sidebar-project-card:has-text("${project1Title}")`)).toBeVisible();
|
||||
await expect(page.locator(`.item.sidebar-project-card:has-text("${project2Title}")`)).toBeVisible();
|
||||
} finally {
|
||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
||||
}
|
||||
});
|
||||
|
||||
test('filter issues by multiple projects in issue list', async ({page}) => {
|
||||
const repoName = `e2e-filter-projects-${Date.now()}`;
|
||||
const project1Title = 'Filter Project A';
|
||||
const project2Title = 'Filter Project B';
|
||||
|
||||
await login(page);
|
||||
await apiCreateRepo(page.request, {name: repoName, autoInit: false});
|
||||
|
||||
try {
|
||||
// Create two projects via UI
|
||||
const project1 = await createProject(page, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: project1Title,
|
||||
});
|
||||
const project2 = await createProject(page, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: project2Title,
|
||||
});
|
||||
|
||||
// Create issues: one in project1, one in project2, one in both
|
||||
await apiCreateIssue(page.request, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: 'Issue in Project A only',
|
||||
projects: [project1.id],
|
||||
});
|
||||
await apiCreateIssue(page.request, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: 'Issue in Project B only',
|
||||
projects: [project2.id],
|
||||
});
|
||||
await apiCreateIssue(page.request, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: 'Issue in both projects',
|
||||
projects: [project1.id, project2.id],
|
||||
});
|
||||
// Create an issue with no project
|
||||
await apiCreateIssue(page.request, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: 'Issue with no project',
|
||||
});
|
||||
|
||||
// Verify only project1 issues are visible
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/issues?project=${project1.id}`);
|
||||
await expect(page.locator('#issue-list')).toContainText('Issue in Project A only');
|
||||
await expect(page.locator('#issue-list')).toContainText('Issue in both projects');
|
||||
await expect(page.locator('#issue-list')).not.toContainText('Issue in Project B only');
|
||||
await expect(page.locator('#issue-list')).not.toContainText('Issue with no project');
|
||||
|
||||
// Verify only project2 issues are visible
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/issues?project=${project2.id}`);
|
||||
await expect(page.locator('#issue-list')).toContainText('Issue in Project B only');
|
||||
await expect(page.locator('#issue-list')).toContainText('Issue in both projects');
|
||||
await expect(page.locator('#issue-list')).not.toContainText('Issue in Project A only');
|
||||
await expect(page.locator('#issue-list')).not.toContainText('Issue with no project');
|
||||
} finally {
|
||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
||||
}
|
||||
});
|
||||
|
||||
test('remove issue from one project keeping others', async ({page}) => {
|
||||
const repoName = `e2e-remove-project-${Date.now()}`;
|
||||
const project1Title = 'Keep This Project';
|
||||
const project2Title = 'Remove This Project';
|
||||
const issueTitle = 'Issue to modify projects';
|
||||
|
||||
await login(page);
|
||||
await apiCreateRepo(page.request, {name: repoName, autoInit: false});
|
||||
|
||||
try {
|
||||
// Create two projects via UI
|
||||
const project1 = await createProject(page, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: project1Title,
|
||||
});
|
||||
const project2 = await createProject(page, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: project2Title,
|
||||
});
|
||||
|
||||
// Create an issue in both projects
|
||||
const issue = await apiCreateIssue(page.request, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: issueTitle,
|
||||
projects: [project1.id, project2.id],
|
||||
});
|
||||
|
||||
// Navigate to the issue page
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/issues/${issue.index}`);
|
||||
|
||||
// Verify both projects are initially shown
|
||||
await expect(page.locator(`.item.sidebar-project-card:has-text("${project1Title}")`)).toBeVisible();
|
||||
await expect(page.locator(`.item.sidebar-project-card.item:has-text("${project2Title}")`)).toBeVisible();
|
||||
|
||||
// Open the projects dropdown
|
||||
await page.locator('.sidebar-project-combo > .ui.dropdown').click();
|
||||
|
||||
// Deselect project2 (click on the already selected item to deselect)
|
||||
await page.locator(`.sidebar-project-combo > .ui.dropdown .item[data-value="${project2.id}"]`).click();
|
||||
|
||||
// Click outside to close the dropdown and trigger the update
|
||||
await page.locator('.issue-content-left').click();
|
||||
|
||||
// Verify project1 is still shown but project2 is removed
|
||||
await expect(page.locator(`.item.sidebar-project-card.item:has-text("${project1Title}")`)).toBeVisible();
|
||||
await expect(page.locator(`.item.sidebar-project-card.item:has-text("${project2Title}")`)).toBeHidden();
|
||||
|
||||
// Reload the page to see the timeline comment
|
||||
await page.reload();
|
||||
|
||||
// Verify the timeline shows "removed this from the project" comment
|
||||
const timelineComments = page.locator('.timeline-item.event');
|
||||
await expect(timelineComments.filter({hasText: 'removed this from the'})).toBeVisible();
|
||||
} finally {
|
||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
||||
}
|
||||
});
|
||||
|
||||
test('filter issues with no project using project=-1', async ({page}) => {
|
||||
const repoName = `e2e-no-project-filter-${Date.now()}`;
|
||||
const projectTitle = 'Some Project';
|
||||
|
||||
await login(page);
|
||||
await apiCreateRepo(page.request, {name: repoName, autoInit: false});
|
||||
|
||||
try {
|
||||
// Create a project via UI
|
||||
const project = await createProject(page, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: projectTitle,
|
||||
});
|
||||
|
||||
// Create an issue with a project
|
||||
await apiCreateIssue(page.request, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: 'Issue with project assigned',
|
||||
projects: [project.id],
|
||||
});
|
||||
|
||||
// Create issues with no project
|
||||
await apiCreateIssue(page.request, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: 'Issue without any project',
|
||||
});
|
||||
await apiCreateIssue(page.request, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: 'Another unassigned issue',
|
||||
});
|
||||
|
||||
// First verify we can see all issues without the filter
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/issues?type=all&state=open`);
|
||||
await expect(page.locator('#issue-list')).toContainText('Issue with project assigned');
|
||||
await expect(page.locator('#issue-list')).toContainText('Issue without any project');
|
||||
await expect(page.locator('#issue-list')).toContainText('Another unassigned issue');
|
||||
|
||||
// Navigate to issue list filtering for issues with no project (project=-1)
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/issues?type=all&state=open&project=-1`);
|
||||
|
||||
// Verify only issues with no project are visible
|
||||
await expect(page.locator('#issue-list')).toContainText('Issue without any project');
|
||||
await expect(page.locator('#issue-list')).toContainText('Another unassigned issue');
|
||||
|
||||
// Verify the issue with a project is NOT visible
|
||||
await expect(page.locator('#issue-list')).not.toContainText('Issue with project assigned');
|
||||
|
||||
// Verify the last item in the list is NOT the issue with a project
|
||||
const issueItems = page.locator('#issue-list .item');
|
||||
const lastIssueItem = issueItems.last();
|
||||
await expect(lastIssueItem).not.toContainText('Issue with project assigned');
|
||||
} finally {
|
||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
||||
}
|
||||
});
|
||||
|
||||
test('close project and view in closed projects list', async ({page}) => {
|
||||
const repoName = `e2e-close-project-${Date.now()}`;
|
||||
const openProjectTitle = 'Open Project';
|
||||
const closedProjectTitle = 'Project To Close';
|
||||
|
||||
await login(page);
|
||||
await apiCreateRepo(page.request, {name: repoName, autoInit: false});
|
||||
|
||||
try {
|
||||
// Create two projects via UI
|
||||
await createProject(page, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: openProjectTitle,
|
||||
});
|
||||
const projectToClose = await createProject(page, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: closedProjectTitle,
|
||||
});
|
||||
|
||||
// Navigate to projects list
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/projects`);
|
||||
|
||||
// Verify both projects are visible in open state
|
||||
await expect(page.locator('.milestone-list')).toContainText(openProjectTitle);
|
||||
await expect(page.locator('.milestone-list')).toContainText(closedProjectTitle);
|
||||
|
||||
// Close the second project by clicking the close link
|
||||
const projectCard = page.locator('.milestone-list > .item').filter({hasText: closedProjectTitle});
|
||||
await projectCard.locator('a.link-action[data-url$="/close"]').click();
|
||||
|
||||
// Wait for redirect back to project view page
|
||||
await page.waitForURL(new RegExp(`/${env.GITEA_TEST_E2E_USER}/${repoName}/projects/${projectToClose.id}`));
|
||||
|
||||
// Navigate to projects list
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/projects`);
|
||||
|
||||
// Click on "Closed" tab to view closed projects
|
||||
await page.locator('.list-header-toggle a.item').filter({hasText: 'Closed'}).click();
|
||||
|
||||
// Wait for the page to load with closed projects
|
||||
await page.waitForURL(/state=closed/);
|
||||
|
||||
// Verify only the closed project is visible
|
||||
await expect(page.locator('.milestone-list')).toContainText(closedProjectTitle);
|
||||
await expect(page.locator('.milestone-list')).not.toContainText(openProjectTitle);
|
||||
|
||||
// Verify the "Closed" tab is active
|
||||
await expect(page.locator('.list-header-toggle a.item.active')).toContainText('Closed');
|
||||
} finally {
|
||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
||||
}
|
||||
});
|
||||
|
||||
test('select projects on new issue page shows in sidebar', async ({page}) => {
|
||||
const repoName = `e2e-new-issue-project-${Date.now()}`;
|
||||
const project1Title = 'Project One';
|
||||
const project2Title = 'Project Two';
|
||||
|
||||
await login(page);
|
||||
await apiCreateRepo(page.request, {name: repoName, autoInit: false});
|
||||
|
||||
try {
|
||||
// Create two projects
|
||||
const project1 = await createProject(page, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: project1Title,
|
||||
});
|
||||
const project2 = await createProject(page, {
|
||||
owner: env.GITEA_TEST_E2E_USER,
|
||||
repo: repoName,
|
||||
title: project2Title,
|
||||
});
|
||||
|
||||
// Navigate to new issue page
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/issues/new`);
|
||||
|
||||
// Open the projects dropdown in the sidebar
|
||||
await page.locator('.sidebar-project-combo > .ui.dropdown').click();
|
||||
|
||||
// Select both projects
|
||||
await page.locator(`.sidebar-project-combo > .ui.dropdown .item[data-value="${project1.id}"]`).click();
|
||||
await page.locator(`.sidebar-project-combo > .ui.dropdown .item[data-value="${project2.id}"]`).click();
|
||||
|
||||
// Click outside to close dropdown
|
||||
await page.locator('.issue-content-left').click();
|
||||
|
||||
// Verify both projects appear in the sidebar list below the dropdown
|
||||
// On new issue page, these are simple cloned items rendered in the list container
|
||||
const projectList = page.locator('.sidebar-project-combo > .ui.list');
|
||||
await expect(projectList.locator(`.item:has-text("${project1Title}")`).first()).toBeVisible();
|
||||
await expect(projectList.locator(`.item:has-text("${project2Title}")`).first()).toBeVisible();
|
||||
} finally {
|
||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
||||
}
|
||||
});
|
||||
9
tests/e2e/licenses.test.ts
Normal file
9
tests/e2e/licenses.test.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
|
||||
test('licenses.txt', async ({page}) => {
|
||||
const resp = await page.goto('/assets/licenses.txt');
|
||||
expect(resp?.status()).toBe(200);
|
||||
const content = await resp!.text();
|
||||
expect(content).toContain('@vue/');
|
||||
expect(content).toContain('gitea.dev/');
|
||||
});
|
||||
17
tests/e2e/login.test.ts
Normal file
17
tests/e2e/login.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {logout} from './utils.ts';
|
||||
|
||||
test('homepage', async ({page}) => {
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('img', {name: 'Logo'})).toHaveAttribute('src', '/assets/img/logo.svg');
|
||||
});
|
||||
|
||||
test('login form and logout', async ({page}) => {
|
||||
await page.goto('/user/login');
|
||||
await page.getByLabel('Username or Email Address').fill(env.GITEA_TEST_E2E_USER);
|
||||
await page.getByLabel('Password').fill(env.GITEA_TEST_E2E_PASSWORD);
|
||||
await page.getByRole('button', {name: 'Sign In'}).click();
|
||||
await expect(page.getByRole('link', {name: 'Sign In'})).toBeHidden();
|
||||
await logout(page);
|
||||
});
|
||||
17
tests/e2e/mermaid.test.ts
Normal file
17
tests/e2e/mermaid.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {env} from 'node:process';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {apiCreateRepo, apiCreateIssue, assertNoJsError, randomString} from './utils.ts';
|
||||
|
||||
test('mermaid diagram in issue', async ({page, request}) => {
|
||||
const repoName = `e2e-mermaid-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
await apiCreateRepo(request, {name: repoName, autoInit: false});
|
||||
const body = '```mermaid\nflowchart LR\n Alpha --> Beta\n Beta --> Gamma\n```\n';
|
||||
const {index} = await apiCreateIssue(request, {owner, repo: repoName, title: 'mermaid test', body});
|
||||
await page.goto(`/${owner}/${repoName}/issues/${index}`);
|
||||
|
||||
const svg = page.frameLocator('iframe.markup-content-iframe').locator('svg');
|
||||
await expect(svg).toContainText(/Alpha[\s\S]*Beta[\s\S]*Gamma/);
|
||||
|
||||
await assertNoJsError(page);
|
||||
});
|
||||
12
tests/e2e/milestone.test.ts
Normal file
12
tests/e2e/milestone.test.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, apiCreateRepo, randomString} from './utils.ts';
|
||||
|
||||
test('create a milestone', async ({page}) => {
|
||||
const repoName = `e2e-milestone-${randomString(8)}`;
|
||||
await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName, autoInit: false})]);
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/milestones/new`);
|
||||
await page.getByPlaceholder('Title').fill('Test Milestone');
|
||||
await page.getByRole('button', {name: 'Create Milestone'}).click();
|
||||
await expect(page.locator('.milestone-list')).toContainText('Test Milestone');
|
||||
});
|
||||
52
tests/e2e/org.test.ts
Normal file
52
tests/e2e/org.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, apiCreateOrg, apiCreateTeam, apiCreateUser, apiDeleteOrg, randomString} from './utils.ts';
|
||||
|
||||
test('create an organization', async ({page}) => {
|
||||
const orgName = `e2e-org-${randomString(8)}`;
|
||||
await login(page);
|
||||
await page.goto('/org/create');
|
||||
await page.getByLabel('Organization Name').fill(orgName);
|
||||
await page.getByRole('button', {name: 'Create Organization'}).click();
|
||||
await expect(page).toHaveURL(new RegExp(`/org/${orgName}`));
|
||||
// delete via API because of issues related to form-fetch-action
|
||||
await apiDeleteOrg(page.request, orgName);
|
||||
});
|
||||
|
||||
test('add team member search', async ({page, request}) => {
|
||||
const orgName = `team-add-${randomString(8)}`;
|
||||
const teamName = `team-add-${randomString(8)}`;
|
||||
const userName = `team-add-${randomString(8)}`;
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
await apiCreateOrg(request, orgName);
|
||||
await apiCreateTeam(request, orgName, teamName);
|
||||
})(),
|
||||
apiCreateUser(request, userName),
|
||||
login(page),
|
||||
]);
|
||||
|
||||
await page.goto(`/org/${orgName}/teams/${teamName}`);
|
||||
const input = page.locator('#search-user-box input.prompt');
|
||||
await input.fill(userName.slice(-6));
|
||||
const result = page.locator('#search-user-box .results .result').first();
|
||||
await expect(result).toContainText(userName);
|
||||
});
|
||||
|
||||
test('delete team via confirm modal', async ({page, request}) => {
|
||||
const orgName = `e2e-del-team-${randomString(8)}`;
|
||||
const teamName = `team-${randomString(8)}`;
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
await apiCreateOrg(request, orgName);
|
||||
await apiCreateTeam(request, orgName, teamName);
|
||||
})(),
|
||||
login(page),
|
||||
]);
|
||||
|
||||
await page.goto(`/org/${orgName}/teams/${teamName}/edit`);
|
||||
await page.getByRole('button', {name: 'Delete Team'}).click();
|
||||
await page.getByRole('button', {name: 'Yes'}).click();
|
||||
await expect(page).toHaveURL(new RegExp(`/org/${orgName}/teams$`));
|
||||
await expect(page.getByText('The team has been deleted.')).toBeVisible();
|
||||
});
|
||||
23
tests/e2e/pr-create.test.ts
Normal file
23
tests/e2e/pr-create.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiCreateFiles, randomString} from './utils.ts';
|
||||
|
||||
test('create a pull request from the compare page', async ({page, request}) => {
|
||||
const repoName = `e2e-pr-create-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
await apiCreateRepo(request, {name: repoName});
|
||||
await Promise.all([
|
||||
apiCreateFiles(request, owner, repoName, [{path: 'feat.txt', content: 'feature content\n'}], {branch: 'main', newBranch: 'feat'}),
|
||||
login(page),
|
||||
]);
|
||||
// expand=1 renders the PR form directly, skipping the "New Pull Request" toggle click
|
||||
await page.goto(`/${owner}/${repoName}/compare/main...feat?expand=1`);
|
||||
|
||||
const title = `e2e-pr-${randomString(8)}`;
|
||||
await page.getByPlaceholder('Title').fill(title);
|
||||
await page.getByRole('button', {name: 'Create Pull Request'}).click();
|
||||
|
||||
// commit, not full load: the PR title heading is server-rendered, so the assertion can resolve before the heavy diff/timeline finishes
|
||||
await page.waitForURL(new RegExp(`/${owner}/${repoName}/pulls/\\d+$`), {waitUntil: 'commit'});
|
||||
await expect(page.getByRole('heading', {name: title})).toBeVisible();
|
||||
});
|
||||
87
tests/e2e/pr-diff-filter.test.ts
Normal file
87
tests/e2e/pr-diff-filter.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {apiCreateFiles, apiCreatePR, apiCreateRepo, apiCreateUser, apiUserHeaders, loginUser, randomString} from './utils.ts';
|
||||
|
||||
test('diff sidebar filtering', async ({page, request}) => {
|
||||
const user = `df-${randomString(8)}`;
|
||||
await apiCreateUser(request, user);
|
||||
const headers = apiUserHeaders(user);
|
||||
const repo = `e2e-difffilter-${randomString(8)}`;
|
||||
await apiCreateRepo(request, {name: repo, headers});
|
||||
|
||||
await apiCreateFiles(request, user, repo, [
|
||||
{path: 'src/a.ts', content: 'a\n'},
|
||||
{path: 'src/b.ts', content: 'b\n'},
|
||||
{path: 'src/.eslintrc', content: 'e\n'}, // dotfiles count as "No extension"
|
||||
{path: 'styles/x.css', content: 'x\n'},
|
||||
{path: 'docs/intro.md', content: 'r\n'},
|
||||
{path: 'Makefile', content: 'm\n'},
|
||||
], {branch: 'main', newBranch: 'feat', headers});
|
||||
const prIndex = await apiCreatePR(request, user, repo, 'feat', 'main', 'diff filter test', {headers});
|
||||
|
||||
await loginUser(page, user);
|
||||
await page.goto(`/${user}/${repo}/pulls/${prIndex}/files`);
|
||||
|
||||
const tree = page.locator('#diff-file-tree');
|
||||
const items = tree.locator('.item-file');
|
||||
const search = tree.getByRole('textbox');
|
||||
const filterTrigger = tree.getByRole('button', {name: 'Filter by file extension'});
|
||||
|
||||
// every PR file is listed
|
||||
await expect(items).toHaveCount(6);
|
||||
|
||||
// sidebar leaves the diff column the bulk of the viewport
|
||||
const boxesWidth = (await page.locator('#diff-file-boxes').boundingBox())!.width;
|
||||
const treeWidth = (await tree.boundingBox())!.width;
|
||||
expect(boxesWidth).toBeGreaterThan(treeWidth * 2);
|
||||
|
||||
// search filters tree and file boxes
|
||||
await search.fill('a.ts');
|
||||
await expect(items).toHaveText([/a\.ts/]);
|
||||
await expect(page.locator('.diff-file-box[data-new-filename="src/a.ts"]')).toBeVisible();
|
||||
|
||||
await tree.getByRole('button', {name: 'Clear filter'}).click();
|
||||
await expect(items).toHaveCount(6);
|
||||
|
||||
// empty-result placeholder
|
||||
await search.fill('zzz-no-such-file');
|
||||
await expect(page.locator('#diff-no-matches')).toBeVisible();
|
||||
await search.fill('');
|
||||
|
||||
// extensions sort alphabetically, then dotfiles, then files without extension
|
||||
await filterTrigger.click();
|
||||
const extItems = page.getByRole('menuitemcheckbox');
|
||||
await expect(extItems).toHaveText(['.css1', '.md1', '.ts2', 'Dotfiles1', 'No extension1', 'All extensions']);
|
||||
|
||||
// deselecting .ts leaves the other extensions
|
||||
const allExtensions = page.getByRole('menuitemcheckbox', {name: 'All extensions'});
|
||||
await page.getByRole('menuitemcheckbox', {name: '.ts'}).click();
|
||||
await expect(items).toHaveCount(4);
|
||||
await expect(filterTrigger).toHaveClass(/\bindicator-dot\b/);
|
||||
await expect(allExtensions).toHaveAttribute('aria-checked', 'false');
|
||||
|
||||
// "All extensions" cycles through select all, select none and back
|
||||
await allExtensions.click();
|
||||
await expect(allExtensions).toHaveAttribute('aria-checked', 'true');
|
||||
await expect(items).toHaveCount(6);
|
||||
|
||||
await allExtensions.click();
|
||||
await expect(items).toHaveCount(0);
|
||||
|
||||
await allExtensions.click();
|
||||
await expect(items).toHaveCount(6);
|
||||
await expect(filterTrigger).not.toHaveClass(/\bindicator-dot\b/);
|
||||
|
||||
// the extension filter lives in the URL and survives a reload
|
||||
await page.getByRole('menuitemcheckbox', {name: '.ts'}).click();
|
||||
await expect(page).toHaveURL(/file-filters/);
|
||||
await page.reload();
|
||||
await expect(items).toHaveCount(4);
|
||||
await expect(filterTrigger).toHaveClass(/\bindicator-dot\b/);
|
||||
|
||||
// hiding the file tree drops the filter
|
||||
await expect(page.locator('.diff-file-box[data-new-filename="src/a.ts"]')).toBeHidden();
|
||||
await filterTrigger.click();
|
||||
await page.locator('.diff-toggle-file-tree-button').click();
|
||||
await expect(tree).toBeHidden();
|
||||
await expect(page.locator('.diff-file-box[data-new-filename="src/a.ts"]')).toBeVisible();
|
||||
});
|
||||
114
tests/e2e/pr-review.test.ts
Normal file
114
tests/e2e/pr-review.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {
|
||||
apiAddCollaborator,
|
||||
apiCreateFiles,
|
||||
apiCreatePR,
|
||||
apiCreateRepo,
|
||||
apiCreateReview,
|
||||
apiCreateUser,
|
||||
apiUserHeaders,
|
||||
loginUser,
|
||||
randomString,
|
||||
} from './utils.ts';
|
||||
|
||||
test('pr review flow', async ({page, request}) => {
|
||||
const poster = `rv-poster-${randomString(8)}`;
|
||||
const reviewer = `rv-reviewer-${randomString(8)}`;
|
||||
const officialReviewer = `rv-official-${randomString(8)}`;
|
||||
await Promise.all([
|
||||
apiCreateUser(request, poster),
|
||||
apiCreateUser(request, reviewer),
|
||||
apiCreateUser(request, officialReviewer),
|
||||
]);
|
||||
const posterHeaders = apiUserHeaders(poster);
|
||||
const repoName = `e2e-prreview-${randomString(8)}`;
|
||||
await apiCreateRepo(request, {name: repoName, headers: posterHeaders});
|
||||
await Promise.all([
|
||||
apiAddCollaborator(
|
||||
request,
|
||||
poster,
|
||||
repoName,
|
||||
officialReviewer,
|
||||
{headers: posterHeaders},
|
||||
),
|
||||
apiCreateFiles(
|
||||
request,
|
||||
poster,
|
||||
repoName,
|
||||
[{path: 'added.txt', content: 'new content\n'}],
|
||||
{branch: 'main', newBranch: 'feat', headers: posterHeaders},
|
||||
),
|
||||
]);
|
||||
const prIndex = await apiCreatePR(request, poster, repoName, 'feat', 'main', 'review test', {headers: posterHeaders});
|
||||
const pullUrl = `/${poster}/${repoName}/pulls/${prIndex}`;
|
||||
|
||||
const reRequestAndApprove = async (
|
||||
username: string,
|
||||
approvalTooltip: string,
|
||||
pendingStateClass: string,
|
||||
official: boolean,
|
||||
) => {
|
||||
await page.goto(pullUrl);
|
||||
const reviewerItem = page.locator('.issue-sidebar-combo .ui.relaxed.list > .item', {
|
||||
has: page.locator(`a[href="/${username}"]`),
|
||||
});
|
||||
const reviewState = reviewerItem.locator('span[data-tooltip-content]');
|
||||
await expect(reviewState).toHaveAttribute('data-tooltip-content', approvalTooltip);
|
||||
await reviewerItem.locator('a[data-tooltip-content="Re-request review"]').click();
|
||||
await expect(reviewState).toHaveAttribute('data-tooltip-content', 'Review pending');
|
||||
await expect(reviewState.locator(`.octicon-dot-fill.${pendingStateClass}`)).toBeVisible();
|
||||
|
||||
expect(await apiCreateReview(request, poster, repoName, prIndex, {
|
||||
event: 'APPROVED',
|
||||
headers: apiUserHeaders(username),
|
||||
})).toMatchObject({state: 'APPROVED', official});
|
||||
};
|
||||
|
||||
// reviewer seeds an inline comment via API so the poster's UI reply exercises the reply-to-review path (#35994)
|
||||
await Promise.all([
|
||||
apiCreateReview(request, poster, repoName, prIndex, {
|
||||
comments: [{path: 'added.txt', body: 'inline to reply to', new_position: 1}],
|
||||
headers: apiUserHeaders(reviewer),
|
||||
}),
|
||||
apiCreateReview(request, poster, repoName, prIndex, {
|
||||
event: 'APPROVED',
|
||||
headers: apiUserHeaders(officialReviewer),
|
||||
}),
|
||||
loginUser(page, poster),
|
||||
]);
|
||||
|
||||
await page.goto(`${pullUrl}/files`);
|
||||
|
||||
// diff viewer renders the added file with its header and one added-line row
|
||||
const fileBox = page.locator('.diff-file-box[data-new-filename="added.txt"]');
|
||||
await expect(fileBox.locator('.diff-file-header .file-link')).toHaveText('added.txt');
|
||||
await expect(fileBox.locator('tr.add-code')).toHaveCount(1);
|
||||
|
||||
// commits tab badge reflects the single PR commit, and the diff stats header counts one changed file
|
||||
const commitsTab = page.locator('.ui.pull.tabular.menu a.item', {has: page.locator('.octicon-git-commit')});
|
||||
await expect(commitsTab.locator('.label')).toHaveText('1');
|
||||
await expect(page.locator('.diff-detail-stats')).toContainText(/1 changed file/);
|
||||
|
||||
// poster replies to the reviewer's inline comment
|
||||
const conversation = fileBox.locator('.conversation-holder');
|
||||
await conversation.locator('.comment-form-reply').click();
|
||||
const replyForm = conversation.locator('form');
|
||||
await replyForm.locator('textarea[name="content"]').fill('my reply body');
|
||||
await replyForm.getByRole('button', {name: 'Reply', exact: true}).click();
|
||||
await expect(conversation.locator('.comment-body')).toContainText(['inline to reply to', 'my reply body']);
|
||||
|
||||
await page.context().clearCookies();
|
||||
await loginUser(page, reviewer);
|
||||
await page.goto(`${pullUrl}/files`);
|
||||
await page.locator('#review-box .js-btn-review').click();
|
||||
const panel = page.locator('.review-box-panel');
|
||||
await panel.locator('textarea[name="content"]').fill(`First approval from ${reviewer}`);
|
||||
await panel.getByRole('button', {name: 'Approve', exact: true}).click();
|
||||
await expect(page.locator('.timeline-item').filter({hasText: `First approval from ${reviewer}`})).toBeVisible();
|
||||
|
||||
await reRequestAndApprove(reviewer, 'Uncounted approval', 'tw-text-text-light', false);
|
||||
|
||||
await page.context().clearCookies();
|
||||
await loginUser(page, officialReviewer);
|
||||
await reRequestAndApprove(officialReviewer, 'Approved', 'tw-text-yellow', true);
|
||||
});
|
||||
23
tests/e2e/pull-merge-box.test.ts
Normal file
23
tests/e2e/pull-merge-box.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {apiCreateFiles, apiCreatePR, apiCreateRepo, assertNoJsError, login, randomString} from './utils.ts';
|
||||
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
|
||||
test('merge box merges a pull request', async ({page, request}) => {
|
||||
const repo = `e2e-merge-box-${randomString(8)}`;
|
||||
const createPR = (async () => {
|
||||
await apiCreateRepo(request, {name: repo});
|
||||
await apiCreateFiles(request, owner, repo, [{path: 'feat.txt', content: 'feature\n'}], {branch: 'main', newBranch: 'feat'});
|
||||
return apiCreatePR(request, owner, repo, 'feat', 'main', 'merge box test');
|
||||
})();
|
||||
const [index] = await Promise.all([createPR, login(page)]);
|
||||
await page.goto(`/${owner}/${repo}/pulls/${index}`, {waitUntil: 'commit'});
|
||||
|
||||
// expand the merge form, then submit the merge
|
||||
await page.getByRole('button', {name: 'Create merge commit'}).click();
|
||||
await page.locator('form.form-fetch-action').getByRole('button', {name: 'Create merge commit'}).click();
|
||||
|
||||
await expect(page.getByText(/successfully merged/i)).toBeVisible();
|
||||
await assertNoJsError(page);
|
||||
});
|
||||
26
tests/e2e/reactions.test.ts
Normal file
26
tests/e2e/reactions.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {env} from 'node:process';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiCreateIssue, randomString} from './utils.ts';
|
||||
|
||||
test('toggle issue reactions', async ({page, request}) => {
|
||||
const repoName = `e2e-reactions-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
await apiCreateRepo(request, {name: repoName, autoInit: false});
|
||||
await Promise.all([
|
||||
apiCreateIssue(request, {owner, repo: repoName, title: 'Reaction test'}),
|
||||
login(page),
|
||||
]);
|
||||
await page.goto(`/${owner}/${repoName}/issues/1`);
|
||||
|
||||
const issueComment = page.locator('.timeline-item.comment.issue-content-comment');
|
||||
|
||||
const reactionPicker = issueComment.locator('.select-reaction');
|
||||
await reactionPicker.click();
|
||||
await reactionPicker.getByLabel('+1').click();
|
||||
|
||||
const reactions = issueComment.getByRole('group', {name: 'Reactions'});
|
||||
await expect(reactions.getByRole('button', {name: /^\+1:/})).toContainText('1');
|
||||
|
||||
await reactions.getByRole('button', {name: /^\+1:/}).click();
|
||||
await expect(reactions.getByRole('button', {name: /^\+1:/})).toHaveCount(0);
|
||||
});
|
||||
10
tests/e2e/readme.test.ts
Normal file
10
tests/e2e/readme.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {apiCreateRepo, randomString} from './utils.ts';
|
||||
|
||||
test('repo readme', async ({page}) => {
|
||||
const repoName = `e2e-readme-${randomString(8)}`;
|
||||
await apiCreateRepo(page.request, {name: repoName});
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}`);
|
||||
await expect(page.locator('#readme')).toContainText(repoName);
|
||||
});
|
||||
67
tests/e2e/register.test.ts
Normal file
67
tests/e2e/register.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, logout, randomString} from './utils.ts';
|
||||
|
||||
test.beforeEach(async ({page}) => {
|
||||
await page.goto('/user/sign_up');
|
||||
});
|
||||
|
||||
test('register page has form', async ({page}) => {
|
||||
await expect(page.getByLabel('Username')).toBeVisible();
|
||||
await expect(page.getByLabel('Email Address')).toBeVisible();
|
||||
await expect(page.getByLabel('Password', {exact: true})).toBeVisible();
|
||||
await expect(page.getByLabel('Confirm Password')).toBeVisible();
|
||||
await expect(page.getByRole('button', {name: 'Register Account'})).toBeVisible();
|
||||
});
|
||||
|
||||
test('register with empty fields shows error', async ({page}) => {
|
||||
// HTML5 required attribute prevents submission, so verify the fields are required
|
||||
await expect(page.locator('input[name="user_name"][required]')).toBeVisible();
|
||||
await expect(page.locator('input[name="email"][required]')).toBeVisible();
|
||||
await expect(page.locator('input[name="password"][required]')).toBeVisible();
|
||||
await expect(page.locator('input[name="retype"][required]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('register with mismatched passwords shows error', async ({page}) => {
|
||||
await page.getByLabel('Username').fill('e2e-register-mismatch');
|
||||
await page.getByLabel('Email Address').fill(`e2e-register-mismatch@${env.GITEA_TEST_E2E_DOMAIN}`);
|
||||
await page.getByLabel('Password', {exact: true}).fill('password123!');
|
||||
await page.getByLabel('Confirm Password').fill('different123!');
|
||||
await page.getByRole('button', {name: 'Register Account'}).click();
|
||||
await expect(page.locator('.ui.negative.message')).toBeVisible();
|
||||
});
|
||||
|
||||
test('register then login', async ({page}) => {
|
||||
const username = `e2e-register-${randomString(8)}`;
|
||||
const email = `${username}@${env.GITEA_TEST_E2E_DOMAIN}`;
|
||||
const password = 'password123!';
|
||||
|
||||
await page.getByLabel('Username').fill(username);
|
||||
await page.getByLabel('Email Address').fill(email);
|
||||
await page.getByLabel('Password', {exact: true}).fill(password);
|
||||
await page.getByLabel('Confirm Password').fill(password);
|
||||
await page.getByRole('button', {name: 'Register Account'}).click();
|
||||
|
||||
// After successful registration, should be redirected away from sign_up
|
||||
await expect(page).not.toHaveURL(/sign_up/);
|
||||
|
||||
// Logout then login with the newly created account
|
||||
await logout(page);
|
||||
await login(page, username, password);
|
||||
});
|
||||
|
||||
test('register with existing username shows error', async ({page}) => {
|
||||
await page.getByLabel('Username').fill(env.GITEA_TEST_E2E_USER);
|
||||
await page.getByLabel('Email Address').fill(`e2e-duplicate@${env.GITEA_TEST_E2E_DOMAIN}`);
|
||||
await page.getByLabel('Password', {exact: true}).fill('password123!');
|
||||
await page.getByLabel('Confirm Password').fill('password123!');
|
||||
await page.getByRole('button', {name: 'Register Account'}).click();
|
||||
await expect(page.locator('.ui.negative.message')).toBeVisible();
|
||||
});
|
||||
|
||||
test('sign in link exists', async ({page}) => {
|
||||
const signInLink = page.getByText('Sign in now!');
|
||||
await expect(signInLink).toBeVisible();
|
||||
await signInLink.click();
|
||||
await expect(page).toHaveURL(/\/user\/login$/);
|
||||
});
|
||||
10
tests/e2e/relative-time.test.ts
Normal file
10
tests/e2e/relative-time.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {assertNoJsError} from './utils.ts';
|
||||
|
||||
test('relative-time renders without errors', async ({page}) => {
|
||||
await page.goto('/devtest/relative-time');
|
||||
const relativeTime = page.getByTestId('relative-time-now');
|
||||
await expect(relativeTime).toHaveAttribute('data-tooltip-content', /.+/);
|
||||
await expect(relativeTime).toHaveText('now');
|
||||
await assertNoJsError(page);
|
||||
});
|
||||
19
tests/e2e/release.test.ts
Normal file
19
tests/e2e/release.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, apiCreateRepo, randomString} from './utils.ts';
|
||||
|
||||
test('create a release', async ({page, request}) => {
|
||||
const repoName = `e2e-release-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
await Promise.all([apiCreateRepo(request, {name: repoName}), login(page)]);
|
||||
await page.goto(`/${owner}/${repoName}/releases/new`);
|
||||
|
||||
const tag = `v1.0.0-${randomString(8)}`;
|
||||
const title = `e2e-release-${randomString(8)}`;
|
||||
await page.getByLabel('Tag name').fill(tag);
|
||||
await page.getByLabel('Release title').fill(title);
|
||||
await page.getByRole('button', {name: 'Publish Release'}).click();
|
||||
|
||||
await page.waitForURL(new RegExp(`/${owner}/${repoName}/releases$`));
|
||||
await expect(page.locator('.release-list-title')).toContainText(title);
|
||||
});
|
||||
24
tests/e2e/repo-collab.test.ts
Normal file
24
tests/e2e/repo-collab.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {apiCreateRepo, apiCreateUser, login, randomString} from './utils.ts';
|
||||
|
||||
test('add collaborator search', async ({page, request}) => {
|
||||
const userName = `repo-collab-${randomString(8)}`;
|
||||
const repoName = `repo-collab-${randomString(8)}`;
|
||||
|
||||
await Promise.all([
|
||||
apiCreateUser(request, userName),
|
||||
apiCreateRepo(request, {name: repoName, autoInit: false}),
|
||||
login(page),
|
||||
]);
|
||||
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/settings/collaboration`);
|
||||
const input = page.locator('#search-user-box input.prompt');
|
||||
await input.fill(userName.slice(-6));
|
||||
const result = page.locator('#search-user-box .results .result').first();
|
||||
await expect(result).toContainText(userName);
|
||||
await result.click();
|
||||
await expect(input).toHaveValue(userName);
|
||||
await page.getByRole('button', {name: 'Add Collaborator'}).click();
|
||||
await expect(page.locator('body')).toContainText(userName);
|
||||
});
|
||||
21
tests/e2e/repo-star-watch.test.ts
Normal file
21
tests/e2e/repo-star-watch.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiCreateUser, apiUserHeaders, randomString} from './utils.ts';
|
||||
|
||||
test('star and watch a repository', async ({page, request}) => {
|
||||
const owner = `sw-owner-${randomString(8)}`;
|
||||
const repoName = `e2e-star-watch-${randomString(8)}`;
|
||||
await apiCreateUser(request, owner);
|
||||
await Promise.all([
|
||||
apiCreateRepo(request, {name: repoName, autoInit: false, headers: apiUserHeaders(owner)}),
|
||||
login(page),
|
||||
]);
|
||||
await page.goto(`/${owner}/${repoName}`);
|
||||
|
||||
// exact match so "Star"/"Watch" don't also match "Unstar"/"Watching"
|
||||
await page.getByRole('button', {name: 'Star', exact: true}).click();
|
||||
await expect(page.getByRole('button', {name: 'Unstar'})).toBeVisible();
|
||||
|
||||
await page.getByRole('button', {name: 'Watch', exact: true}).click();
|
||||
await page.getByRole('menuitem', {name: 'All activity', exact: true}).click();
|
||||
await expect(page.getByRole('button', {name: 'Watching'})).toBeVisible();
|
||||
});
|
||||
12
tests/e2e/repo.test.ts
Normal file
12
tests/e2e/repo.test.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {env} from 'node:process';
|
||||
import {test} from '@playwright/test';
|
||||
import {login, randomString} from './utils.ts';
|
||||
|
||||
test('create a repository', async ({page}) => {
|
||||
const repoName = `e2e-repo-${randomString(8)}`;
|
||||
await login(page);
|
||||
await page.goto('/repo/create');
|
||||
await page.locator('input[name="repo_name"]').fill(repoName);
|
||||
await page.getByRole('button', {name: 'Create Repository'}).click();
|
||||
await page.waitForURL(new RegExp(`/${env.GITEA_TEST_E2E_USER}/${repoName}$`));
|
||||
});
|
||||
16
tests/e2e/user-settings.test.ts
Normal file
16
tests/e2e/user-settings.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {loginUser, apiCreateUser, randomString} from './utils.ts';
|
||||
|
||||
test('update profile biography', async ({page, request}) => {
|
||||
const username = `e2e-settings-${randomString(8)}`;
|
||||
const bio = `e2e-bio-${randomString(8)}`;
|
||||
await apiCreateUser(request, username);
|
||||
await loginUser(page, username);
|
||||
await page.goto('/user/settings');
|
||||
await page.getByLabel('Biography').fill(bio);
|
||||
await page.getByRole('button', {name: 'Update Profile'}).click();
|
||||
await expect(page.getByLabel('Biography')).toHaveValue(bio);
|
||||
await page.getByLabel('Biography').fill('');
|
||||
await page.getByRole('button', {name: 'Update Profile'}).click();
|
||||
await expect(page.getByLabel('Biography')).toHaveValue('');
|
||||
});
|
||||
244
tests/e2e/utils.ts
Normal file
244
tests/e2e/utils.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import {env} from 'node:process';
|
||||
import {expect} from '@playwright/test';
|
||||
import type {APIRequestContext, APIResponse, Locator, Page} from '@playwright/test';
|
||||
|
||||
/** Generate a random alphanumeric string. */
|
||||
export function randomString(length: number): string {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let index = 0; index < length; index++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function baseUrl() {
|
||||
return env.GITEA_TEST_E2E_URL?.replace(/\/$/g, '');
|
||||
}
|
||||
|
||||
function apiAuthHeader(username: string, password: string) {
|
||||
return {Authorization: `Basic ${globalThis.btoa(`${username}:${password}`)}`};
|
||||
}
|
||||
|
||||
export function apiHeaders() {
|
||||
return apiAuthHeader(env.GITEA_TEST_E2E_USER, env.GITEA_TEST_E2E_PASSWORD);
|
||||
}
|
||||
|
||||
async function apiRetry(fn: () => Promise<APIResponse>, label: string): Promise<APIResponse> {
|
||||
const maxAttempts = 5;
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
const response = await fn();
|
||||
if (response.ok()) return response;
|
||||
if ([500, 502, 503].includes(response.status()) && attempt < maxAttempts - 1) {
|
||||
const jitter = Math.random() * 500;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1) + jitter));
|
||||
continue;
|
||||
}
|
||||
throw new Error(`${label} failed: ${response.status()} ${await response.text()}`);
|
||||
}
|
||||
throw new Error(`${label} failed after ${maxAttempts} attempts`);
|
||||
}
|
||||
|
||||
export async function apiCreateRepo(requestContext: APIRequestContext, {name, autoInit = true, headers}: {name: string; autoInit?: boolean; headers?: Record<string, string>}) {
|
||||
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/user/repos`, {
|
||||
headers: headers || apiHeaders(),
|
||||
data: {name, auto_init: autoInit},
|
||||
}), 'apiCreateRepo');
|
||||
}
|
||||
|
||||
export async function apiAddCollaborator(requestContext: APIRequestContext, owner: string, repo: string, collaborator: string, {headers}: {headers?: Record<string, string>} = {}) {
|
||||
await apiRetry(() => requestContext.put(`${baseUrl()}/api/v1/repos/${owner}/${repo}/collaborators/${collaborator}`, {
|
||||
headers: headers || apiHeaders(),
|
||||
data: {permission: 'write'},
|
||||
}), 'apiAddCollaborator');
|
||||
}
|
||||
|
||||
export async function apiCreateOrg(requestContext: APIRequestContext, name: string, {headers}: {headers?: Record<string, string>} = {}) {
|
||||
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/orgs`, {
|
||||
headers: headers || apiHeaders(),
|
||||
data: {username: name},
|
||||
}), 'apiCreateOrg');
|
||||
}
|
||||
|
||||
export async function apiCreateTeam(requestContext: APIRequestContext, org: string, name: string, {permission = 'read', units = ['repo.code'], headers}: {permission?: string; units?: Array<string>; headers?: Record<string, string>} = {}) {
|
||||
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/orgs/${org}/teams`, {
|
||||
headers: headers || apiHeaders(),
|
||||
data: {name, permission, units},
|
||||
}), 'apiCreateTeam');
|
||||
}
|
||||
|
||||
export async function apiStartStopwatch(requestContext: APIRequestContext, owner: string, repo: string, issueIndex: number, {headers}: {headers?: Record<string, string>} = {}) {
|
||||
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/issues/${issueIndex}/stopwatch/start`, {
|
||||
headers: headers || apiHeaders(),
|
||||
}), 'apiStartStopwatch');
|
||||
}
|
||||
|
||||
/** Commit one or more files in a single API call. */
|
||||
export async function apiCreateFiles(requestContext: APIRequestContext, owner: string, repo: string, files: Array<{path: string; content: string}>, {branch, newBranch, headers}: {branch?: string; newBranch?: string; headers?: Record<string, string>} = {}) {
|
||||
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/contents`, {
|
||||
headers: headers || apiHeaders(),
|
||||
data: {
|
||||
branch, new_branch: newBranch,
|
||||
files: files.map((file) => ({operation: 'create', path: file.path, content: Buffer.from(file.content, 'utf8').toString('base64')})),
|
||||
},
|
||||
}), 'apiCreateFiles');
|
||||
}
|
||||
|
||||
export async function apiCancelStopwatch(requestContext: APIRequestContext, owner: string, repo: string, issueIndex: number, {headers}: {headers?: Record<string, string>} = {}) {
|
||||
await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/repos/${owner}/${repo}/issues/${issueIndex}/stopwatch/delete`, {
|
||||
headers: headers || apiHeaders(),
|
||||
}), 'apiCancelStopwatch');
|
||||
}
|
||||
|
||||
export async function apiCloseIssue(requestContext: APIRequestContext, owner: string, repo: string, issueIndex: number, {headers}: {headers?: Record<string, string>} = {}) {
|
||||
await apiRetry(() => requestContext.patch(`${baseUrl()}/api/v1/repos/${owner}/${repo}/issues/${issueIndex}`, {
|
||||
headers: headers || apiHeaders(),
|
||||
data: {state: 'closed'},
|
||||
}), 'apiCloseIssue');
|
||||
}
|
||||
|
||||
/** Create a PR via API. Returns the PR index for subsequent operations. */
|
||||
export async function apiCreatePR(requestContext: APIRequestContext, owner: string, repo: string, head: string, base: string, title: string, {headers}: {headers?: Record<string, string>} = {}): Promise<number> {
|
||||
const response = await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/pulls`, {
|
||||
headers: headers || apiHeaders(),
|
||||
data: {head, base, title},
|
||||
}), 'apiCreatePR');
|
||||
return (await response.json()).number;
|
||||
}
|
||||
|
||||
/** Create a review on a PR. `event: "COMMENT"` submits immediately without a pending review. */
|
||||
export async function apiCreateReview(requestContext: APIRequestContext, owner: string, repo: string, index: number, {event = 'COMMENT', body, comments = [], headers}: {event?: string; body?: string; comments?: Array<{path: string; body: string; new_position?: number; old_position?: number}>; headers?: Record<string, string>} = {}) {
|
||||
const response = await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/pulls/${index}/reviews`, {
|
||||
headers: headers || apiHeaders(),
|
||||
data: {event, body, comments},
|
||||
}), 'apiCreateReview');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function createProjectColumn(requestContext: APIRequestContext, owner: string, repo: string, projectID: string, title: string) {
|
||||
await apiRetry(() => requestContext.post(`${baseUrl()}/${owner}/${repo}/projects/${projectID}/columns/new`, {
|
||||
headers: apiHeaders(),
|
||||
form: {title},
|
||||
}), 'createProjectColumn');
|
||||
}
|
||||
|
||||
export async function apiDeleteRepo(requestContext: APIRequestContext, owner: string, name: string) {
|
||||
await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/repos/${owner}/${name}`, {
|
||||
headers: apiHeaders(),
|
||||
}), 'apiDeleteRepo');
|
||||
}
|
||||
|
||||
export async function apiDeleteOrg(requestContext: APIRequestContext, name: string) {
|
||||
await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/orgs/${name}`, {
|
||||
headers: apiHeaders(),
|
||||
}), 'apiDeleteOrg');
|
||||
}
|
||||
|
||||
/** Password shared by all test users — used for both API user creation and browser login. */
|
||||
export const testUserPassword = 'e2e-password!aA1';
|
||||
|
||||
export function apiUserHeaders(username: string) {
|
||||
return apiAuthHeader(username, testUserPassword);
|
||||
}
|
||||
|
||||
export async function apiCreateUser(requestContext: APIRequestContext, username: string) {
|
||||
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/admin/users`, {
|
||||
headers: apiHeaders(),
|
||||
data: {username, password: testUserPassword, email: `${username}@${env.GITEA_TEST_E2E_DOMAIN}`, must_change_password: false},
|
||||
}), 'apiCreateUser');
|
||||
}
|
||||
|
||||
export async function apiDeleteUser(requestContext: APIRequestContext, username: string) {
|
||||
await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/admin/users/${username}?purge=true`, {
|
||||
headers: apiHeaders(),
|
||||
}), 'apiDeleteUser');
|
||||
}
|
||||
|
||||
export async function createProject(
|
||||
page: Page,
|
||||
{owner, repo, title}: {owner: string; repo: string; title: string},
|
||||
): Promise<{id: number}> {
|
||||
// Navigate to new project page
|
||||
await page.goto(`/${owner}/${repo}/projects/new`);
|
||||
|
||||
// Fill in project details
|
||||
await page.getByLabel('Title').fill(title);
|
||||
|
||||
// Submit the form
|
||||
await page.getByRole('button', {name: 'Create Project'}).click();
|
||||
|
||||
// Wait for redirect to projects list
|
||||
await page.waitForURL(new RegExp(`/${owner}/${repo}/projects$`));
|
||||
|
||||
// Extract the project ID from the project link in the list
|
||||
const projectLink = page.locator('.milestone-list > .item').filter({hasText: title}).locator('a').first();
|
||||
const href = await projectLink.getAttribute('href');
|
||||
const match = /\/projects\/(\d+)/.exec(href || '');
|
||||
const id = match ? parseInt(match[1]) : 0;
|
||||
|
||||
return {id};
|
||||
}
|
||||
|
||||
export async function apiCreateIssue(
|
||||
requestContext: APIRequestContext,
|
||||
{owner, repo, title, body, projects, headers}: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
title: string;
|
||||
body?: string;
|
||||
projects?: number[];
|
||||
headers?: Record<string, string>;
|
||||
},
|
||||
): Promise<{index: number}> {
|
||||
let result: {index: number} = {index: 0};
|
||||
await apiRetry(async () => {
|
||||
const response = await requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/issues`, {
|
||||
headers: headers || apiHeaders(),
|
||||
data: {title, body: body || '', projects: projects || []},
|
||||
});
|
||||
if (response.ok()) {
|
||||
const json = await response.json();
|
||||
// API returns "number" field for the issue index
|
||||
result = {index: json.number};
|
||||
}
|
||||
return response;
|
||||
}, 'apiCreateIssue');
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function clickDropdownItem(page: Page, trigger: Locator, itemText: string) {
|
||||
await trigger.click();
|
||||
await page.getByText(itemText).click();
|
||||
}
|
||||
|
||||
export async function loginUser(page: Page, username: string) {
|
||||
return login(page, username, testUserPassword);
|
||||
}
|
||||
|
||||
export async function login(page: Page, username = env.GITEA_TEST_E2E_USER, password = env.GITEA_TEST_E2E_PASSWORD) {
|
||||
const response = await page.request.post('/user/login', {
|
||||
form: {user_name: username, password},
|
||||
maxRedirects: 0,
|
||||
});
|
||||
const status = response.status();
|
||||
if (status !== 302 && status !== 303) throw new Error(`login as ${username} failed: HTTP ${status}`);
|
||||
}
|
||||
|
||||
export async function assertNoJsError(page: Page) {
|
||||
await expect(page.locator('.js-global-error')).toHaveCount(0);
|
||||
}
|
||||
|
||||
/* asserts the child has no horizontal inset from its parent — catches padding/border anywhere
|
||||
* in between regardless of which element declares it */
|
||||
export async function assertFlushWithParent(child: Locator, parent: Locator) {
|
||||
const [childBox, parentBox] = await Promise.all([child.boundingBox(), parent.boundingBox()]);
|
||||
if (!childBox || !parentBox) throw new Error('boundingBox returned null');
|
||||
expect(childBox.x).toBe(parentBox.x);
|
||||
expect(childBox.width).toBe(parentBox.width);
|
||||
}
|
||||
|
||||
export async function logout(page: Page) {
|
||||
await page.context().clearCookies(); // workaround issues related to fomantic dropdown
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('link', {name: 'Sign In'})).toBeVisible();
|
||||
}
|
||||
67
tests/e2e/webauthn.test.ts
Normal file
67
tests/e2e/webauthn.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {test, expect, type Page} from '@playwright/test';
|
||||
import {apiCreateUser, loginUser, randomString, testUserPassword} from './utils.ts';
|
||||
|
||||
const signedIn = /^(?!.*\/user\/(login|webauthn))/; // the target of a finished login varies
|
||||
|
||||
async function registerKey(page: Page, nickname: string) {
|
||||
await page.goto('/user/settings/security');
|
||||
await page.getByLabel('Nickname').fill(nickname);
|
||||
await page.getByRole('button', {name: 'Add Security Key'}).click();
|
||||
}
|
||||
|
||||
async function signInWithPassword(page: Page, username: string) {
|
||||
await page.context().clearCookies();
|
||||
await page.goto('/user/login');
|
||||
await page.getByLabel('Username or Email Address').fill(username);
|
||||
await page.getByLabel('Password').fill(testUserPassword);
|
||||
await page.getByRole('button', {name: 'Sign In'}).click();
|
||||
}
|
||||
|
||||
// regression: credProtect level 3 hid the credential from the second-factor login
|
||||
test('security key survives credProtect', async ({page, request, browserName}) => {
|
||||
test.skip(browserName !== 'chromium', 'only the CDP authenticator emulates credProtect'); // eslint-disable-line playwright/no-skipped-test -- conditional skip, the reason is in the message
|
||||
|
||||
const username = `e2e-credprotect-${randomString(8)}`;
|
||||
await apiCreateUser(request, username);
|
||||
|
||||
const cdp = await page.context().newCDPSession(page);
|
||||
await cdp.send('WebAuthn.enable');
|
||||
await cdp.send('WebAuthn.addVirtualAuthenticator', {options: {
|
||||
protocol: 'ctap2',
|
||||
ctap2Version: 'ctap2_1',
|
||||
transport: 'usb',
|
||||
hasResidentKey: true,
|
||||
hasUserVerification: true,
|
||||
hasCredBlob: true, // CDP only emulates credProtect together with credBlob
|
||||
isUserVerified: true,
|
||||
}});
|
||||
|
||||
await loginUser(page, username);
|
||||
await registerKey(page, 'e2e-key');
|
||||
await expect(page.getByText('e2e-key')).toBeVisible();
|
||||
|
||||
await registerKey(page, 'e2e-key-again');
|
||||
await expect(page.locator('#webauthn-error-msg')).toContainText('already registered');
|
||||
|
||||
await signInWithPassword(page, username);
|
||||
await expect(page).toHaveURL(signedIn);
|
||||
});
|
||||
|
||||
// this authenticator has no credProtect, so it cannot replace the test above
|
||||
test('security key signs in as second factor and as passkey', async ({page, request}) => {
|
||||
const username = `e2e-passkey-${randomString(8)}`;
|
||||
await apiCreateUser(request, username);
|
||||
await page.context().credentials.install();
|
||||
|
||||
await loginUser(page, username);
|
||||
await registerKey(page, 'e2e-key');
|
||||
await expect(page.getByText('e2e-key')).toBeVisible();
|
||||
|
||||
await signInWithPassword(page, username);
|
||||
await expect(page).toHaveURL(signedIn);
|
||||
|
||||
await page.context().clearCookies();
|
||||
await page.goto('/user/login');
|
||||
await page.getByText('Sign in with a passkey').click();
|
||||
await expect(page).toHaveURL(signedIn);
|
||||
});
|
||||
34
tests/fuzz/fuzz_test.go
Normal file
34
tests/fuzz/fuzz_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package fuzz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/markup"
|
||||
"gitea.dev/modules/markup/markdown"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
func newFuzzRenderContext() *markup.RenderContext {
|
||||
return markup.NewTestRenderContext("https://example.com/go-gitea/gitea", map[string]string{"user": "go-gitea", "repo": "gitea"})
|
||||
}
|
||||
|
||||
func FuzzMarkdownRenderRaw(f *testing.F) {
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
setting.IsInTesting = true
|
||||
setting.AppURL = "http://localhost:3000/"
|
||||
markdown.RenderRaw(newFuzzRenderContext(), bytes.NewReader(data), io.Discard)
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzMarkupPostProcess(f *testing.F) {
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
setting.IsInTesting = true
|
||||
setting.AppURL = "http://localhost:3000/"
|
||||
markup.PostProcessDefault(newFuzzRenderContext(), bytes.NewReader(data), io.Discard)
|
||||
})
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 107 B |
Binary file not shown.
@@ -0,0 +1 @@
|
||||
# Testing documents in LFS
|
||||
@@ -0,0 +1 @@
|
||||
# Testing READMEs in LFS
|
||||
@@ -0,0 +1 @@
|
||||
ref: refs/heads/master
|
||||
@@ -0,0 +1,6 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
b895782bd271fdd266dd06e5880ea4abdc3a0dc7
|
||||
@@ -0,0 +1 @@
|
||||
ref: refs/heads/master
|
||||
@@ -0,0 +1,6 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
90e402c3937a4639725fcc59ca1f529e7dc8506f
|
||||
@@ -0,0 +1 @@
|
||||
ref: refs/heads/master
|
||||
@@ -0,0 +1,7 @@
|
||||
[core]
|
||||
bare = false
|
||||
repositoryformatversion = 0
|
||||
filemode = false
|
||||
symlinks = false
|
||||
ignorecase = true
|
||||
logallrefupdates = true
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-checkout.\n"; exit 2; }
|
||||
git lfs post-checkout "$@"
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-commit.\n"; exit 2; }
|
||||
git lfs post-commit "$@"
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-merge.\n"; exit 2; }
|
||||
git lfs post-merge "$@"
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/pre-push.\n"; exit 2; }
|
||||
git lfs pre-push "$@"
|
||||
BIN
tests/gitea-repositories-meta/migration/lfs-test.git/index
Normal file
BIN
tests/gitea-repositories-meta/migration/lfs-test.git/index
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
dummy2
|
||||
@@ -0,0 +1 @@
|
||||
dummy1
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
546244003622c64b2fc3c2cd544d7a29882c8383
|
||||
@@ -0,0 +1 @@
|
||||
ref: refs/heads/branch1
|
||||
@@ -0,0 +1,10 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = false
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
symlinks = false
|
||||
ignorecase = true
|
||||
[user]
|
||||
name = user2
|
||||
email = user2@example.com
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
x•ŤK
|
||||
Â0@]çł$“ß´ âÖcL’)#1ŹoѸzđŕńR«µŔ࣋@daĘAB$"Ë“ŽI˛XcÓ
|
||||
ćEćěâä˝âmÜ[‡í%ÝŔů‹«Ľą>W9ĄV/€žĽ›‘¬<E28098>ŁFŐn÷Ő<C3B7>ż"u{”Qx…_>˙
|
||||
@@ -0,0 +1 @@
|
||||
cdaca8cf1d36e1e4e508a940f6e157e239beccfa
|
||||
@@ -0,0 +1 @@
|
||||
cdaca8cf1d36e1e4e508a940f6e157e239beccfa
|
||||
@@ -0,0 +1 @@
|
||||
ref: refs/heads/branch1
|
||||
@@ -0,0 +1,10 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = false
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
symlinks = false
|
||||
ignorecase = true
|
||||
[user]
|
||||
name = user2
|
||||
email = user2@example.com
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
x•ŤK
|
||||
Â0@]çł$“ß´ âÖcL’)#1ŹoѸzđŕńR«µŔ࣋@daĘAB$"Ë“ŽI˛XcÓ
|
||||
ćEćěâä˝âmÜ[‡í%ÝŔů‹«Ľą>W9ĄV/€žĽ›‘¬<E28098>ŁFŐn÷Ő<C3B7>ż"u{”Qx…_>˙
|
||||
@@ -0,0 +1 @@
|
||||
cdaca8cf1d36e1e4e508a940f6e157e239beccfa
|
||||
@@ -0,0 +1 @@
|
||||
cdaca8cf1d36e1e4e508a940f6e157e239beccfa
|
||||
@@ -0,0 +1 @@
|
||||
ref: refs/heads/branch1
|
||||
@@ -0,0 +1,10 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = false
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
symlinks = false
|
||||
ignorecase = true
|
||||
[user]
|
||||
name = user2
|
||||
email = user2@example.com
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
x•ŤK
|
||||
Â0@]çł$“ß´ âÖcL’)#1ŹoѸzđŕńR«µŔ࣋@daĘAB$"Ë“ŽI˛XcÓ
|
||||
ćEćěâä˝âmÜ[‡í%ÝŔů‹«Ľą>W9ĄV/€žĽ›‘¬<E28098>ŁFŐn÷Ő<C3B7>ż"u{”Qx…_>˙
|
||||
@@ -0,0 +1 @@
|
||||
cdaca8cf1d36e1e4e508a940f6e157e239beccfa
|
||||
@@ -0,0 +1 @@
|
||||
cdaca8cf1d36e1e4e508a940f6e157e239beccfa
|
||||
1
tests/gitea-repositories-meta/org3/repo3.git/HEAD
Normal file
1
tests/gitea-repositories-meta/org3/repo3.git/HEAD
Normal file
@@ -0,0 +1 @@
|
||||
ref: refs/heads/master
|
||||
6
tests/gitea-repositories-meta/org3/repo3.git/config
Normal file
6
tests/gitea-repositories-meta/org3/repo3.git/config
Normal file
@@ -0,0 +1,6 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
7
tests/gitea-repositories-meta/org3/repo3.git/hooks/post-receive
Executable file
7
tests/gitea-repositories-meta/org3/repo3.git/hooks/post-receive
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
ORI_DIR=`pwd`
|
||||
SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
|
||||
cd "$ORI_DIR"
|
||||
for i in `ls "$SHELL_FOLDER/post-receive.d"`; do
|
||||
sh "$SHELL_FOLDER/post-receive.d/$i"
|
||||
done
|
||||
2
tests/gitea-repositories-meta/org3/repo3.git/hooks/post-receive.d/gitea
Executable file
2
tests/gitea-repositories-meta/org3/repo3.git/hooks/post-receive.d/gitea
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" post-receive
|
||||
7
tests/gitea-repositories-meta/org3/repo3.git/hooks/pre-receive
Executable file
7
tests/gitea-repositories-meta/org3/repo3.git/hooks/pre-receive
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
ORI_DIR=`pwd`
|
||||
SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
|
||||
cd "$ORI_DIR"
|
||||
for i in `ls "$SHELL_FOLDER/pre-receive.d"`; do
|
||||
sh "$SHELL_FOLDER/pre-receive.d/$i"
|
||||
done
|
||||
2
tests/gitea-repositories-meta/org3/repo3.git/hooks/pre-receive.d/gitea
Executable file
2
tests/gitea-repositories-meta/org3/repo3.git/hooks/pre-receive.d/gitea
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" pre-receive
|
||||
7
tests/gitea-repositories-meta/org3/repo3.git/hooks/update
Executable file
7
tests/gitea-repositories-meta/org3/repo3.git/hooks/update
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
ORI_DIR=`pwd`
|
||||
SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
|
||||
cd "$ORI_DIR"
|
||||
for i in `ls "$SHELL_FOLDER/update.d"`; do
|
||||
sh "$SHELL_FOLDER/update.d/$i" $1 $2 $3
|
||||
done
|
||||
2
tests/gitea-repositories-meta/org3/repo3.git/hooks/update.d/gitea
Executable file
2
tests/gitea-repositories-meta/org3/repo3.git/hooks/update.d/gitea
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" update $1 $2 $3
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user