diff --git a/tests/e2e/repo-new.test.e2e.ts b/tests/e2e/repo-new.test.e2e.ts new file mode 100644 index 0000000000..c9cc29ad56 --- /dev/null +++ b/tests/e2e/repo-new.test.e2e.ts @@ -0,0 +1,134 @@ +// @watch start +// templates/repo/create**.tmpl +// web_src/css/{form,repo}.css +// @watch end + +import {expect} from '@playwright/test'; +import {test, dynamic_id, save_visual, login_user, login} from './utils_e2e.ts'; +import {validate_form} from './shared/forms.ts'; + +test.beforeAll(async ({browser}, workerInfo) => { + await login_user(browser, workerInfo, 'user2'); +}); + +test('New repo: invalid', async ({browser}, workerInfo) => { + const page = await login({browser}, workerInfo); + const response = await page.goto('/repo/create'); + expect(response?.status()).toBe(200); + // check that relevant form content is hidden or available + await expect(page.getByRole('group', {name: 'Use a template You can select'}).getByRole('combobox')).toBeVisible(); + await expect(page.getByText('.gitignore Select .gitignore')).toBeHidden(); + await expect(page.getByText('Labels Select a label set')).toBeHidden(); + await validate_form({page}, 'fieldset'); + await save_visual(page); + + await page.getByLabel('Repository name').fill('*invalid'); + await page.getByRole('button', {name: 'Create repository'}).click(); + await expect(page.getByText('Repository name should contain only alphanumeric')).toBeVisible(); + await save_visual(page); +}); + +test('New repo: initialize', async ({browser}, workerInfo) => { + const page = await login({browser}, workerInfo); + const response = await page.goto('/repo/create'); + expect(response?.status()).toBe(200); + // check that relevant form content is hidden or available + await expect(page.getByRole('group', {name: 'Use a template You can select'}).getByRole('combobox')).toBeVisible(); + await expect(page.getByText('.gitignore Select .gitignore')).toBeHidden(); + // fill initialization section + await page.getByText('Start the Git history with').click(); + await page.getByText('Select .gitignore templates').click(); + await page.getByLabel('.gitignore Select .gitignore').fill('Go'); + await page.getByRole('option', {name: 'Go', exact: true}).click(); + await page.keyboard.press('Escape'); + await page.getByLabel('License Select a license file').click(); + await page.getByRole('option', {name: 'MIT', exact: true}).click(); + await page.keyboard.press('Escape'); + // add advanced settings + await page.getByText('Click to expand').click(); + await page.getByPlaceholder('master').fill('main'); + await page.getByLabel('Make repository a template').check(); + + await validate_form({page}, 'fieldset'); + await save_visual(page); + const reponame = dynamic_id(); + await page.getByLabel('Repository name').fill(reponame); + await page.getByRole('button', {name: 'Create repository'}).click(); + await expect(page.getByRole('link', {name: '.gitignore'})).toBeVisible(); + await expect(page.getByRole('link', {name: 'LICENSE', exact: true})).toBeVisible(); + if (!workerInfo.project.name.includes('Mobile')) { + await expect(page.getByText('Template', {exact: true})).toBeVisible(); + } + await save_visual(page); +}); + +test('New repo: initialize later', async ({browser}, workerInfo) => { + const page = await login({browser}, workerInfo); + const response = await page.goto('/repo/create'); + expect(response?.status()).toBe(200); + + const reponame = dynamic_id(); + await page.getByLabel('Repository name').fill(reponame); + await page.getByPlaceholder('Enter short description').fill(`Description for repo ${reponame}`); + await page.getByText('Click to expand').click(); + await page.getByPlaceholder('master').fill('devbranch'); + await validate_form({page}, 'fieldset'); + await page.getByRole('button', {name: 'Create repository'}).click(); + expect(page.url()).toBe(`http://localhost:3003/user2/${reponame}`); + await expect(page.getByRole('link', {name: 'New file'})).toBeVisible(); + await expect(page.getByRole('heading', {name: 'Creating a new repository on'})).toBeVisible(); + await save_visual(page); + + // add a README + await page.getByRole('link', {name: 'New file'}).click(); + // wait for loading spinner to disappear + // Otherwise, filling the filename might not populate the tree_path form field or preview tab + // The editor has race conditions, likely related to https://codeberg.org/forgejo/forgejo/issues/3371 + await expect(page.locator('.is-loading')).toBeHidden(); + await page.locator('.view-lines').click(); + await page.keyboard.type('# Heading\n\nHello Forgejo!'); + await page.getByPlaceholder('Name your fileā€¦').fill('README.md'); + await expect(page.getByText('Preview')).toBeVisible(); + await page.getByPlaceholder('Add ""').fill('My first commit message'); + await page.getByRole('button', {name: 'Commit changes'}).click(); + expect(page.url()).toBe(`http://localhost:3003/user2/${reponame}/src/branch/devbranch/README.md`); + await expect(page.getByRole('link', {name: 'My first commit message'})).toBeVisible(); + await expect(page.getByText('Hello Forgejo!')).toBeVisible(); + await save_visual(page); +}); + +test('New repo: from template', async ({browser}, workerInfo) => { + test.skip(['Mobile Safari', 'webkit'].includes(workerInfo.project.name), 'WebKit browsers seem to have CORS issues with localhost here.'); + const page = await login({browser}, workerInfo); + const response = await page.goto('/repo/create'); + expect(response?.status()).toBe(200); + + const reponame = dynamic_id(); + await page.getByRole('group', {name: 'Use a template You can select'}).getByRole('combobox').click(); + await page.getByRole('option', {name: 'user27/template1'}).click(); + await page.getByText('Git content (Default branch)').click(); + await save_visual(page); + await page.getByLabel('Repository name').fill(reponame); + await page.getByRole('button', {name: 'Create repository'}).click(); + await expect(page.getByRole('link', {name: `${reponame}.log`})).toBeVisible(); + await save_visual(page); +}); + +test('New repo: label set', async ({browser}, workerInfo) => { + const page = await login({browser}, workerInfo); + await page.goto('/repo/create'); + + const reponame = dynamic_id(); + await page.getByText('Click to expand').click(); + await page.getByLabel('Labels Select a label set').click(); + await page.getByRole('option', {name: 'Advanced (Kind/Bug, Kind/'}).click(); + // close dropdown via unrelated click + await page.getByText('You can select an existing').click(); + await save_visual(page); + await page.getByLabel('Repository name').fill(reponame); + await page.getByRole('button', {name: 'Create repository'}).click(); + await page.goto(`/user2/${reponame}/issues`); + await page.getByRole('link', {name: 'Labels'}).click(); + await expect(page.getByText('Kind/Bug Something is not')).toBeVisible(); + await save_visual(page); +}); diff --git a/tests/e2e/shared/forms.ts b/tests/e2e/shared/forms.ts index 99ad5a0a6d..fc608489b0 100644 --- a/tests/e2e/shared/forms.ts +++ b/tests/e2e/shared/forms.ts @@ -7,6 +7,9 @@ export async function validate_form({page}: {page: Page}, scope: 'form' | 'field 'span[data-tooltip-content', // exclude weird non-semantic HTML disabled content '.disabled', + // legacy dropdowns don't use semantic HTML yet, + // avoid using these where possible + '.ui.dropdown', ]; await accessibilityCheck({page}, [scope], excludedElements, []); diff --git a/tests/e2e/utils_e2e.ts b/tests/e2e/utils_e2e.ts index 09189e6826..31fc999fb0 100644 --- a/tests/e2e/utils_e2e.ts +++ b/tests/e2e/utils_e2e.ts @@ -81,6 +81,14 @@ export async function save_visual(page: Page) { await page.locator('.flex-item-body > relative-time').filter({hasText: /now|minute/}).evaluateAll((nodes) => { for (const node of nodes) node.outerHTML = 'relative time in repo'; }); + // dynamically generated UUIDs + await page.getByText('dyn-id-').evaluateAll((nodes) => { + for (const node of nodes) node.innerHTML = node.innerHTML.replaceAll(/dyn-id-[a-f0-9-]+/g, 'dynamic-id'); + }); + // repeat above, work around https://github.com/microsoft/playwright/issues/34152 + await page.getByText('dyn-id-').evaluateAll((nodes) => { + for (const node of nodes) node.innerHTML = node.innerHTML.replaceAll(/dyn-id-[a-f0-9-]+/g, 'dynamic-id'); + }); await page.locator('relative-time').evaluateAll((nodes) => { for (const node of nodes) node.outerHTML = 'time element'; }); @@ -97,6 +105,8 @@ export async function save_visual(page: Page) { page.locator('#repo_migrating'), // update order of recently created repos is not fully deterministic page.locator('.flex-item-main').filter({hasText: 'relative time in repo'}), + // dynamic IDs in fixed-size inputs + page.locator('input[value*="dyn-id-"]'), ], }); } @@ -122,3 +132,8 @@ export async function create_temp_user(browser: Browser, workerInfo: TestInfo, r return {context: await login_user(browser, workerInfo, username), username}; } + +// returns a random string with a pattern that can be filtered for screenshots automatically +export function dynamic_id() { + return `dyn-id-${globalThis.crypto.randomUUID()}`; +}