Add pr-review e2e test and speed up e2e tests (#37345)
- add pr-review e2e test - speed up most tests by logging in via POST to avoid the login form, login form is still exercised in a dedicated test - speed up most tests be removing post-test cleanup, unnecessary because each repo is created with a unique name - misc parallelization and api call reduction - total suite runtime is about the same as before --------- Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -1,20 +1,16 @@
|
|||||||
import {env} from 'node:process';
|
import {env} from 'node:process';
|
||||||
import {expect, test} from '@playwright/test';
|
import {expect, test} from '@playwright/test';
|
||||||
import {login, apiCreateRepo, apiDeleteRepo, randomString} from './utils.ts';
|
import {login, apiCreateRepo, randomString} from './utils.ts';
|
||||||
|
|
||||||
test('codeeditor textarea updates correctly', async ({page, request}) => {
|
test('codeeditor textarea updates correctly', async ({page, request}) => {
|
||||||
const repoName = `e2e-codeeditor-${randomString(8)}`;
|
const repoName = `e2e-codeeditor-${randomString(8)}`;
|
||||||
await Promise.all([apiCreateRepo(request, {name: repoName}), login(page)]);
|
await Promise.all([apiCreateRepo(request, {name: repoName}), login(page)]);
|
||||||
try {
|
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/_new/main`);
|
||||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/_new/main`);
|
await page.getByPlaceholder('Name your file…').fill('test.js');
|
||||||
await page.getByPlaceholder('Name your file…').fill('test.js');
|
await expect(page.locator('[data-tab="write"] .editor-loading')).toBeHidden();
|
||||||
await expect(page.locator('[data-tab="write"] .editor-loading')).toBeHidden();
|
const editor = page.locator('.cm-content[role="textbox"]');
|
||||||
const editor = page.locator('.cm-content[role="textbox"]');
|
await expect(editor).toBeVisible();
|
||||||
await expect(editor).toBeVisible();
|
await editor.click();
|
||||||
await editor.click();
|
await page.keyboard.type('const hello = "world";');
|
||||||
await page.keyboard.type('const hello = "world";');
|
await expect(page.locator('textarea[name="content"]')).toHaveValue('const hello = "world";');
|
||||||
await expect(page.locator('textarea[name="content"]')).toHaveValue('const hello = "world";');
|
|
||||||
} finally {
|
|
||||||
await apiDeleteRepo(request, env.GITEA_TEST_E2E_USER, repoName);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {test, expect} from '@playwright/test';
|
import {test, expect} from '@playwright/test';
|
||||||
import {loginUser, baseUrl, apiUserHeaders, apiCreateUser, apiDeleteUser, apiCreateRepo, apiCreateIssue, apiStartStopwatch, timeoutFactor, randomString} from './utils.ts';
|
import {loginUser, baseUrl, apiUserHeaders, apiCreateUser, apiCreateRepo, apiCreateIssue, apiStartStopwatch, timeoutFactor, randomString} from './utils.ts';
|
||||||
|
|
||||||
// These tests rely on a short EVENT_SOURCE_UPDATE_TIME in the e2e server config.
|
// These tests rely on a short EVENT_SOURCE_UPDATE_TIME in the e2e server config.
|
||||||
test.describe('events', () => {
|
test.describe('events', () => {
|
||||||
@@ -15,6 +15,7 @@ test.describe('events', () => {
|
|||||||
apiCreateRepo(request, {name: repoName, headers: apiUserHeaders(owner)}),
|
apiCreateRepo(request, {name: repoName, headers: apiUserHeaders(owner)}),
|
||||||
loginUser(page, owner),
|
loginUser(page, owner),
|
||||||
]);
|
]);
|
||||||
|
await page.goto('/');
|
||||||
const badge = page.locator('a.not-mobile .notification_count');
|
const badge = page.locator('a.not-mobile .notification_count');
|
||||||
await expect(badge).toBeHidden();
|
await expect(badge).toBeHidden();
|
||||||
|
|
||||||
@@ -23,9 +24,6 @@ test.describe('events', () => {
|
|||||||
|
|
||||||
// Wait for the notification badge to appear via server event
|
// Wait for the notification badge to appear via server event
|
||||||
await expect(badge).toBeVisible({timeout: 15000 * timeoutFactor});
|
await expect(badge).toBeVisible({timeout: 15000 * timeoutFactor});
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
await Promise.all([apiDeleteUser(request, commenter), apiDeleteUser(request, owner)]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('stopwatch', async ({page, request}) => {
|
test('stopwatch', async ({page, request}) => {
|
||||||
@@ -34,20 +32,20 @@ test.describe('events', () => {
|
|||||||
|
|
||||||
await apiCreateUser(request, name);
|
await apiCreateUser(request, name);
|
||||||
|
|
||||||
// Create repo, issue, and start stopwatch before login
|
// Login in parallel with repo+issue+stopwatch setup (all independent after user exists)
|
||||||
await apiCreateRepo(request, {name, headers});
|
await Promise.all([
|
||||||
await apiCreateIssue(request, name, name, {title: 'events stopwatch test', headers});
|
loginUser(page, name),
|
||||||
await apiStartStopwatch(request, name, name, 1, {headers});
|
(async () => {
|
||||||
|
await apiCreateRepo(request, {name, headers});
|
||||||
// Login — page renders with the active stopwatch element
|
await apiCreateIssue(request, name, name, {title: 'events stopwatch test', headers});
|
||||||
await loginUser(page, name);
|
await apiStartStopwatch(request, name, name, 1, {headers});
|
||||||
|
})(),
|
||||||
|
]);
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
// Verify stopwatch is visible and links to the correct issue
|
// Verify stopwatch is visible and links to the correct issue
|
||||||
const stopwatch = page.locator('.active-stopwatch.not-mobile');
|
const stopwatch = page.locator('.active-stopwatch.not-mobile');
|
||||||
await expect(stopwatch).toBeVisible();
|
await expect(stopwatch).toBeVisible();
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
await apiDeleteUser(request, name);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('logout propagation', async ({browser, request}) => {
|
test('logout propagation', async ({browser, request}) => {
|
||||||
@@ -75,8 +73,5 @@ test.describe('events', () => {
|
|||||||
await expect(page2.getByRole('link', {name: 'Sign In'})).toBeVisible();
|
await expect(page2.getByRole('link', {name: 'Sign In'})).toBeVisible();
|
||||||
|
|
||||||
await context.close();
|
await context.close();
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
await apiDeleteUser(request, name);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {env} from 'node:process';
|
import {env} from 'node:process';
|
||||||
import {expect, test} from '@playwright/test';
|
import {expect, test} from '@playwright/test';
|
||||||
import {login, apiCreateRepo, apiCreateFile, apiDeleteRepo, assertFlushWithParent, assertNoJsError, randomString} from './utils.ts';
|
import {login, apiCreateRepo, apiCreateFile, assertFlushWithParent, assertNoJsError, randomString} from './utils.ts';
|
||||||
|
|
||||||
test('external file', async ({page, request}) => {
|
test('external file', async ({page, request}) => {
|
||||||
const repoName = `e2e-external-render-${randomString(8)}`;
|
const repoName = `e2e-external-render-${randomString(8)}`;
|
||||||
@@ -9,19 +9,15 @@ test('external file', async ({page, request}) => {
|
|||||||
apiCreateRepo(request, {name: repoName}),
|
apiCreateRepo(request, {name: repoName}),
|
||||||
login(page),
|
login(page),
|
||||||
]);
|
]);
|
||||||
try {
|
await apiCreateFile(request, owner, repoName, 'test.external', '<p>rendered content</p>');
|
||||||
await apiCreateFile(request, owner, repoName, 'test.external', '<p>rendered content</p>');
|
await page.goto(`/${owner}/${repoName}/src/branch/main/test.external`);
|
||||||
await page.goto(`/${owner}/${repoName}/src/branch/main/test.external`);
|
const iframe = page.locator('iframe.external-render-iframe');
|
||||||
const iframe = page.locator('iframe.external-render-iframe');
|
await expect(iframe).toBeVisible();
|
||||||
await expect(iframe).toBeVisible();
|
await expect(iframe).toHaveAttribute('data-src', new RegExp(`/${owner}/${repoName}/render/branch/main/test\\.external`));
|
||||||
await expect(iframe).toHaveAttribute('data-src', new RegExp(`/${owner}/${repoName}/render/branch/main/test\\.external`));
|
const frame = page.frameLocator('iframe.external-render-iframe');
|
||||||
const frame = page.frameLocator('iframe.external-render-iframe');
|
await expect(frame.locator('p')).toContainText('rendered content');
|
||||||
await expect(frame.locator('p')).toContainText('rendered content');
|
await assertFlushWithParent(iframe, page.locator('.file-view'));
|
||||||
await assertFlushWithParent(iframe, page.locator('.file-view'));
|
await assertNoJsError(page);
|
||||||
await assertNoJsError(page);
|
|
||||||
} finally {
|
|
||||||
await apiDeleteRepo(request, owner, repoName);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('openapi file', async ({page, request}) => {
|
test('openapi file', async ({page, request}) => {
|
||||||
@@ -31,31 +27,27 @@ test('openapi file', async ({page, request}) => {
|
|||||||
apiCreateRepo(request, {name: repoName}),
|
apiCreateRepo(request, {name: repoName}),
|
||||||
login(page),
|
login(page),
|
||||||
]);
|
]);
|
||||||
try {
|
const title = 'Test <API> & "quoted"';
|
||||||
const title = 'Test <API> & "quoted"';
|
const spec = JSON.stringify({
|
||||||
const spec = JSON.stringify({
|
openapi: '3.0.0',
|
||||||
openapi: '3.0.0',
|
info: {title, version: '1.0'},
|
||||||
info: {title, version: '1.0'},
|
paths: {'/pets': {get: {responses: {'200': {description: 'OK', content: {'application/json': {schema: {$ref: '#/components/schemas/Pet'}}}}}}}},
|
||||||
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'}}}}}},
|
||||||
components: {schemas: {Pet: {type: 'object', properties: {children: {type: 'array', items: {$ref: '#/components/schemas/Pet'}}}}}},
|
});
|
||||||
});
|
await apiCreateFile(request, owner, repoName, 'openapi.json', spec);
|
||||||
await apiCreateFile(request, owner, repoName, 'openapi.json', spec);
|
await page.goto(`/${owner}/${repoName}/src/branch/main/openapi.json`);
|
||||||
await page.goto(`/${owner}/${repoName}/src/branch/main/openapi.json`);
|
const iframe = page.locator('iframe.external-render-iframe');
|
||||||
const iframe = page.locator('iframe.external-render-iframe');
|
await expect(iframe).toBeVisible();
|
||||||
await expect(iframe).toBeVisible();
|
const viewer = page.frameLocator('iframe.external-render-iframe').locator('#frontend-render-viewer');
|
||||||
const viewer = page.frameLocator('iframe.external-render-iframe').locator('#frontend-render-viewer');
|
await expect(viewer.locator('.swagger-ui')).toBeVisible();
|
||||||
await expect(viewer.locator('.swagger-ui')).toBeVisible();
|
await expect(viewer.locator('.info .title')).toContainText(title);
|
||||||
await expect(viewer.locator('.info .title')).toContainText(title);
|
// expanding the operation triggers swagger-ui's $ref resolver, which fetches window.location
|
||||||
// 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"
|
||||||
// (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-tag').first().click();
|
await viewer.locator('.opblock').first().click();
|
||||||
await viewer.locator('.opblock').first().click();
|
await expect(viewer.getByText('Could not resolve reference')).toHaveCount(0);
|
||||||
await expect(viewer.getByText('Could not resolve reference')).toHaveCount(0);
|
// poll: postMessage resize may not have settled yet when the visibility checks pass
|
||||||
// poll: postMessage resize may not have settled yet when the visibility checks pass
|
await expect.poll(async () => (await iframe.boundingBox())!.height).toBeGreaterThan(300);
|
||||||
await expect.poll(async () => (await iframe.boundingBox())!.height).toBeGreaterThan(300);
|
await assertFlushWithParent(iframe, page.locator('.file-view'));
|
||||||
await assertFlushWithParent(iframe, page.locator('.file-view'));
|
await assertNoJsError(page);
|
||||||
await assertNoJsError(page);
|
|
||||||
} finally {
|
|
||||||
await apiDeleteRepo(request, owner, repoName);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,32 +1,28 @@
|
|||||||
import {env} from 'node:process';
|
import {env} from 'node:process';
|
||||||
import {expect, test} from '@playwright/test';
|
import {expect, test} from '@playwright/test';
|
||||||
import {apiCreateBranch, apiCreateRepo, apiCreateFile, apiDeleteRepo, assertFlushWithParent, assertNoJsError, login, randomString} from './utils.ts';
|
import {apiCreateRepo, apiCreateFile, assertFlushWithParent, assertNoJsError, login, randomString} from './utils.ts';
|
||||||
|
|
||||||
test('3d model file', async ({page, request}) => {
|
test('3d model file', async ({page, request}) => {
|
||||||
const repoName = `e2e-3d-render-${randomString(8)}`;
|
const repoName = `e2e-3d-render-${randomString(8)}`;
|
||||||
const owner = env.GITEA_TEST_E2E_USER;
|
const owner = env.GITEA_TEST_E2E_USER;
|
||||||
await apiCreateRepo(request, {name: repoName});
|
await apiCreateRepo(request, {name: repoName});
|
||||||
try {
|
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';
|
||||||
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 apiCreateFile(request, owner, repoName, 'test.stl', stl);
|
||||||
await apiCreateFile(request, owner, repoName, 'test.stl', stl);
|
await page.goto(`/${owner}/${repoName}/src/branch/main/test.stl?display=rendered`);
|
||||||
await page.goto(`/${owner}/${repoName}/src/branch/main/test.stl?display=rendered`);
|
const iframe = page.locator('iframe.external-render-iframe');
|
||||||
const iframe = page.locator('iframe.external-render-iframe');
|
await expect(iframe).toBeVisible();
|
||||||
await expect(iframe).toBeVisible();
|
const frame = page.frameLocator('iframe.external-render-iframe');
|
||||||
const frame = page.frameLocator('iframe.external-render-iframe');
|
const viewer = frame.locator('#frontend-render-viewer');
|
||||||
const viewer = frame.locator('#frontend-render-viewer');
|
await expect(viewer.locator('canvas')).toBeVisible();
|
||||||
await expect(viewer.locator('canvas')).toBeVisible();
|
expect((await viewer.boundingBox())!.height).toBeGreaterThan(300);
|
||||||
expect((await viewer.boundingBox())!.height).toBeGreaterThan(300);
|
await assertFlushWithParent(iframe, page.locator('.file-view'));
|
||||||
await assertFlushWithParent(iframe, page.locator('.file-view'));
|
// bgcolor passed via gitea-iframe-bgcolor; 3D viewer reads it from body bgcolor — must match parent
|
||||||
// bgcolor passed via gitea-iframe-bgcolor; 3D viewer reads it from body bgcolor — must match parent
|
const [parentBg, iframeBg] = await Promise.all([
|
||||||
const [parentBg, iframeBg] = await Promise.all([
|
page.evaluate(() => getComputedStyle(document.body).backgroundColor),
|
||||||
page.evaluate(() => getComputedStyle(document.body).backgroundColor),
|
frame.locator('body').evaluate((el) => getComputedStyle(el).backgroundColor),
|
||||||
frame.locator('body').evaluate((el) => getComputedStyle(el).backgroundColor),
|
]);
|
||||||
]);
|
expect(iframeBg).toBe(parentBg);
|
||||||
expect(iframeBg).toBe(parentBg);
|
await assertNoJsError(page);
|
||||||
await assertNoJsError(page);
|
|
||||||
} finally {
|
|
||||||
await apiDeleteRepo(request, owner, repoName);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('pdf file', async ({page, request}) => {
|
test('pdf file', async ({page, request}) => {
|
||||||
@@ -34,16 +30,12 @@ test('pdf file', async ({page, request}) => {
|
|||||||
const repoName = `e2e-pdf-render-${randomString(8)}`;
|
const repoName = `e2e-pdf-render-${randomString(8)}`;
|
||||||
const owner = env.GITEA_TEST_E2E_USER;
|
const owner = env.GITEA_TEST_E2E_USER;
|
||||||
await apiCreateRepo(request, {name: repoName});
|
await apiCreateRepo(request, {name: repoName});
|
||||||
try {
|
await apiCreateFile(request, owner, repoName, 'test.pdf', '%PDF-1.0\n%%EOF\n');
|
||||||
await apiCreateFile(request, owner, repoName, 'test.pdf', '%PDF-1.0\n%%EOF\n');
|
await page.goto(`/${owner}/${repoName}/src/branch/main/test.pdf`);
|
||||||
await page.goto(`/${owner}/${repoName}/src/branch/main/test.pdf`);
|
const container = page.locator('.file-view-render-container');
|
||||||
const container = page.locator('.file-view-render-container');
|
await expect(container).toHaveAttribute('data-render-name', 'pdf-viewer');
|
||||||
await expect(container).toHaveAttribute('data-render-name', 'pdf-viewer');
|
expect((await container.boundingBox())!.height).toBeGreaterThan(300);
|
||||||
expect((await container.boundingBox())!.height).toBeGreaterThan(300);
|
await assertFlushWithParent(container, page.locator('.file-view'));
|
||||||
await assertFlushWithParent(container, page.locator('.file-view'));
|
|
||||||
} finally {
|
|
||||||
await apiDeleteRepo(request, owner, repoName);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('asciicast file', async ({page, request}) => {
|
test('asciicast file', async ({page, request}) => {
|
||||||
@@ -54,16 +46,12 @@ test('asciicast file', async ({page, request}) => {
|
|||||||
const branch = '日本語-branch';
|
const branch = '日本語-branch';
|
||||||
const branchEnc = encodeURIComponent(branch);
|
const branchEnc = encodeURIComponent(branch);
|
||||||
await Promise.all([apiCreateRepo(request, {name: repoName, autoInit: false}), login(page)]);
|
await Promise.all([apiCreateRepo(request, {name: repoName, autoInit: false}), login(page)]);
|
||||||
try {
|
const cast = '{"version": 2, "width": 80, "height": 24}\n[0.0, "o", "hi"]\n';
|
||||||
const cast = '{"version": 2, "width": 80, "height": 24}\n[0.0, "o", "hi"]\n';
|
// on an empty repo, apiCreateFile with newBranch creates that branch as the initial commit
|
||||||
await apiCreateFile(request, owner, repoName, 'readme.cast', cast);
|
await apiCreateFile(request, owner, repoName, 'readme.cast', cast, {newBranch: branch});
|
||||||
await apiCreateBranch(request, owner, repoName, branch);
|
await page.goto(`/${owner}/${repoName}/src/branch/${branchEnc}`);
|
||||||
await page.goto(`/${owner}/${repoName}/src/branch/${branchEnc}`);
|
const container = page.locator('.asciinema-player-container');
|
||||||
const container = page.locator('.asciinema-player-container');
|
await expect(container).toHaveAttribute('data-asciinema-player-src', `/${owner}/${repoName}/raw/branch/${branchEnc}/readme.cast`);
|
||||||
await expect(container).toHaveAttribute('data-asciinema-player-src', `/${owner}/${repoName}/raw/branch/${branchEnc}/readme.cast`);
|
await expect(container.locator('.ap-wrapper')).toBeVisible();
|
||||||
await expect(container.locator('.ap-wrapper')).toBeVisible();
|
expect((await container.boundingBox())!.height).toBeGreaterThan(300);
|
||||||
expect((await container.boundingBox())!.height).toBeGreaterThan(300);
|
|
||||||
} finally {
|
|
||||||
await apiDeleteRepo(request, owner, repoName);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {env} from 'node:process';
|
import {env} from 'node:process';
|
||||||
import {test, expect} from '@playwright/test';
|
import {test, expect} from '@playwright/test';
|
||||||
import {login, apiCreateRepo, apiCreateIssue, apiDeleteRepo, createProjectColumn, randomString} from './utils.ts';
|
import {login, apiCreateRepo, apiCreateIssue, createProjectColumn, randomString} from './utils.ts';
|
||||||
|
|
||||||
test('assign issue to project and change column', async ({page}) => {
|
test('assign issue to project and change column', async ({page}) => {
|
||||||
const repoName = `e2e-issue-project-${randomString(8)}`;
|
const repoName = `e2e-issue-project-${randomString(8)}`;
|
||||||
@@ -26,5 +26,4 @@ test('assign issue to project and change column', async ({page}) => {
|
|||||||
await columnCombo.locator('.ui.dropdown').click();
|
await columnCombo.locator('.ui.dropdown').click();
|
||||||
await columnCombo.locator('.menu a.item', {hasText: 'In Progress'}).click();
|
await columnCombo.locator('.menu a.item', {hasText: 'In Progress'}).click();
|
||||||
await expect(columnCombo.getByTestId('sidebar-project-column-text')).toHaveText('In Progress');
|
await expect(columnCombo.getByTestId('sidebar-project-column-text')).toHaveText('In Progress');
|
||||||
await apiDeleteRepo(page.request, user, repoName);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
|
import {env} from 'node:process';
|
||||||
import {test, expect} from '@playwright/test';
|
import {test, expect} from '@playwright/test';
|
||||||
import {login, logout} from './utils.ts';
|
import {logout} from './utils.ts';
|
||||||
|
|
||||||
test('homepage', async ({page}) => {
|
test('homepage', async ({page}) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await expect(page.getByRole('img', {name: 'Logo'})).toHaveAttribute('src', '/assets/img/logo.svg');
|
await expect(page.getByRole('img', {name: 'Logo'})).toHaveAttribute('src', '/assets/img/logo.svg');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('login and logout', async ({page}) => {
|
test('login form and logout', async ({page}) => {
|
||||||
await login(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);
|
await logout(page);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {env} from 'node:process';
|
import {env} from 'node:process';
|
||||||
import {test, expect} from '@playwright/test';
|
import {test, expect} from '@playwright/test';
|
||||||
import {login, apiCreateRepo, apiDeleteRepo, randomString} from './utils.ts';
|
import {login, apiCreateRepo, randomString} from './utils.ts';
|
||||||
|
|
||||||
test('create a milestone', async ({page}) => {
|
test('create a milestone', async ({page}) => {
|
||||||
const repoName = `e2e-milestone-${randomString(8)}`;
|
const repoName = `e2e-milestone-${randomString(8)}`;
|
||||||
@@ -9,5 +9,4 @@ test('create a milestone', async ({page}) => {
|
|||||||
await page.getByPlaceholder('Title').fill('Test Milestone');
|
await page.getByPlaceholder('Title').fill('Test Milestone');
|
||||||
await page.getByRole('button', {name: 'Create Milestone'}).click();
|
await page.getByRole('button', {name: 'Create Milestone'}).click();
|
||||||
await expect(page.locator('.milestone-list')).toContainText('Test Milestone');
|
await expect(page.locator('.milestone-list')).toContainText('Test Milestone');
|
||||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
|
||||||
});
|
});
|
||||||
|
|||||||
53
tests/e2e/pr-review.test.ts
Normal file
53
tests/e2e/pr-review.test.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import {test, expect} from '@playwright/test';
|
||||||
|
import {apiCreateFile, 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)}`;
|
||||||
|
await Promise.all([apiCreateUser(request, poster), apiCreateUser(request, reviewer)]);
|
||||||
|
const posterHeaders = apiUserHeaders(poster);
|
||||||
|
const repoName = `e2e-prreview-${randomString(8)}`;
|
||||||
|
await apiCreateRepo(request, {name: repoName, headers: posterHeaders});
|
||||||
|
await apiCreateFile(request, poster, repoName, 'added.txt', 'new content\n', {branch: 'main', newBranch: 'feat'});
|
||||||
|
const prIndex = await apiCreatePR(request, poster, repoName, 'feat', 'main', 'review test', {headers: posterHeaders});
|
||||||
|
|
||||||
|
// 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),
|
||||||
|
}),
|
||||||
|
loginUser(page, poster),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await page.goto(`/${poster}/${repoName}/pulls/${prIndex}/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']);
|
||||||
|
|
||||||
|
// switch to reviewer and submit an approve review
|
||||||
|
await page.context().clearCookies();
|
||||||
|
await loginUser(page, reviewer);
|
||||||
|
await page.goto(`/${poster}/${repoName}/pulls/${prIndex}/files`);
|
||||||
|
await page.locator('#review-box .js-btn-review').click();
|
||||||
|
const panel = page.locator('.review-box-panel');
|
||||||
|
await panel.locator('textarea[name="content"]').fill('LGTM');
|
||||||
|
await panel.getByRole('button', {name: 'Approve', exact: true}).click();
|
||||||
|
await expect(page.locator('.timeline-item .octicon-check').first()).toBeVisible();
|
||||||
|
await expect(page.locator('.timeline-item').filter({hasText: 'LGTM'})).toBeVisible();
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import {env} from 'node:process';
|
import {env} from 'node:process';
|
||||||
import {expect, test} from '@playwright/test';
|
import {expect, test} from '@playwright/test';
|
||||||
import {login, apiCreateRepo, apiCreateIssue, apiDeleteRepo, randomString} from './utils.ts';
|
import {login, apiCreateRepo, apiCreateIssue, randomString} from './utils.ts';
|
||||||
|
|
||||||
test('toggle issue reactions', async ({page, request}) => {
|
test('toggle issue reactions', async ({page, request}) => {
|
||||||
const repoName = `e2e-reactions-${randomString(8)}`;
|
const repoName = `e2e-reactions-${randomString(8)}`;
|
||||||
@@ -10,21 +10,17 @@ test('toggle issue reactions', async ({page, request}) => {
|
|||||||
apiCreateIssue(request, owner, repoName, {title: 'Reaction test'}),
|
apiCreateIssue(request, owner, repoName, {title: 'Reaction test'}),
|
||||||
login(page),
|
login(page),
|
||||||
]);
|
]);
|
||||||
try {
|
await page.goto(`/${owner}/${repoName}/issues/1`);
|
||||||
await page.goto(`/${owner}/${repoName}/issues/1`);
|
|
||||||
|
|
||||||
const issueComment = page.locator('.timeline-item.comment.first');
|
const issueComment = page.locator('.timeline-item.comment.first');
|
||||||
|
|
||||||
const reactionPicker = issueComment.locator('.select-reaction');
|
const reactionPicker = issueComment.locator('.select-reaction');
|
||||||
await reactionPicker.click();
|
await reactionPicker.click();
|
||||||
await reactionPicker.getByLabel('+1').click();
|
await reactionPicker.getByLabel('+1').click();
|
||||||
|
|
||||||
const reactions = issueComment.getByRole('group', {name: 'Reactions'});
|
const reactions = issueComment.getByRole('group', {name: 'Reactions'});
|
||||||
await expect(reactions.getByRole('button', {name: /^\+1:/})).toContainText('1');
|
await expect(reactions.getByRole('button', {name: /^\+1:/})).toContainText('1');
|
||||||
|
|
||||||
await reactions.getByRole('button', {name: /^\+1:/}).click();
|
await reactions.getByRole('button', {name: /^\+1:/}).click();
|
||||||
await expect(reactions.getByRole('button', {name: /^\+1:/})).toHaveCount(0);
|
await expect(reactions.getByRole('button', {name: /^\+1:/})).toHaveCount(0);
|
||||||
} finally {
|
|
||||||
await apiDeleteRepo(request, owner, repoName);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import {env} from 'node:process';
|
import {env} from 'node:process';
|
||||||
import {test, expect} from '@playwright/test';
|
import {test, expect} from '@playwright/test';
|
||||||
import {apiCreateRepo, apiDeleteRepo, randomString} from './utils.ts';
|
import {apiCreateRepo, randomString} from './utils.ts';
|
||||||
|
|
||||||
test('repo readme', async ({page}) => {
|
test('repo readme', async ({page}) => {
|
||||||
const repoName = `e2e-readme-${randomString(8)}`;
|
const repoName = `e2e-readme-${randomString(8)}`;
|
||||||
await apiCreateRepo(page.request, {name: repoName});
|
await apiCreateRepo(page.request, {name: repoName});
|
||||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}`);
|
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}`);
|
||||||
await expect(page.locator('#readme')).toContainText(repoName);
|
await expect(page.locator('#readme')).toContainText(repoName);
|
||||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {env} from 'node:process';
|
import {env} from 'node:process';
|
||||||
import {test, expect} from '@playwright/test';
|
import {test, expect} from '@playwright/test';
|
||||||
import {login, logout, apiDeleteUser, randomString} from './utils.ts';
|
import {login, logout, randomString} from './utils.ts';
|
||||||
|
|
||||||
test.beforeEach(async ({page}) => {
|
test.beforeEach(async ({page}) => {
|
||||||
await page.goto('/user/sign_up');
|
await page.goto('/user/sign_up');
|
||||||
@@ -48,9 +48,6 @@ test('register then login', async ({page}) => {
|
|||||||
// Logout then login with the newly created account
|
// Logout then login with the newly created account
|
||||||
await logout(page);
|
await logout(page);
|
||||||
await login(page, username, password);
|
await login(page, username, password);
|
||||||
|
|
||||||
// delete via API because of issues related to form-fetch-action
|
|
||||||
await apiDeleteUser(page.request, username);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('register with existing username shows error', async ({page}) => {
|
test('register with existing username shows error', async ({page}) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {env} from 'node:process';
|
import {env} from 'node:process';
|
||||||
import {test} from '@playwright/test';
|
import {test} from '@playwright/test';
|
||||||
import {login, apiDeleteRepo, randomString} from './utils.ts';
|
import {login, randomString} from './utils.ts';
|
||||||
|
|
||||||
test('create a repository', async ({page}) => {
|
test('create a repository', async ({page}) => {
|
||||||
const repoName = `e2e-repo-${randomString(8)}`;
|
const repoName = `e2e-repo-${randomString(8)}`;
|
||||||
@@ -9,5 +9,4 @@ test('create a repository', async ({page}) => {
|
|||||||
await page.locator('input[name="repo_name"]').fill(repoName);
|
await page.locator('input[name="repo_name"]').fill(repoName);
|
||||||
await page.getByRole('button', {name: 'Create Repository'}).click();
|
await page.getByRole('button', {name: 'Create Repository'}).click();
|
||||||
await page.waitForURL(new RegExp(`/${env.GITEA_TEST_E2E_USER}/${repoName}$`));
|
await page.waitForURL(new RegExp(`/${env.GITEA_TEST_E2E_USER}/${repoName}$`));
|
||||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
import {test, expect} from '@playwright/test';
|
import {test, expect} from '@playwright/test';
|
||||||
import {loginUser, apiCreateUser, apiDeleteUser, randomString} from './utils.ts';
|
import {loginUser, apiCreateUser, randomString} from './utils.ts';
|
||||||
|
|
||||||
test('update profile biography', async ({page, request}) => {
|
test('update profile biography', async ({page, request}) => {
|
||||||
const username = `e2e-settings-${randomString(8)}`;
|
const username = `e2e-settings-${randomString(8)}`;
|
||||||
const bio = `e2e-bio-${randomString(8)}`;
|
const bio = `e2e-bio-${randomString(8)}`;
|
||||||
await apiCreateUser(request, username);
|
await apiCreateUser(request, username);
|
||||||
try {
|
await loginUser(page, username);
|
||||||
await loginUser(page, username);
|
await page.goto('/user/settings');
|
||||||
await page.goto('/user/settings');
|
await page.getByLabel('Biography').fill(bio);
|
||||||
await page.getByLabel('Biography').fill(bio);
|
await page.getByRole('button', {name: 'Update Profile'}).click();
|
||||||
await page.getByRole('button', {name: 'Update Profile'}).click();
|
await expect(page.getByLabel('Biography')).toHaveValue(bio);
|
||||||
await expect(page.getByLabel('Biography')).toHaveValue(bio);
|
await page.getByLabel('Biography').fill('');
|
||||||
await page.getByLabel('Biography').fill('');
|
await page.getByRole('button', {name: 'Update Profile'}).click();
|
||||||
await page.getByRole('button', {name: 'Update Profile'}).click();
|
await expect(page.getByLabel('Biography')).toHaveValue('');
|
||||||
await expect(page.getByLabel('Biography')).toHaveValue('');
|
|
||||||
} finally {
|
|
||||||
await apiDeleteUser(request, username);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,10 +60,10 @@ export async function apiStartStopwatch(requestContext: APIRequestContext, owner
|
|||||||
}), 'apiStartStopwatch');
|
}), 'apiStartStopwatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apiCreateFile(requestContext: APIRequestContext, owner: string, repo: string, filepath: string, content: string) {
|
export async function apiCreateFile(requestContext: APIRequestContext, owner: string, repo: string, filepath: string, content: string, {branch, newBranch, message}: {branch?: string; newBranch?: string; message?: string} = {}) {
|
||||||
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/contents/${filepath}`, {
|
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/contents/${filepath}`, {
|
||||||
headers: apiHeaders(),
|
headers: apiHeaders(),
|
||||||
data: {content: globalThis.btoa(content)},
|
data: {content: Buffer.from(content, 'utf8').toString('base64'), branch, new_branch: newBranch, message},
|
||||||
}), 'apiCreateFile');
|
}), 'apiCreateFile');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +74,28 @@ export async function apiCreateBranch(requestContext: APIRequestContext, owner:
|
|||||||
}), 'apiCreateBranch');
|
}), 'apiCreateBranch');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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> {
|
||||||
|
let prIndex = 0;
|
||||||
|
await apiRetry(async () => {
|
||||||
|
const response = await requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/pulls`, {
|
||||||
|
headers: headers || apiHeaders(),
|
||||||
|
data: {head, base, title},
|
||||||
|
});
|
||||||
|
if (response.ok()) prIndex = (await response.json()).number;
|
||||||
|
return response;
|
||||||
|
}, 'apiCreatePR');
|
||||||
|
return prIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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>} = {}) {
|
||||||
|
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/pulls/${index}/reviews`, {
|
||||||
|
headers: headers || apiHeaders(),
|
||||||
|
data: {event, body, comments},
|
||||||
|
}), 'apiCreateReview');
|
||||||
|
}
|
||||||
|
|
||||||
export async function createProjectColumn(requestContext: APIRequestContext, owner: string, repo: string, projectID: string, title: string) {
|
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`, {
|
await apiRetry(() => requestContext.post(`${baseUrl()}/${owner}/${repo}/projects/${projectID}/columns/new`, {
|
||||||
headers: apiHeaders(),
|
headers: apiHeaders(),
|
||||||
@@ -118,11 +140,12 @@ export async function loginUser(page: Page, username: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function login(page: Page, username = env.GITEA_TEST_E2E_USER, password = env.GITEA_TEST_E2E_PASSWORD) {
|
export async function login(page: Page, username = env.GITEA_TEST_E2E_USER, password = env.GITEA_TEST_E2E_PASSWORD) {
|
||||||
await page.goto('/user/login');
|
const response = await page.request.post('/user/login', {
|
||||||
await page.getByLabel('Username or Email Address').fill(username);
|
form: {user_name: username, password},
|
||||||
await page.getByLabel('Password').fill(password);
|
maxRedirects: 0,
|
||||||
await page.getByRole('button', {name: 'Sign In'}).click();
|
});
|
||||||
await expect(page.getByRole('link', {name: 'Sign In'})).toBeHidden();
|
const status = response.status();
|
||||||
|
if (status !== 302 && status !== 303) throw new Error(`login as ${username} failed: HTTP ${status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function assertNoJsError(page: Page) {
|
export async function assertNoJsError(page: Page) {
|
||||||
|
|||||||
Reference in New Issue
Block a user