Migrate most cypress tests to playwright - #25388
Merged
trasher merged 38 commits intoSep 4, 2026
Merged
Conversation
Extra changes: * The cypress `beforeEach` becomes a `setupForm()` helper called at the start of each test: a plain `beforeAll` would run once per worker and a `beforeEach` would have to share the created ids through a module level variable, which is fragile under `fullyParallel`. * The form is created in the worker entity with a uuid suffixed name. * `cy.reload()` after clicking "Save" becomes `FormPage::doSaveFormEditorAndReload()`, which additionally waits for the "Item successfully updated" alert before reloading. * Removing the `target="_blank"` attribute of the "Preview" link becomes `FormPage::doPreviewForm()`, which reads the href and navigates to it. * `cy.checkAndCloseAlert()` becomes a plain assertion on the alert: the test ends there so there is nothing left to un-obstruct. * The `testDefaultValue()` helper is renamed `assertDefaultValue()` to match the `assertFunctionPatterns` of the `playwright/expect-expect` eslint rule.
AdrienClairembault
force-pushed
the
cypress-to-playright1
branch
2 times, most recently
from
September 3, 2026 14:36
54e6b06 to
a0f373f
Compare
Extra changes:
* The cypress `beforeEach` becomes a `setupForm()` helper called at the
start of each test (see the number.spec.ts migration for the reason).
* The form is created in the worker entity with a uuid suffixed name.
* The repeated dropdown assertions are extracted in two helpers,
`assertSingleDeviceDropdownIsDisplayed()` and
`assertMultipleDevicesDropdownIsDisplayed()`. The
`.closest('.devices-dropdown').find('select')` raw locator chain is no
longer needed: `GlpiPage::getDropdownByLabel()` already targets the
select2 container from the `<select>` aria-label, and the `<select>`
itself is reached with `getByLabel()`.
* `should('not.exist')` on a dropdown becomes `toBeHidden()`: both
dropdowns always exist in the DOM, cypress only saw one of them because
`getDropdownByLabelText` filters on `:visible`.
* `cy.reload()` after clicking "Save" becomes
`FormPage::doSaveFormEditorAndReload()`.
* The question locator is re-resolved after each reload since the
previous one is detached.
Extra changes:
* `cy.get('#search-itemtype').clear()` + `.type()` becomes a single
`fill()`.
* The raw locators are kept (with an eslint ignore) but the malformed
`'[data-itemtype=User'` / `'[data-itemtype=Computer'` selectors (no
closing bracket, tolerated by jQuery) are written as valid CSS.
* The tab is opened with `GlpiPage::doGoToTab()`.
Extra changes:
* `GlpiPage::getDropdownByLabel()` gained an optional `exact` argument.
The "Approval step" field is mandatory, so its label is rendered as
"Approval step *" and could not be matched with the hardcoded
`exact: true`.
* `cy.intercept()` + `cy.wait('@alias')` becomes `page.waitForResponse()`.
The interception is registered after the "Send an approval request"
button is clicked instead of before, so it can only match the call
triggered by the "Approval step" dropdown.
* The ticket is created in the worker entity.
Extra changes:
* The `describe()` blocks used as step labels inside the `it()` become
`test.step()`.
* The computer and the webhook are created in the worker entity, and the
webhook name is uuid suffixed (it was the shared literal "Test webhook",
which would also collide with the future webhooks.spec.ts).
* "Create a filter", "Save" and "Preview results" are matched with a
non-exact `getByRole()` instead of `GlpiPage::getButton()`: these
buttons embed a tabler icon, which is part of the accessible name
computed by playwright.
* `should('exist')` / `should('not.exist')` become `toBeAttached()` /
`not.toBeAttached()`.
* `invoke("removeClass", "d-none")` becomes an `evaluate()` call.
* The malformed `'input[name="criteria[0][value]"'` selector (no closing
bracket, tolerated by jQuery) is written as valid CSS. The existing
TODO about this bad selector is kept.
Extra changes:
* The cypress `beforeEach` becomes a `setupForm()` helper called at the
start of each test (see the number.spec.ts migration for the reason).
* The form is created in the worker entity with a uuid suffixed name.
* `cy.findByRole('region', {name: 'Question details'}).within(...)`
becomes two small locator helpers, `getCheckbox()` and
`getDefaultValueInput()`, scoped to the question locator. They are
re-resolved on each call since the default value input is re-rendered
every time the date/time checkboxes change.
* `.clear()` + `.type()` becomes a single `fill()`.
* `.invoke('val').should('match', /.../)` becomes
`expect(await input.inputValue()).toMatch(/.../)`.
Extra changes:
* The cypress `beforeEach` becomes a `setupForm()` helper returning the
ids the tests need (see the number.spec.ts migration for the reason).
The `Date.now()` key becomes a uuid.
* The form and the ITIL categories are created in the worker entity.
* `cy.hasDropdownValue()` has no playwright equivalent, so the spec
carries a local `assertDropdownHasValue()` helper. Unlike the cypress
command it *searches* the value instead of scanning the initial list of
options: the end user dropdown is loaded through ajax and only returns
a page of results, which is not enough now that every worker creates
its own categories in parallel.
* `invoke('val').should('deep.equal', [...])` on the multiple
`<select>` becomes an `evaluate()` reading `selectedOptions`.
* Setting "Filter ticket categories" actually *unselects* "Request
categories", so `doSetDropdownValue()` is called with
`check_value: false`.
* The profile reset to Super-Admin is wrapped in a `finally` since the
session is shared between the tests of a worker.
* `cy.checkAndCloseAlert('Item successfully updated')` becomes
`FormPage::doSaveFormEditor()`, which already asserts that alert.
Extra changes: * `cy.injectAndCheckA11y()` becomes an `AxeBuilder` run scoped to `#planning_container`, following the pattern already used in page_layout.spec.ts. None of the exclusions of the cypress `injectAndCheckA11y` command were needed here, so no rule is suppressed. * `cy.disableAnimations()` becomes the `page.waitForFunction()` on `document.getAnimations()` idiom used in page_layout.spec.ts. * Creates the tests/e2e/specs/Planning directory.
The 9 tests are appended to the existing actors.spec.ts (which covers
something else) inside an "Actor form question type" describe block, as
suggested by the migration study.
Extra changes:
* The cypress `beforeEach` becomes a `setupForm()` helper called at the
start of each test (see the number.spec.ts migration for the reason).
* The form and the groups are created in the worker entity, with uuid
suffixed names.
* The `'E2E Tests'` actor becomes the worker account. Its friendly name
("E2E worker account NN") is used when picking it in the dropdown and
its login ("e2e_worker_account_NN", from `getWorkerLogin()`) is used in
the assertions made after a reload, mirroring what the cypress test did
with "E2E Tests" / "e2e_tests".
* Option, "Users" checkbox and actor names are matched with a non-exact
`getByRole()`: those labels embed an icon or a login suffix which
playwright includes in the accessible name (cypress did not).
* `cy.wait(500)` with its "I don't know why this is needed" comment is
dropped: the test passes without it.
* "can disable itemtypes" now creates a `Group`. The cypress test entity
contained groups, the worker entities are empty, so the dropdown would
have shown "No results found" and no "Group" option group at all.
* `cy.reload()` after clicking "Save" becomes
`FormPage::doSaveFormEditorAndReload()`, and the question locator is
re-resolved after each reload.
* Removing the `target` attribute of the "Preview" link becomes
`FormPage::doPreviewForm()`.
* `cy.checkAndCloseAlert()` becomes a plain assertion on the alert.
The 5 tests are appended to the existing selectables.spec.ts (which
covers something else) inside a "Selectable form question types"
describe block, as suggested by the migration study.
Extra changes:
* The `describe()` blocks used as step labels inside the `it()`s
("Configure question" / "Fill form") are replaced by plain comments.
* The cypress `beforeEach` becomes a `setupForm()` helper called at the
start of each test (see the number.spec.ts migration for the reason).
The form is created in the worker entity with a uuid suffixed name.
* The three "test can duplicate a ..." titles lose their "test " prefix,
rejected by the `playwright/valid-title` eslint rule.
* The question name is now set before the question type instead of after
(`FormPage::addQuestion()` fills the name, which is focused right after
the question is added).
* `cy.reload()` after clicking "Save" becomes
`FormPage::doSaveFormEditorAndReload()`, and the question locators are
re-resolved after each reload.
* Removing the `target="_blank"` attribute of the "Preview" link becomes
`FormPage::doPreviewForm()`.
* The source question's default option assertion uses `toHaveText()` like
the duplicated one, instead of cypress' `findByRole('textbox', {name:
'Option 2'})` on the select2 container.
* `cy.findByText(...)` becomes `getByText(...).first()`: unlike
testing-library, playwright's `getByText` also matches the ancestors
holding the text.
Extra changes:
* The cypress `beforeEach` becomes a `setupForm()` helper called at the
start of each test (see the number.spec.ts migration for the reason).
The form is created in the worker entity with a uuid suffixed name.
* `cy.findByRole('region', {...}).within(...)` becomes locators scoped to
the region locator; they are re-resolved after "Save changes" since the
policy cards are re-rendered.
* The "Allow unauthenticated users ?" checkbox and the "Save changes"
button embed an icon, so their accessible name is matched non-exactly.
* The clipboard is read with `page.evaluate()` after granting the
`clipboard-read` / `clipboard-write` permissions.
* Creates the tests/e2e/specs/Form/AccessPolicies directory.
Extra changes: * `cy.injectAndCheckA11y()` becomes an `AxeBuilder` run scoped to `.modal.show`, following the pattern already used in page_layout.spec.ts. Five of the exclusions of the cypress `injectAndCheckA11y` command are actually triggered by this modal and are ported as `.exclude()` calls (TinyMCE, select2, JQuery file upload, the flatpickr altInput and the qtip tooltips used as labels); the nine others are dropped. * `cy.disableAnimations()` becomes the `page.waitForFunction()` on `document.getAnimations()` idiom used in page_layout.spec.ts. * The FullCalendar and bootstrap modal raw locators are kept as-is (with an eslint ignore), plus an explicit wait for the modal to be visible before running axe.
Extra changes:
* All the non-accessible selectors are kept verbatim, as the existing
TODO comment asks. The whole test body is wrapped in an
`eslint-disable playwright/no-raw-locators` block rather than one
ignore per line, since almost every locator is a raw one here.
* `cy.get(...).within(...)` becomes locators chained on a parent locator.
* `should('not.exist')` becomes `not.toBeAttached()` and
`should('not.be.empty')` becomes `not.toBeEmpty()`.
* The conditional assertions on the "Regex" button (only for the free
text tags) trigger `playwright/no-conditional-in-test` and
`playwright/no-conditional-expect`; the loop is kept as-is with an
eslint ignore and a comment.
* Creates the tests/e2e/specs/Kanban directory.
Extra changes:
* The ITIL object and the ticket are created in the worker entity.
* `cy.getMany([...])` disappears: the ids are plain awaited values.
* `cy.findByLabelText('Solution').awaitTinyMCE().type(...)` becomes
`GlpiPage::initRichTextByLabel()` + `fill()`.
* The "Actions" button embeds an icon, so its accessible name is matched
non-exactly.
* `cy.getWithAPI()` becomes `api.getItem()`.
* Creates the tests/e2e/specs/ITILObject directory.
Extra changes:
* The cypress `before()` becomes a `setupFixtures()` helper called at the
start of each test: a `beforeAll` would run once per worker and each
worker would only see its own copy of the fixtures. The `Date.now()`
key becomes a uuid.
* All the items are created in the worker entity.
* `cy.waitForNetworkIdle(500)` after applying a template becomes an
explicit `page.waitForResponse('**/ajax/task.php')`, which is the
request that actually applies the template.
* The conditional `if (!$checkbox.is(':checked')) check({force: true})`
becomes a plain `check()`: the checkbox is always unchecked on a new
task form, and `playwright/no-force-option` forbids the force option.
* The TinyMCE iframe is reached with `contentFrame()` instead of
`$iframe.contents()`, and `clear()` + `type()` becomes `fill()`.
* The `.itiltask`, `select[name=...]`, `input[name="pending"]` and
`[id^="pending-reasons-setup-"]` raw locators are kept (with an eslint
ignore) as these elements have no accessible name.
* Template and pending reason values are picked with
`doSearchAndClickDropdownValue()` rather than scanning the initial list
of options: every worker adds its own templates in parallel and the
dropdowns are paginated.
* The "View other actions" button and the "Create a task" link embed an
icon, so their accessible name is matched non-exactly.
Extra changes:
* `cy.intercept('/Form/Export?*')` + reading the filename from the
`content-disposition` header + `cy.readFile('cypress/downloads/...')`
becomes `page.waitForEvent('download')` and a read of
`download.path()`. Playwright hands us the file directly so the
filename is not needed any more.
* The forms are created in the worker entity with uuid suffixed names,
`cy.createFormWithAPI()` becoming `api.createItem()` and
`cy.visitFormTab('Form')` becoming `FormPage::goto()`.
* The "Actions" and "Export form" buttons embed an icon, so their
accessible name is matched non-exactly.
* Creates the tests/e2e/specs/Form/Serializer directory.
Extra changes:
* `getWorkerEntityName()` is ported from `main` into
`utils/WorkerEntities.ts`, as suggested by the migration study.
* The "export-of-2-forms.json" fixture hardcodes the cypress dataset
entity "Root entity > E2ETestEntity", which does not exist in the
playwright dataset: without a rewrite, "My valid form" would also be
reported as invalid and the whole spec would be meaningless. The
fixture is therefore read, its entity completename replaced by the
worker entity's, and handed to `setInputFiles()` as an in-memory
buffer. The file on disk is left untouched (it is still used by the
phpunit tests).
* `»E2ETestEntity` becomes `»<worker entity name>` and the "E2E Tests"
user becomes the worker account friendly name.
* Selecting the entity replacement is done with `check_value: false` plus
an explicit assertion: the option is listed with a "»" prefix but the
selected value is rendered with its completename.
* The repeated preview assertions are extracted in
`assertFormIsReadyToBeImported()` / `assertFormCannotBeImported()`.
* `cy.findAllByRole('row').as('preview')` becomes a live `getByRole('row')`
locator, and `should('not.exist')` becomes `not.toBeAttached()` /
`toHaveCount(0)`.
* The "Import forms" button embeds an icon, so its accessible name is
matched non-exactly.
Extra changes: * `GlpiPage::getRichTextByLabel()` and `initRichTextByLabel()` gained an optional `exact` argument (defaulting to the previous partial match). The rendered "Description" question also contains a "Question description" note, which the partial match caught, resulting in a strict mode violation. * The top-level cypress `before()` becomes a `setupFixtures()` helper called at the start of each test: a `beforeAll` would run once per worker and each worker would only see its own copy. The `Date.now()` key becomes a uuid. * The ITILCategory, Computer and Location are created in the worker entity, and the computer belongs to the worker user instead of the hardcoded `users_id: 7`. * The profile is restored to Super-Admin in a `finally` block since the session is shared between the tests of a worker. * Values are picked with `doSearchAndClickDropdownValue()` (which handles the "»" prefix of the tree dropdowns) instead of scanning the initial list of options, since every worker adds its own items in parallel. "User devices" and "Observers" are matched non-exactly, their options being prefixed by the itemtype / suffixed by the login. * `cy.getWithAPI()` becomes `api.getItem()`. * Creates the tests/e2e/specs/Form/ServiceCatalog directory.
Extra changes:
* The cypress `before()` becomes a `setupCategory()` helper called at the
start of each test: a `beforeAll` would run once per worker and each
worker would only see its own copy. The `Date.now()` key becomes a
uuid. `Glpi\Form\Category` has no `entities_id` column so it stays
global, like in the existing category.spec.ts.
* The form and the knowbase item are created in the worker entity with
uuid suffixed names (the knowbase item is pinned to the top of the
catalog of the entity it lives in).
* `cy.findByLabelText("Description").awaitTinyMCE()` becomes
`GlpiPage::initRichTextByLabel()`, and `.type()` becomes `fill()` (the
field is empty at that point).
* Selecting the category is done with `check_value: false` plus an
explicit assertion: the option is listed with a "»" prefix but the
selected value is rendered without it.
* The "Save changes" button embeds an icon, so its accessible name is
matched non-exactly.
* `should('have.css', ...)` becomes `toHaveCSS(...)`, and the
`[data-service-catalog-config]` raw locator is kept with an eslint
ignore.
Already migrated: tests/e2e/specs/Form/QuestionTypes/dropdown.spec.ts covers the same 9 tests, in the same order, with the same coverage. Only the titles differ, having lost their "test " prefix.
Extra changes:
* The cypress `before()` becomes a `setupWebhook()` helper called at the
start of each test: a `beforeAll` would run once per worker and each
worker would only see its own copy.
* The webhook is created in the worker entity (it was `entities_id: 1`,
which is the shared parent in the playwright dataset) with a uuid
suffixed name, also keeping it distinct from the one created by
Search/filterable.spec.ts.
* `getByLabel(/^\s*Secret/)`: the `\s*` is needed because playwright does
not normalize whitespaces when matching with a regular expression, and
the label content is indented. A plain `/^Secret/` matched nothing.
* `.next()` / `.next().next()` become `+ *` / `+ * + *` raw locators (with
an eslint ignore), the disclose and copy buttons having no accessible
name.
* `.trigger('mousedown')` / `.trigger('mouseup')` become
`dispatchEvent()`; this branch's markup needs these events rather than
a click.
* `invoke('outerWidth'/'outerHeight')` becomes a single `boundingBox()`
read.
* The clipboard is read with `page.evaluate()` after granting the
`clipboard-read` / `clipboard-write` permissions.
* A TODO is left about the per-test `{retries: 0}` of the payload editor
test, which playwright can only configure per file or per describe.
* Creates the tests/e2e/specs/Setup/webhooks.spec.ts file next to the
existing notifications.spec.ts.
Extra changes:
* The cypress `before()` becomes a `setupCalendar()` helper called at the
start of each test: a `beforeAll` would run once per worker and each
worker would only see its own copy.
* The calendar and the holiday are created in the worker entity (they
were `entities_id: 1`, the shared parent in the playwright dataset)
with uuid suffixed names. This matters most for the holiday, which is
perpetual and would otherwise show up in every worker's calendar.
* The holiday is picked with `doSearchAndClickDropdownValue()` rather
than scanning the initial list of options, since every worker adds its
own holidays in parallel.
* `cy.findAllByRole('row')` becomes a live `getByRole('row')` locator
re-resolved from the tabpanel after the "Add" click, and the length
assertion becomes `toHaveCount(2)`.
* Creates the tests/e2e/specs/Setup/Dropdowns directory.
Extra changes:
* The cypress `beforeEach` becomes a `setupDcRoom()` helper called at the
start of the test, returning the ids and names it needs.
* The datacenter, the room and the three racks are created in the worker
entity (they were `entities_id: 1`, the shared parent in the playwright
dataset) with uuid suffixed names, the fixed "DC for E2E" names would
have collided between workers.
* The "View graphical representation" button only holds an icon so its
accessible name is empty; it is reached with `getByTitle()`.
* `cy.contains('...')` on the grid items becomes
`filter({ hasText: ... })`.
* `cy.url().should('include', ...)` becomes `toHaveURL()` with a regexp.
* The level-3 heading assertion and its `//TODO` are kept as-is (this
branch renders it at level 3, unlike `main`).
* The `table.outbound`, `div.cell_add[...]` and `div.grid-stack-item` raw
locators are kept with an eslint ignore.
Extra changes:
* The cypress `before()` becomes a `setupReservableItems()` helper called
at the start of each test: a `beforeAll` would run once per worker and
each worker would only see its own copy.
* The computer and the monitor are created in the worker entity (they
were `entities_id: 1`, the shared parent in the playwright dataset)
with uuid suffixed names, the fixed "Reservable computer" /
"Reservable monitor" names would have collided between workers.
* The "E2E Tests" reserving user becomes the worker account, picked with
`doSearchAndClickDropdownValue()`.
* The "Find available reservation items" cell assertions now include the
item name: with unique names per worker, `Computers - ` alone is not
precise enough to prove *our* item is the one listed.
* Deleting a reservation opens a native confirmation dialog. Cypress
accepts those automatically, playwright dismisses them, so a
`page.once('dialog', ...)` handler is added.
* The `cy.on('uncaught:exception')` workaround for "tinyMCE is not
defined" is dropped (with a note): playwright does not fail a test on
page errors.
* `cy.findByLabelText('Item type').select(value, {force: true})` becomes
`selectOption()`, `playwright/no-force-option` forbidding the force
option (and it turned out not to be needed).
* `cy.url().should('include'/'match', ...)` becomes `toHaveURL()`.
* This branch's old FullCalendar raw locators (`.fc-week .fc-day`,
`.fc-day-grid-event`, `.fc-header-toolbar .fc-center`, the `prev`
label) are kept as-is with an eslint ignore.
Extra changes:
* The cypress version passed an `entity: 1` key to `createWithAPI`, which
is not a valid field: the items silently ended up in the active entity
of the API session (E2ETestEntity, hence the assertion on
"Root entity > E2ETestEntity"). `entities_id: getWorkerEntityId()` is
now passed explicitly and the assertion targets the worker entity
completename.
* The fixed "Budget for E2E test" / "Computer for budget" /
"Graphic card for budget" names are uuid suffixed, and the assertions
use those names so they cannot match another worker's row.
* The nested `cy.get('@alias').then(...)` pyramid disappears: the ids are
plain awaited values.
* `cy.getRowCells()` has no playwright equivalent, so the spec carries a
local `assertRowCells()` helper mapping a row's cells to their column
header. It reads the headers with `allTextContents()` and not
`allInnerTexts()`, the headers being uppercased through CSS.
* Creates the tests/e2e/specs/Management directory.
Placed in tests/e2e/specs/Helpdesk/ next to the existing
home_config.spec.ts and service_catalog.spec.ts.
Extra changes:
* The knowbase item and the tickets are created in the worker entity and
all names are uuid suffixed ("Open ticket 1" & co were shared
literals).
* `users_id: 7` on the tickets was silently ignored (`glpi_tickets` has
no `users_id` column), so cypress relied on the requester defaulting to
the API session user, which happened to be the logged in user. The
playwright API account is a different user, so the requester is now set
explicitly with `_users_id_requester: getWorkerUserId()`, otherwise the
helpdesk lists were empty.
* `users_id` is dropped from the `Reminder`: `Reminder::canCreateItem()`
only allows creating a reminder you own, and the API account can not
impersonate the worker user. This has no impact on the test, the
helpdesk tab lists the *public* reminders (the ones the current user is
a visibility target of, i.e. the `Reminder_User` row) and ignores the
author.
* The column headers assertion is relaxed from 6 to 5 columns and the
"Entity" one is dropped, with a TODO: that column is only added when
more than one entity is active in the session, and a playwright worker
is pinned to its single childless entity. The pre-existing dependency
on the global DisplayPreference rows is documented in a second TODO.
* `cy.request({url, failOnStatusCode: true})` on the tiles becomes
`page.request.get()` + an `ok()` assertion.
* `cy.findAllByText(...)` becomes `getByText(...).first()`: unlike
testing-library, playwright's `getByText` also matches the ancestors
holding the text.
* The reset to the "Ongoing tickets" tab is kept, the last opened tab
being stored in the session.
Placed in tests/e2e/specs/Helpdesk/ next to the existing
home_config.spec.ts and service_catalog.spec.ts.
Extra changes:
* The forms are now created with an explicit
`entities_id: getWorkerEntityId()`. `cy.createFormWithAPI()` relied on
the active entity of the API session, which is the root entity for the
playwright API account: the forms would have been visible to every
worker.
* The knowbase item `_visibility` moves from `entities_id: 1` (the shared
parent) to the worker entity, and stops being recursive.
* `Date.now()` becomes a uuid with its dashes stripped, so it stays a
single searchable word while being collision free between workers.
* The profile is restored to Super-Admin in a `finally` block since the
session is shared between the tests of a worker; the unpinning of the
pinned form also moves to a `finally` so it happens even if the test
fails (a pinned form shows at the top of the service catalog of its
entity).
* The trailing `true` argument of `cy.changeProfile('Self-Service', true)`
is dropped: it has never been read by the command.
* `.clear()` + `.type()` becomes a single `fill()`, and
`should('not.exist')` becomes `not.toBeAttached()`.
Extra changes:
* The single cypress `after()` calling `cy.disableDebugMode()` becomes an
`afterEach` using the `debug` fixture: an `afterAll` would only run
once per worker while the debug flag lives in the session shared by all
the tests of that worker. The same hook restores the Super-Admin
profile, the first test switching to Admin.
* The non-accessible selectors are kept verbatim (with an eslint ignore),
with a `.first()` on the "Change mode" item: the user menu is rendered
twice, only one being shown depending on the screen size.
* The bogus `should('not.exist', {timeout: 200})` option is dropped;
`not.toBeAttached()` already retries.
* `invoke('attr', 'href').should('include', ...)` becomes
`toHaveAttribute()` with a regexp.
* Creates the tests/e2e/specs/DebugMode directory.
Extra changes:
* `DebugModeSwitcher` had to be fixed first. It posted the wanted mode to
`ajax/switchdebug.php`, which stores the raw (thus string) POST value
into `$_SESSION['glpi_use_mode']`, while `Html::displayFooter()`
compares it *strictly* to the integer `Session::DEBUG_MODE`: the debug
toolbar was never rendered. It now goes through the toggle branch of
the endpoint (no mode sent, which is what the "Change mode" link of the
user menu does), so GLPI stores the integer constant. The current mode
is read back from the presence of the toolbar. The existing consumers
(error_page.spec.ts, debug_mode.spec.ts) still pass.
* The single cypress `after()` calling `cy.disableDebugMode()` becomes an
`afterEach`: an `afterAll` would only run once per worker while the
debug flag lives in the session shared by all its tests.
* The theme switcher test restores the "auror" palette in a `finally`, the
palette being stored on the user.
* All the non-accessible selectors are kept verbatim; the whole file is
wrapped in an `eslint-disable playwright/no-raw-locators` block rather
than one ignore per line.
* The "Toggle debug bar", "Toggle debug content area", "Close" and
"Toggle manual input" buttons only hold an icon: playwright computes
their accessible name from the icon glyph rather than falling back to
the title, so they are reached with `getByTitle()`.
* `invoke('text').should('match', ...)` becomes `toContainText(regexp)`.
One regexp needed `\s+` instead of `\s`: unlike testing-library,
playwright does not normalize whitespaces when matching a regexp.
* `getDatagridValue()` keeps a `.first()` to mirror cypress' `contains()`,
which returns the first partial match ("Total resources" also matches
"Total resources size").
* `cy.intercept()` + `cy.wait('@alias')` becomes `page.waitForResponse()`
filtering on the pathname and the `action` query parameter.
* `.clear()` + `.type('User{enter}')` becomes `fill()` + `press('Enter')`.
Named entity_creation.spec.ts, tests/e2e/specs/Entity/entity.spec.ts
already existing with a different coverage (assistance properties, survey
options).
Extra changes:
* The test now starts by switching to the worker entity *with* recursion.
The creation of the first sub entity would otherwise be refused by
`Entity::canCreateChild()` as soon as a previous run left sub entities
behind.
* The entity context switches go through the `entity` fixture instead of
the entity selector UI. The tree is driven by fancytree, which only
renders a handful of rows at a time and rebuilds them when filtered,
making an entity created by the test itself unreachable in practice
(the switch button of a filtered row does not even submit its form).
The entity selector UI is already covered by entity_selector.spec.ts.
* The last step used "Select all" (the whole structure), which would
create the last entity in the root entity, outside of the worker
entity. It now switches to the sub entity *with* recursion, which
exercises the same `Entity::canCreateChild()` branch while staying
scoped.
* `Math.random()` becomes a uuid.
* `cy.intercept()` + `cy.wait('@alias')` becomes `page.waitForResponse()`
filtered on the POST method, and
`cy.visit(url, {failOnStatusCode: false})` becomes a `page.goto()` whose
returned status is asserted.
* An `afterEach` restores the worker entity, as entity_selector.spec.ts
already does.
* A TODO is left about the three created entities not being purged: the
API refuses to delete an `Entity`. They all live inside the worker
entity so they do not leak to the other workers.
Extra changes:
* The three `cy.logout()` calls are replaced by the `anonymousPage`
fixture: logging out would destroy the storage state shared by all the
tests of the worker.
* The cypress `beforeEach` becomes a `setupForm()` helper called at the
start of each test (see the number.spec.ts migration for the reason).
The form is created in the worker entity.
* The profile is restored to Super-Admin in a `finally` block for the two
tests that switch to Self-Service, the session being shared between the
tests of a worker.
* The "Save changes" button and the "Allow unauthenticated users ?"
checkbox embed an icon, so their accessible name is matched
non-exactly.
* `cy.focused().type(...)` after "Add a question" becomes
`FormPage::addQuestion()`, and
`findByRole('combobox', {name: 'Short answer'}).select('Actors')`
becomes `FormPage::setQuestionType()`.
* Closing the save alert becomes `FormPage::doSaveFormEditor()`, which
asserts the alert instead (nothing is left to un-obstruct).
* The direct access URL is read once with `inputValue()`, and the
"not regenerated" check becomes a `toHaveValue()` assertion.
The 3 tests are appended to the existing item.spec.ts, in a separate "Item form question type - default values" describe block: unlike the suite already there, they need to *save* the form, which requires the session to stay on the entity holding it. They are ported from the version `main` already has (commit ccead16), as suggested by the migration study, minus its "Defining multiple tickets as default value" test which covers a feature (glpi-project#24081) that does not exist on 11.0. Their titles are `main`'s. Extra changes: * `cy.updateWithAPI('Profile_User', 6, {entities_id: 0})` and its reset to `entities_id: 1` are cypress-dataset specific. As on `main`, a temporary `Profile_User` granting the worker user a recursive Super-Admin access to the root entity is created instead, then purged, with `profile.invalidateCachedProfile()` around it. * The items used as default values are created in a dedicated sub entity with uuid suffixed names: `main` relied on a fresh entity per test, but the form importer of this branch always imports into the worker entity, so the session stays there (recursively) and a rerun would otherwise find the items of the previous run. * `cy.waitForNetworkIdle(150)` is dropped, the retrying assertions of `doSetDropdownValue()` cover it. * `click({force: true})` on the Save button (obstructed by a toast) is not needed any more, `playwright/no-force-option` forbidding it anyway. * Removing the `target="_blank"` attribute of the "Preview" link becomes `FormPage::doPreviewForm()`.
Extra changes:
* `test.describe.configure({ mode: 'serial' })`: the cypress version
relied on the two presets running in order so the default value was
restored by the last one. The default is now restored explicitly in an
`afterEach`.
* `cy.updateTestUserSettings()` (hardcoded on `User, 7`) is replaced by a
POST on `/front/preference.php` for `getWorkerUserId()`.
`api.updateItem('User', ...)` would not have worked:
`show_search_form` is read from `$_SESSION['glpishow_search_form']`,
which is only refreshed at login or when the user updates its own
preferences. Cypress got away with an API call because it logged in
again before each test, a playwright worker reuses a single session.
The request is sent as an ajax one so the CSRF token is preserved and
can be reused, like the other session switchers of `utils/` do.
* The trashbin, browse mode and unpublished toggles are stored in the
session shared by all the tests of a worker, and every test asserts
they are off by default: each one is now toggled back in a `finally`
block.
* `findByRole('radio', {name: ...}).next().click()` becomes a `+ *` raw
locator (with an eslint ignore): the radio inputs are visually hidden
and only their label is clickable.
* `should('not.exist')` becomes `not.toBeAttached()`.
AdrienClairembault
force-pushed
the
cypress-to-playright1
branch
from
September 4, 2026 09:00
a0f373f to
ebe4d8f
Compare
AdrienClairembault
marked this pull request as ready for review
September 4, 2026 09:20
trasher
approved these changes
Sep 4, 2026
trasher
left a comment
Contributor
There was a problem hiding this comment.
Reviewed PHP code and changes that have a global impact.
Test are green, test migration itself should be OK.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Migrate most of the remaining cypress test to playwright.
Only 5 cypress test files remains after this, I will look at them in another PR because they have some specific requirements (for example, the maintenance test can't run at the same time as the rest of the suite as putting GLPI in maintenance mode would block all others test workers).
Please note tests have been migrated as verbatim as possible (any forced changes are detailed in each commits).
The goals is not to improve them but to migrate them to playwright so we can remove our cypress dependency.