Skip to content

Migrate most cypress tests to playwright - #25388

Merged
trasher merged 38 commits into
glpi-project:11.0/bugfixesfrom
AdrienClairembault:cypress-to-playright1
Sep 4, 2026
Merged

Migrate most cypress tests to playwright#25388
trasher merged 38 commits into
glpi-project:11.0/bugfixesfrom
AdrienClairembault:cypress-to-playright1

Conversation

@AdrienClairembault

@AdrienClairembault AdrienClairembault commented Sep 3, 2026

Copy link
Copy Markdown
Member

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.

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
AdrienClairembault force-pushed the cypress-to-playright1 branch 2 times, most recently from 54e6b06 to a0f373f Compare September 3, 2026 14:36
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
AdrienClairembault marked this pull request as ready for review September 4, 2026 09:20

@trasher trasher left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed PHP code and changes that have a global impact.

Test are green, test migration itself should be OK.

@trasher
trasher merged commit 6e2ced0 into glpi-project:11.0/bugfixes Sep 4, 2026
13 checks passed
@AdrienClairembault
AdrienClairembault deleted the cypress-to-playright1 branch September 4, 2026 10:18
@cedric-anne cedric-anne added this to the 11.0.9 milestone Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants