From 38d2933cc12a55ca2f048530c48b775b59183dc1 Mon Sep 17 00:00:00 2001 From: Otto Richter Date: Wed, 25 Dec 2024 03:56:39 +0100 Subject: [PATCH 1/5] New repo: Clean up and improve CSS - drop custom layout rules for this page - move form-related content to form.css - extend new form CSS to add gap between labels and input fields (cherry picked from commit 471e5b197539676e397128be90a9f40aa8d917ed) --- web_src/css/base.css | 91 -------------------------------- web_src/css/form.css | 122 ++++++++++++++++++++++++++++++++++++++----- web_src/css/repo.css | 4 -- 3 files changed, 110 insertions(+), 107 deletions(-) diff --git a/web_src/css/base.css b/web_src/css/base.css index fc3200d7da..dd761a1b75 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -652,97 +652,6 @@ img.ui.avatar, background: var(--color-active); } -.ui.form .fields.error .field textarea, -.ui.form .fields.error .field select, -.ui.form .fields.error .field input:not([type]), -.ui.form .fields.error .field input[type="date"], -.ui.form .fields.error .field input[type="datetime-local"], -.ui.form .fields.error .field input[type="email"], -.ui.form .fields.error .field input[type="number"], -.ui.form .fields.error .field input[type="password"], -.ui.form .fields.error .field input[type="search"], -.ui.form .fields.error .field input[type="tel"], -.ui.form .fields.error .field input[type="time"], -.ui.form .fields.error .field input[type="text"], -.ui.form .fields.error .field input[type="file"], -.ui.form .fields.error .field input[type="url"], -.ui.form .fields.error .field .ui.dropdown, -.ui.form .fields.error .field .ui.dropdown .item, -.ui.form .field.error .ui.dropdown, -.ui.form .field.error .ui.dropdown .text, -.ui.form .field.error .ui.dropdown .item, -.ui.form .field.error textarea, -.ui.form .field.error select, -.ui.form .field.error input:not([type]), -.ui.form .field.error input[type="date"], -.ui.form .field.error input[type="datetime-local"], -.ui.form .field.error input[type="email"], -.ui.form .field.error input[type="number"], -.ui.form .field.error input[type="password"], -.ui.form .field.error input[type="search"], -.ui.form .field.error input[type="tel"], -.ui.form .field.error input[type="time"], -.ui.form .field.error input[type="text"], -.ui.form .field.error input[type="file"], -.ui.form .field.error input[type="url"], -.ui.form .field.error select:focus, -.ui.form .field.error input:not([type]):focus, -.ui.form .field.error input[type="date"]:focus, -.ui.form .field.error input[type="datetime-local"]:focus, -.ui.form .field.error input[type="email"]:focus, -.ui.form .field.error input[type="number"]:focus, -.ui.form .field.error input[type="password"]:focus, -.ui.form .field.error input[type="search"]:focus, -.ui.form .field.error input[type="tel"]:focus, -.ui.form .field.error input[type="time"]:focus, -.ui.form .field.error input[type="text"]:focus, -.ui.form .field.error input[type="file"]:focus, -.ui.form .field.error input[type="url"]:focus { - background-color: var(--color-error-bg); - border-color: var(--color-error-border); - color: var(--color-error-text); -} - -.ui.form .fields.error .field .ui.dropdown, -.ui.form .field.error .ui.dropdown, -.ui.form .fields.error .field .ui.dropdown:hover, -.ui.form .field.error .ui.dropdown:hover { - border-color: var(--color-error-border) !important; -} - -.ui.form .fields.error .field .ui.dropdown .menu .item:hover, -.ui.form .field.error .ui.dropdown .menu .item:hover { - background-color: var(--color-error-bg-hover); -} - -.ui.form .fields.error .field .ui.dropdown .menu .active.item, -.ui.form .field.error .ui.dropdown .menu .active.item { - background-color: var(--color-error-bg-active) !important; -} - -.ui.form .fields.error .dropdown .menu, -.ui.form .field.error .dropdown .menu { - border-color: var(--color-error-border) !important; -} - -input:-webkit-autofill, -input:-webkit-autofill:focus, -input:-webkit-autofill:hover, -input:-webkit-autofill:active, -.ui.form .field.field input:-webkit-autofill, -.ui.form .field.field input:-webkit-autofill:focus, -.ui.form .field.field input:-webkit-autofill:hover, -.ui.form .field.field input:-webkit-autofill:active { - -webkit-background-clip: text; - -webkit-text-fill-color: var(--color-text); - box-shadow: 0 0 0 100px var(--color-primary-light-6) inset !important; - border-color: var(--color-primary-light-4) !important; -} - -.ui.form .field.muted { - opacity: var(--opacity-disabled); -} - .text.primary { color: var(--color-primary) !important; } diff --git a/web_src/css/form.css b/web_src/css/form.css index fb9364db45..bf50114344 100644 --- a/web_src/css/form.css +++ b/web_src/css/form.css @@ -18,6 +18,11 @@ fieldset label:has(input[type="number"]) { font-weight: var(--font-weight-medium); } +/* override inline style on custom input elements */ +fieldset label .ui.dropdown { + width: 100% !important; +} + fieldset .help { font-weight: var(--font-weight-normal); } @@ -27,9 +32,17 @@ fieldset .help { padding-bottom: 0; } -fieldset input[type="checkbox"], -fieldset input[type="radio"] { +fieldset label > input, +fieldset label > textarea, +fieldset label > .ui.dropdown, +fieldset label + .ui.dropdown { + margin-top: 0.28rem !important; +} + +fieldset label > input[type="checkbox"], +fieldset label > input[type="radio"] { margin-right: 0.75em; + margin-top: 0 !important; vertical-align: initial !important; /* overrides a semantic.css rule, remove when obsolete */ } @@ -142,6 +155,101 @@ textarea:focus, color: var(--color-input-text); } +/* error messages */ +fieldset label.error textarea, +fieldset label.error select, +fieldset label.error input, +.ui.form .fields.error .field textarea, +.ui.form .fields.error .field select, +.ui.form .fields.error .field input:not([type]), +.ui.form .fields.error .field input[type="date"], +.ui.form .fields.error .field input[type="datetime-local"], +.ui.form .fields.error .field input[type="email"], +.ui.form .fields.error .field input[type="number"], +.ui.form .fields.error .field input[type="password"], +.ui.form .fields.error .field input[type="search"], +.ui.form .fields.error .field input[type="tel"], +.ui.form .fields.error .field input[type="time"], +.ui.form .fields.error .field input[type="text"], +.ui.form .fields.error .field input[type="file"], +.ui.form .fields.error .field input[type="url"], +.ui.form .fields.error .field .ui.dropdown, +.ui.form .fields.error .field .ui.dropdown .item, +.ui.form .field.error .ui.dropdown, +.ui.form .field.error .ui.dropdown .text, +.ui.form .field.error .ui.dropdown .item, +.ui.form .field.error textarea, +.ui.form .field.error select, +.ui.form .field.error input:not([type]), +.ui.form .field.error input[type="date"], +.ui.form .field.error input[type="datetime-local"], +.ui.form .field.error input[type="email"], +.ui.form .field.error input[type="number"], +.ui.form .field.error input[type="password"], +.ui.form .field.error input[type="search"], +.ui.form .field.error input[type="tel"], +.ui.form .field.error input[type="time"], +.ui.form .field.error input[type="text"], +.ui.form .field.error input[type="file"], +.ui.form .field.error input[type="url"], +.ui.form .field.error select:focus, +.ui.form .field.error input:not([type]):focus, +.ui.form .field.error input[type="date"]:focus, +.ui.form .field.error input[type="datetime-local"]:focus, +.ui.form .field.error input[type="email"]:focus, +.ui.form .field.error input[type="number"]:focus, +.ui.form .field.error input[type="password"]:focus, +.ui.form .field.error input[type="search"]:focus, +.ui.form .field.error input[type="tel"]:focus, +.ui.form .field.error input[type="time"]:focus, +.ui.form .field.error input[type="text"]:focus, +.ui.form .field.error input[type="file"]:focus, +.ui.form .field.error input[type="url"]:focus { + background-color: var(--color-error-bg); + border-color: var(--color-error-border); + color: var(--color-error-text); +} + +.ui.form .fields.error .field .ui.dropdown, +.ui.form .field.error .ui.dropdown, +.ui.form .fields.error .field .ui.dropdown:hover, +.ui.form .field.error .ui.dropdown:hover { + border-color: var(--color-error-border) !important; +} + +.ui.form .fields.error .field .ui.dropdown .menu .item:hover, +.ui.form .field.error .ui.dropdown .menu .item:hover { + background-color: var(--color-error-bg-hover); +} + +.ui.form .fields.error .field .ui.dropdown .menu .active.item, +.ui.form .field.error .ui.dropdown .menu .active.item { + background-color: var(--color-error-bg-active) !important; +} + +.ui.form .fields.error .dropdown .menu, +.ui.form .field.error .dropdown .menu { + border-color: var(--color-error-border) !important; +} + +input:-webkit-autofill, +input:-webkit-autofill:focus, +input:-webkit-autofill:hover, +input:-webkit-autofill:active, +.ui.form .field.field input:-webkit-autofill, +.ui.form .field.field input:-webkit-autofill:focus, +.ui.form .field.field input:-webkit-autofill:hover, +.ui.form .field.field input:-webkit-autofill:active { + -webkit-background-clip: text; + -webkit-text-fill-color: var(--color-text); + box-shadow: 0 0 0 100px var(--color-primary-light-6) inset !important; + border-color: var(--color-primary-light-4) !important; +} + +.ui.form .field.muted { + opacity: var(--opacity-disabled); +} + .ui.form .field > label, .ui.form .inline.fields > label, .ui.form .inline.fields .field > label, @@ -400,14 +508,12 @@ textarea:focus, .repository.new.fork form .header { padding-left: 280px !important; } - .repository.new.repo form .inline.field > label, .repository.new.migrate form .inline.field > label, .repository.new.fork form .inline.field > label { text-align: right; width: 250px !important; word-wrap: break-word; } - .repository.new.repo form .help, .repository.new.migrate form .help, .repository.new.fork form .help { margin-left: 265px !important; @@ -417,10 +523,8 @@ textarea:focus, .repository.new.fork form .optional .title { margin-left: 250px !important; } - .repository.new.repo form .inline.field > input, .repository.new.migrate form .inline.field > input, .repository.new.fork form .inline.field > input, - .repository.new.repo form .inline.field > textarea, .repository.new.migrate form .inline.field > textarea, .repository.new.fork form .inline.field > textarea { width: 50%; @@ -440,7 +544,6 @@ textarea:focus, } } -.repository.new.repo form .dropdown .text, .repository.new.migrate form .dropdown .text, .repository.new.fork form .dropdown .text { margin-right: 0 !important; @@ -453,7 +556,6 @@ textarea:focus, text-align: center; } -.repository.new.repo form .selection.dropdown, .repository.new.migrate form .selection.dropdown, .repository.new.fork form .selection.dropdown, .repository.new.fork form .field a { @@ -490,10 +592,6 @@ textarea:focus, } } -.repository.new.repo .ui.form .selection.dropdown:not(.owner) { - width: 50% !important; -} - @media (max-width: 767.98px) { .repository.new.repo .ui.form .selection.dropdown:not(.owner) { width: 100% !important; diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 484cdf1c85..675e5565bd 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -4,10 +4,6 @@ user-select: none; } -.repository .owner.dropdown { - min-width: 40% !important; -} - .repository .unicode-escaped .escaped-code-point[data-escaped]::before { visibility: visible; content: attr(data-escaped); From cb745a771ae3e8fe92fdf5e4de446d2a4254aae2 Mon Sep 17 00:00:00 2001 From: Otto Richter Date: Wed, 25 Dec 2024 03:58:07 +0100 Subject: [PATCH 2/5] New repo: Rework basic settings - separate template - ensure correct labelling of elements - drop additional required indicators for field that already have browser semantics (the icon has colour contrast issues anyway), especially as the first dropdown cannot be left empty (cherry picked from commit 81599155e817e00bd0588df4cb201d1081104388) --- templates/repo/create.tmpl | 52 ++------------------------------ templates/repo/create_basic.tmpl | 45 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 49 deletions(-) create mode 100644 templates/repo/create_basic.tmpl diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index df4288a2f2..79ed22d78e 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -16,56 +16,10 @@

{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}

{{end}} -
- - - {{ctx.Locale.Tr "repo.owner_helper"}} -
+
+ {{template "repo/create_basic" .}} +
-
- - - {{ctx.Locale.Tr "repo.repo_name_helper"}} -
-
- -
- - -
- {{if .IsForcedPrivate}} - {{ctx.Locale.Tr "repo.visibility_helper_forced"}} - {{end}} - {{ctx.Locale.Tr "repo.visibility_description"}} -
-
- - -

diff --git a/templates/repo/create_advanced.tmpl b/templates/repo/create_advanced.tmpl new file mode 100644 index 0000000000..c0274701f8 --- /dev/null +++ b/templates/repo/create_advanced.tmpl @@ -0,0 +1,45 @@ + + +{{$supportedFormatsLength := len .SupportedObjectFormats}} +{{/* Only offer object format selection if there is an actual choice */}} +{{if ge $supportedFormatsLength 2}} + +{{else}} + +{{end}} + + + + diff --git a/templates/repo/create_basic.tmpl b/templates/repo/create_basic.tmpl index 78a4e8b957..0396629fef 100644 --- a/templates/repo/create_basic.tmpl +++ b/templates/repo/create_basic.tmpl @@ -1,5 +1,7 @@ -
diff --git a/templates/repo/create_init.tmpl b/templates/repo/create_init.tmpl new file mode 100644 index 0000000000..729b44c8e6 --- /dev/null +++ b/templates/repo/create_init.tmpl @@ -0,0 +1,56 @@ + + +
+ + + + + {{$supportedReadmesLength := len .Readmes}} + {{/* Only offer README selection if there is an actual choice */}} + {{if ge $supportedReadmesLength 2}} + + {{else}} + + {{end}} +
diff --git a/tests/integration/repo_generate_test.go b/tests/integration/repo_generate_test.go index 4bd9f32119..bcee0df417 100644 --- a/tests/integration/repo_generate_test.go +++ b/tests/integration/repo_generate_test.go @@ -43,7 +43,7 @@ func assertRepoCreateForm(t *testing.T, htmlDoc *HTMLDoc, owner *user_model.User // the template menu is loaded client-side, so don't assert the option exists assert.Equal(t, templateID, htmlDoc.GetInputValueByName("repo_template"), "Unexpected repo_template selection") - for _, name := range []string{"issue_labels", "gitignores", "license", "readme", "object_format_name"} { + for _, name := range []string{"issue_labels", "gitignores", "license", "object_format_name"} { htmlDoc.AssertDropdownHasOptions(t, name) } } From f1b98d16c78752fcaa05008468b20e030be6b8f2 Mon Sep 17 00:00:00 2001 From: Otto Richter Date: Fri, 27 Dec 2024 22:57:24 +0100 Subject: [PATCH 5/5] tests(e2e): Test new repo dialog and behaviour - screenshots and basic accessibility scan of collapsed and expanded sections - the dropdowns do not pass the accessibility checks, but I haven't found an easy fix - I manually confirmed the dropdown behaviour via orca and firefox, though (cherry picked from commit 8d829a97b21650f51f820834b6f92e9aa3855140) --- tests/e2e/repo-new.test.e2e.ts | 134 +++++++++++++++++++++++++++++++++ tests/e2e/shared/forms.ts | 3 + tests/e2e/utils_e2e.ts | 15 ++++ 3 files changed, 152 insertions(+) create mode 100644 tests/e2e/repo-new.test.e2e.ts 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()}`; +}