@verdaccio/e2e-ui 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/build/cjs/features.cjs +14 -0
  2. package/build/cjs/features.cjs.map +1 -1
  3. package/build/cjs/index.cjs +3 -0
  4. package/build/cjs/index.cjs.map +1 -1
  5. package/build/cjs/testIds.cjs +23 -1
  6. package/build/cjs/testIds.cjs.map +1 -1
  7. package/build/cjs/tests/change-password.cjs +20 -6
  8. package/build/cjs/tests/change-password.cjs.map +1 -1
  9. package/build/cjs/tests/home-detail-ux.cjs +166 -0
  10. package/build/cjs/tests/home-detail-ux.cjs.map +1 -0
  11. package/build/cjs/tests/index.cjs +2 -0
  12. package/build/cjs/tests/publish.cjs +1 -1
  13. package/build/cjs/tests/publish.cjs.map +1 -1
  14. package/build/cjs/tests/settings.cjs +5 -2
  15. package/build/cjs/tests/settings.cjs.map +1 -1
  16. package/build/esm/features.js +14 -0
  17. package/build/esm/features.js.map +1 -1
  18. package/build/esm/index.js +3 -1
  19. package/build/esm/index.js.map +1 -1
  20. package/build/esm/testIds.js +23 -1
  21. package/build/esm/testIds.js.map +1 -1
  22. package/build/esm/tests/change-password.js +20 -6
  23. package/build/esm/tests/change-password.js.map +1 -1
  24. package/build/esm/tests/home-detail-ux.js +166 -0
  25. package/build/esm/tests/home-detail-ux.js.map +1 -0
  26. package/build/esm/tests/index.js +2 -1
  27. package/build/esm/tests/publish.js +1 -1
  28. package/build/esm/tests/publish.js.map +1 -1
  29. package/build/esm/tests/settings.js +5 -2
  30. package/build/esm/tests/settings.js.map +1 -1
  31. package/build/features.d.ts +45 -0
  32. package/build/index.d.ts +1 -1
  33. package/build/testIds.d.ts +34 -0
  34. package/build/tests/home-detail-ux.d.ts +10 -0
  35. package/build/tests/index.d.ts +1 -0
  36. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"change-password.js","names":[],"sources":["../../../src/tests/change-password.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Tests for the Change Password page at /-/web/change-password.\n *\n * The page renders only when the server is started with\n * `flags.changePassword: true` (otherwise the React component\n * redirects to `/` on mount). Each test logs in first, navigates\n * directly to the page, and drives the form.\n *\n * Selector strategy: the ChangePassword form does not ship stable\n * `id`/testid attributes on its inputs, but every field is registered\n * via react-hook-form's `register('<name>')`, which sets a stable\n * `name` attribute on the underlying `<input>`. The labels themselves\n * are `t('security.changePassword.*')` calls — when the i18n bundle\n * hasn't finished loading (or isn't loaded at all on this route), MUI\n * renders the literal i18n key as the label, so any selector that\n * matches on visible label text silently misses every field and the\n * form stays empty (which would also make the mismatch test \"pass\"\n * for the wrong reason: the submit button is disabled because the\n * form is empty, not because yup rejected the mismatch).\n *\n * The stable, i18n-independent contract from ChangePassword.tsx is:\n * register('username') → input[name=\"username\"]\n * register('oldPassword') → input[name=\"oldPassword\"]\n * register('newPassword') → input[name=\"newPassword\"]\n * register('confirmPassword') → input[name=\"confirmPassword\"]\n * submit button → form button[type=\"submit\"]\n */\nexport function changePasswordTests(config: RegistryConfig) {\n const { header, login } = config.testIds;\n const { loginDialog } = config.selectors;\n const { features } = config;\n\n // The wrongOldPassword banner text differs across verdaccio lines: older\n // published ui-theme shows a hardcoded \"Failed to change password\", while\n // current master surfaces the server message (\"internal server error\") or\n // the i18n fallback (\"Unable to change password\") via authErrorMessage. The\n // contract this test pins is \"a non-empty error banner appears, not\n // silence\", so it asserts visibility + non-empty text rather than an exact\n // string. The Express \"Cannot PUT\" 404 body is the one text that must never\n // appear — that means the reset_password route was not registered (the\n // server was started without flags.changePassword).\n\n describe('change password', () => {\n const CHANGE_PASSWORD_PATH = '/-/web/change-password';\n const { user, password } = config.credentials;\n\n /**\n * Tests mutate the user's password. We track the \"current\" value\n * across tests so the `after()` hook can restore the original,\n * leaving the registry in the same state other suites assume.\n */\n let currentPassword = password;\n\n /**\n * Capability check. The ChangePassword page's `useEffect` redirects\n * to `/` whenever `configuration.flags.changePassword` is not truthy\n * — which is the case on any registry that either (a) didn't set\n * `flags.changePassword: true` in its config, or (b) runs a\n * verdaccio build whose middleware doesn't yet propagate the flag\n * into `__VERDACCIO_BASENAME_UI_OPTIONS` (older `verdaccio:6`\n * tagged images fall in this bucket).\n *\n * In either case the suite has nothing to exercise, so skip the\n * whole describe with a clear reason rather than burning five\n * seconds per test on cy.contains timeouts.\n */\n before(function () {\n cy.visit(config.registryUrl);\n cy.window().then((win) => {\n const opts = (win as any).__VERDACCIO_BASENAME_UI_OPTIONS;\n const enabled = !!opts?.flags?.changePassword;\n if (!enabled) {\n // eslint-disable-next-line no-console\n console.warn(\n '[change-password] server did not advertise flags.changePassword=true ' +\n '— skipping suite. ui-options.flags: ' +\n JSON.stringify(opts?.flags ?? {})\n );\n this.skip();\n }\n });\n });\n\n beforeEach(() => {\n // Intercept the login POST and wait on it explicitly instead of\n // leaning on a visual sentinel — mirrors the pattern used by\n // signinTests and avoids a race between cy.login's fire-and-forget\n // submit and the next cy.visit.\n cy.intercept('POST', '/-/verdaccio/sec/login').as('signChangePwd');\n cy.visit(config.registryUrl);\n cy.login(user, currentPassword, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@signChangePwd').its('response.statusCode').should('eq', 200);\n\n cy.visit(CHANGE_PASSWORD_PATH);\n // If flags.changePassword is off server-side, the component's\n // useEffect redirects to `/` and this assertion times out — which\n // is the correct signal that the registry is misconfigured.\n // Use the stable `type=\"submit\"` selector so the assertion is\n // independent of whether the i18n bundle has resolved by now.\n cy.get('form button[type=\"submit\"]', { timeout: 5000 }).should('be.visible');\n });\n\n after(() => {\n // Restore the original password so subsequent spec files\n // (and retries) can still log in with `config.credentials`.\n if (currentPassword === password) return;\n cy.intercept('POST', '/-/verdaccio/sec/login').as('signChangePwdRestore');\n cy.visit(config.registryUrl);\n cy.login(user, currentPassword, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@signChangePwdRestore').its('response.statusCode').should('eq', 200);\n cy.visit(CHANGE_PASSWORD_PATH);\n cy.get('input[name=\"username\"]').type(user);\n cy.get('input[name=\"oldPassword\"]').type(currentPassword);\n cy.get('input[name=\"newPassword\"]').type(password);\n cy.get('input[name=\"confirmPassword\"]').type(password);\n cy.get('form button[type=\"submit\"]').click();\n currentPassword = password;\n });\n\n // ── Validation (client-side yup) ─────────────────────────────\n\n maybeIt(features.changePassword.validation)(\n 'should disable the submit button while the form is empty',\n () => {\n cy.get('form button[type=\"submit\"]').should('be.disabled');\n }\n );\n\n maybeIt(features.changePassword.validation)(\n 'should keep submit disabled when new and confirm passwords mismatch',\n () => {\n cy.get('input[name=\"username\"]').type(user);\n cy.get('input[name=\"oldPassword\"]').type(currentPassword);\n cy.get('input[name=\"newPassword\"]').type('newSecretPass123');\n cy.get('input[name=\"confirmPassword\"]').type('different-value');\n // yup schema rejects mismatch → isValid stays false → button disabled.\n cy.get('form button[type=\"submit\"]').should('be.disabled');\n }\n );\n\n // ── Server error path ────────────────────────────────────────\n\n maybeIt(features.changePassword.wrongOldPassword)(\n 'should show an error banner when the old password is wrong',\n () => {\n cy.intercept('PUT', '/-/verdaccio/sec/reset_password').as('reset');\n cy.get('input[name=\"username\"]').type(user);\n cy.get('input[name=\"oldPassword\"]').type('definitely-wrong-xyz');\n cy.get('input[name=\"newPassword\"]').type('newSecretPass123');\n cy.get('input[name=\"confirmPassword\"]').type('newSecretPass123');\n cy.get('form button[type=\"submit\"]').should('not.be.disabled').click();\n // Server rejects the wrong password (the route exists → not a 404).\n cy.wait('@reset').its('response.statusCode').should('not.eq', 200);\n // onSubmit's catch sets errors.root → rendered via LoginDialogFormError.\n // Assert a non-empty banner (not silence), text-agnostic across lines,\n // but never the raw \"Cannot PUT\" 404 (route missing).\n cy.getByTestId(login.error, { timeout: 5000 })\n .should('be.visible')\n .and('not.contain.text', 'Cannot PUT')\n .invoke('text')\n .then((text) => expect(text.trim()).to.not.be.empty);\n // Still on the change-password page so the user can retry.\n cy.location('pathname').should('include', CHANGE_PASSWORD_PATH);\n }\n );\n\n // ── Happy path (mutates state; keeps `currentPassword` in sync) ─\n\n maybeIt(features.changePassword.happyPath)(\n 'should change the password and navigate to the success page',\n () => {\n const newPassword = `${currentPassword}-rotated`;\n cy.intercept('PUT', '/-/verdaccio/sec/reset_password').as('reset');\n\n cy.get('input[name=\"username\"]').type(user);\n cy.get('input[name=\"oldPassword\"]').type(currentPassword);\n cy.get('input[name=\"newPassword\"]').type(newPassword);\n cy.get('input[name=\"confirmPassword\"]').type(newPassword);\n cy.get('form button[type=\"submit\"]').should('not.be.disabled').click();\n\n cy.wait('@reset').its('response.statusCode').should('eq', 200);\n // Post-submit the component navigates to Route.SUCCESS with a\n // messageType query param. Assert the pathname; the message text\n // is i18n-driven and out of scope for this selector layer.\n cy.location('pathname', { timeout: 5000 }).should('include', '/-/web/success');\n\n // Track the rotation so `after()` can restore it.\n currentPassword = newPassword;\n }\n );\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,oBAAoB,QAAwB;CAC1D,MAAM,EAAE,QAAQ,UAAU,OAAO;CACjC,MAAM,EAAE,gBAAgB,OAAO;CAC/B,MAAM,EAAE,aAAa;CAYrB,SAAS,yBAAyB;EAChC,MAAM,uBAAuB;EAC7B,MAAM,EAAE,MAAM,aAAa,OAAO;;;;;;EAOlC,IAAI,kBAAkB;;;;;;;;;;;;;;EAetB,OAAO,WAAY;GACjB,GAAG,MAAM,OAAO,WAAW;GAC3B,GAAG,OAAO,CAAC,CAAC,MAAM,QAAQ;IACxB,MAAM,OAAQ,IAAY;IAE1B,IAAI,CAAC,CADY,CAAC,MAAM,OAAO,gBACjB;KAEZ,QAAQ,KACN,8GAEE,KAAK,UAAU,MAAM,SAAS,CAAC,CAAC,CACpC;KACA,KAAK,KAAK;IACZ;GACF,CAAC;EACH,CAAC;EAED,iBAAiB;GAKf,GAAG,UAAU,QAAQ,wBAAwB,CAAC,CAAC,GAAG,eAAe;GACjE,GAAG,MAAM,OAAO,WAAW;GAC3B,GAAG,MAAM,MAAM,iBAAiB;IAC9B,aAAa,OAAO;IACpB,GAAG;GACL,CAAC;GACD,GAAG,KAAK,gBAAgB,CAAC,CAAC,IAAI,qBAAqB,CAAC,CAAC,OAAO,MAAM,GAAG;GAErE,GAAG,MAAM,oBAAoB;GAM7B,GAAG,IAAI,gCAA8B,EAAE,SAAS,IAAK,CAAC,CAAC,CAAC,OAAO,YAAY;EAC7E,CAAC;EAED,YAAY;GAGV,IAAI,oBAAoB,UAAU;GAClC,GAAG,UAAU,QAAQ,wBAAwB,CAAC,CAAC,GAAG,sBAAsB;GACxE,GAAG,MAAM,OAAO,WAAW;GAC3B,GAAG,MAAM,MAAM,iBAAiB;IAC9B,aAAa,OAAO;IACpB,GAAG;GACL,CAAC;GACD,GAAG,KAAK,uBAAuB,CAAC,CAAC,IAAI,qBAAqB,CAAC,CAAC,OAAO,MAAM,GAAG;GAC5E,GAAG,MAAM,oBAAoB;GAC7B,GAAG,IAAI,0BAAwB,CAAC,CAAC,KAAK,IAAI;GAC1C,GAAG,IAAI,6BAA2B,CAAC,CAAC,KAAK,eAAe;GACxD,GAAG,IAAI,6BAA2B,CAAC,CAAC,KAAK,QAAQ;GACjD,GAAG,IAAI,iCAA+B,CAAC,CAAC,KAAK,QAAQ;GACrD,GAAG,IAAI,8BAA4B,CAAC,CAAC,MAAM;GAC3C,kBAAkB;EACpB,CAAC;EAID,QAAQ,SAAS,eAAe,UAAU,CAAC,CACzC,kEACM;GACJ,GAAG,IAAI,8BAA4B,CAAC,CAAC,OAAO,aAAa;EAC3D,CACF;EAEA,QAAQ,SAAS,eAAe,UAAU,CAAC,CACzC,6EACM;GACJ,GAAG,IAAI,0BAAwB,CAAC,CAAC,KAAK,IAAI;GAC1C,GAAG,IAAI,6BAA2B,CAAC,CAAC,KAAK,eAAe;GACxD,GAAG,IAAI,6BAA2B,CAAC,CAAC,KAAK,kBAAkB;GAC3D,GAAG,IAAI,iCAA+B,CAAC,CAAC,KAAK,iBAAiB;GAE9D,GAAG,IAAI,8BAA4B,CAAC,CAAC,OAAO,aAAa;EAC3D,CACF;EAIA,QAAQ,SAAS,eAAe,gBAAgB,CAAC,CAC/C,oEACM;GACJ,GAAG,UAAU,OAAO,iCAAiC,CAAC,CAAC,GAAG,OAAO;GACjE,GAAG,IAAI,0BAAwB,CAAC,CAAC,KAAK,IAAI;GAC1C,GAAG,IAAI,6BAA2B,CAAC,CAAC,KAAK,sBAAsB;GAC/D,GAAG,IAAI,6BAA2B,CAAC,CAAC,KAAK,kBAAkB;GAC3D,GAAG,IAAI,iCAA+B,CAAC,CAAC,KAAK,kBAAkB;GAC/D,GAAG,IAAI,8BAA4B,CAAC,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM;GAErE,GAAG,KAAK,QAAQ,CAAC,CAAC,IAAI,qBAAqB,CAAC,CAAC,OAAO,UAAU,GAAG;GAIjE,GAAG,YAAY,MAAM,OAAO,EAAE,SAAS,IAAK,CAAC,CAAC,CAC3C,OAAO,YAAY,CAAC,CACpB,IAAI,oBAAoB,YAAY,CAAC,CACrC,OAAO,MAAM,CAAC,CACd,MAAM,SAAS,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,KAAK;GAErD,GAAG,SAAS,UAAU,CAAC,CAAC,OAAO,WAAW,oBAAoB;EAChE,CACF;EAIA,QAAQ,SAAS,eAAe,SAAS,CAAC,CACxC,qEACM;GACJ,MAAM,cAAc,GAAG,gBAAgB;GACvC,GAAG,UAAU,OAAO,iCAAiC,CAAC,CAAC,GAAG,OAAO;GAEjE,GAAG,IAAI,0BAAwB,CAAC,CAAC,KAAK,IAAI;GAC1C,GAAG,IAAI,6BAA2B,CAAC,CAAC,KAAK,eAAe;GACxD,GAAG,IAAI,6BAA2B,CAAC,CAAC,KAAK,WAAW;GACpD,GAAG,IAAI,iCAA+B,CAAC,CAAC,KAAK,WAAW;GACxD,GAAG,IAAI,8BAA4B,CAAC,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM;GAErE,GAAG,KAAK,QAAQ,CAAC,CAAC,IAAI,qBAAqB,CAAC,CAAC,OAAO,MAAM,GAAG;GAI7D,GAAG,SAAS,YAAY,EAAE,SAAS,IAAK,CAAC,CAAC,CAAC,OAAO,WAAW,gBAAgB;GAG7E,kBAAkB;EACpB,CACF;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"change-password.js","names":[],"sources":["../../../src/tests/change-password.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Tests for the Change Password page at /-/web/change-password.\n *\n * The page renders only when the server is started with\n * `flags.changePassword: true` (otherwise the React component\n * redirects to `/` on mount). Each test logs in first, navigates\n * directly to the page, and drives the form.\n *\n * Selector strategy: the ChangePassword form does not ship stable\n * `id`/testid attributes on its inputs, but every field is registered\n * via react-hook-form's `register('<name>')`, which sets a stable\n * `name` attribute on the underlying `<input>`. The labels themselves\n * are `t('security.changePassword.*')` calls — when the i18n bundle\n * hasn't finished loading (or isn't loaded at all on this route), MUI\n * renders the literal i18n key as the label, so any selector that\n * matches on visible label text silently misses every field and the\n * form stays empty (which would also make the mismatch test \"pass\"\n * for the wrong reason: the submit button is disabled because the\n * form is empty, not because yup rejected the mismatch).\n *\n * The stable, i18n-independent contract from ChangePassword.tsx is:\n * register('username') → input[name=\"username\"]\n * register('oldPassword') → input[name=\"oldPassword\"]\n * register('newPassword') → input[name=\"newPassword\"]\n * register('confirmPassword') → input[name=\"confirmPassword\"]\n * submit button → form button[type=\"submit\"]\n */\nexport function changePasswordTests(config: RegistryConfig) {\n const { header, login } = config.testIds;\n const { loginDialog } = config.selectors;\n const { features } = config;\n\n // The wrongOldPassword banner text differs across verdaccio lines: older\n // published ui-theme shows a hardcoded \"Failed to change password\", while\n // current master surfaces the server message (\"internal server error\") or\n // the i18n fallback (\"Unable to change password\") via authErrorMessage. The\n // contract this test pins is \"a non-empty error banner appears, not\n // silence\", so it asserts visibility + non-empty text rather than an exact\n // string. The Express \"Cannot PUT\" 404 body is the one text that must never\n // appear — that means the reset_password route was not registered (the\n // server was started without flags.changePassword).\n\n // the form asks for the username only on released UIs; 9.x takes it from the session\n const typeUsernameIfPresent = (user: string) => {\n cy.get('input[name=\"oldPassword\"]').should('exist');\n cy.get('form').then(($form) => {\n const $username = $form.find('input[name=\"username\"]');\n if ($username.length) cy.wrap($username).type(user);\n });\n };\n\n describe('change password', () => {\n const CHANGE_PASSWORD_PATH = '/-/web/change-password';\n const { user, password } = config.credentials;\n\n /**\n * Tests mutate the user's password. We track the \"current\" value\n * across tests so the `after()` hook can restore the original,\n * leaving the registry in the same state other suites assume.\n */\n let currentPassword = password;\n\n /**\n * Capability check. The ChangePassword page's `useEffect` redirects\n * to `/` whenever `configuration.flags.changePassword` is not truthy\n * — which is the case on any registry that either (a) didn't set\n * `flags.changePassword: true` in its config, or (b) runs a\n * verdaccio build whose middleware doesn't yet propagate the flag\n * into `__VERDACCIO_BASENAME_UI_OPTIONS` (older `verdaccio:6`\n * tagged images fall in this bucket).\n *\n * In either case the suite has nothing to exercise, so skip the\n * whole describe with a clear reason rather than burning five\n * seconds per test on cy.contains timeouts.\n */\n before(function () {\n cy.visit(config.registryUrl);\n cy.window().then((win) => {\n const opts = (win as any).__VERDACCIO_BASENAME_UI_OPTIONS;\n const enabled = !!opts?.flags?.changePassword;\n if (!enabled) {\n // eslint-disable-next-line no-console\n console.warn(\n '[change-password] server did not advertise flags.changePassword=true ' +\n '— skipping suite. ui-options.flags: ' +\n JSON.stringify(opts?.flags ?? {})\n );\n this.skip();\n }\n });\n });\n\n beforeEach(() => {\n // Intercept the login POST and wait on it explicitly instead of\n // leaning on a visual sentinel — mirrors the pattern used by\n // signinTests and avoids a race between cy.login's fire-and-forget\n // submit and the next cy.visit.\n cy.intercept('POST', '/-/verdaccio/sec/login').as('signChangePwd');\n cy.visit(config.registryUrl);\n cy.login(user, currentPassword, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@signChangePwd').its('response.statusCode').should('eq', 200);\n\n cy.visit(CHANGE_PASSWORD_PATH);\n // If flags.changePassword is off server-side, the component's\n // useEffect redirects to `/` and this assertion times out — which\n // is the correct signal that the registry is misconfigured.\n // Use the stable `type=\"submit\"` selector so the assertion is\n // independent of whether the i18n bundle has resolved by now.\n cy.get('form button[type=\"submit\"]', { timeout: 5000 }).should('be.visible');\n });\n\n after(() => {\n // Restore the original password so subsequent spec files\n // (and retries) can still log in with `config.credentials`.\n if (currentPassword === password) return;\n cy.intercept('POST', '/-/verdaccio/sec/login').as('signChangePwdRestore');\n cy.visit(config.registryUrl);\n cy.login(user, currentPassword, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@signChangePwdRestore').its('response.statusCode').should('eq', 200);\n cy.visit(CHANGE_PASSWORD_PATH);\n typeUsernameIfPresent(user);\n cy.get('input[name=\"oldPassword\"]').type(currentPassword);\n cy.get('input[name=\"newPassword\"]').type(password);\n cy.get('input[name=\"confirmPassword\"]').type(password);\n cy.get('form button[type=\"submit\"]').click();\n currentPassword = password;\n });\n\n // ── Validation (client-side yup) ─────────────────────────────\n\n maybeIt(features.changePassword.validation)(\n 'should disable the submit button while the form is empty',\n () => {\n cy.get('form button[type=\"submit\"]').should('be.disabled');\n }\n );\n\n maybeIt(features.changePassword.validation)(\n 'should keep submit disabled when new and confirm passwords mismatch',\n () => {\n typeUsernameIfPresent(user);\n cy.get('input[name=\"oldPassword\"]').type(currentPassword);\n cy.get('input[name=\"newPassword\"]').type('newSecretPass123');\n cy.get('input[name=\"confirmPassword\"]').type('different-value');\n // yup schema rejects mismatch → isValid stays false → button disabled.\n cy.get('form button[type=\"submit\"]').should('be.disabled');\n }\n );\n\n // ── Server error path ────────────────────────────────────────\n\n maybeIt(features.changePassword.wrongOldPassword)(\n 'should show an error when the old password is wrong',\n () => {\n cy.intercept('PUT', '/-/verdaccio/sec/reset_password').as('reset');\n typeUsernameIfPresent(user);\n cy.get('input[name=\"oldPassword\"]').type('definitely-wrong-xyz');\n cy.get('input[name=\"newPassword\"]').type('newSecretPass123');\n cy.get('input[name=\"confirmPassword\"]').type('newSecretPass123');\n cy.get('form button[type=\"submit\"]').should('not.be.disabled').click();\n // Server rejects the wrong password (the route exists → not a 404).\n cy.wait('@reset').its('response.statusCode').should('not.eq', 200);\n // onSubmit's catch sets errors.root → rendered via LoginDialogFormError.\n // Assert a non-empty banner (not silence), text-agnostic across lines,\n // but never the raw \"Cannot PUT\" 404 (route missing).\n // released UIs show a banner; 9.x flags the old password field instead\n cy.get('form', { timeout: 5000 }).should(($form) => {\n const $banner = $form.find(`[data-testid=\"${login.error}\"]`);\n const $invalidField = $form.find('input[name=\"oldPassword\"][aria-invalid=\"true\"]');\n expect($banner.length + $invalidField.length, 'error shown').to.be.greaterThan(0);\n const message = $banner.length\n ? $banner.text()\n : $invalidField.closest('.MuiFormControl-root').find('.MuiFormHelperText-root').text();\n expect(message.trim()).to.not.be.empty;\n expect(message).to.not.contain('Cannot PUT');\n });\n // Still on the change-password page so the user can retry.\n cy.location('pathname').should('include', CHANGE_PASSWORD_PATH);\n }\n );\n\n // ── Happy path (mutates state; keeps `currentPassword` in sync) ─\n\n maybeIt(features.changePassword.happyPath)(\n 'should change the password and navigate to the success page',\n () => {\n const newPassword = `${currentPassword}-rotated`;\n cy.intercept('PUT', '/-/verdaccio/sec/reset_password').as('reset');\n\n typeUsernameIfPresent(user);\n cy.get('input[name=\"oldPassword\"]').type(currentPassword);\n cy.get('input[name=\"newPassword\"]').type(newPassword);\n cy.get('input[name=\"confirmPassword\"]').type(newPassword);\n cy.get('form button[type=\"submit\"]').should('not.be.disabled').click();\n\n cy.wait('@reset').its('response.statusCode').should('eq', 200);\n // Post-submit the component navigates to Route.SUCCESS with a\n // messageType query param. Assert the pathname; the message text\n // is i18n-driven and out of scope for this selector layer.\n cy.location('pathname', { timeout: 5000 }).should('include', '/-/web/success');\n\n // Track the rotation so `after()` can restore it.\n currentPassword = newPassword;\n }\n );\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,166 @@
1
+ import { maybeIt } from "../features.js";
2
+ //#region src/tests/home-detail-ux.ts
3
+ /**
4
+ * Home and package page UX that ships with the 9.x web UI: the package
5
+ * list toolbar, the tab in the address, the welcome guide, the install
6
+ * box (bun and `.npmrc`), copy feedback and search grouped by origin.
7
+ *
8
+ * Every test is a probe (flag defaults to `false`): released lines do
9
+ * not have these features yet.
10
+ */
11
+ function homeDetailUxTests(config) {
12
+ const { features } = config;
13
+ const flags = features.homeDetailUx;
14
+ const { header, home, package: pkg, search } = config.testIds;
15
+ describe("home and detail ux", () => {
16
+ describe("with an empty registry", () => {
17
+ beforeEach(() => {
18
+ cy.intercept("GET", "/-/verdaccio/data/packages").as("pkgs");
19
+ cy.visit(config.registryUrl);
20
+ cy.wait("@pkgs");
21
+ });
22
+ maybeIt(flags.welcomeGuide)("should guide the first publish with a setup tab per package manager", () => {
23
+ cy.getByTestId(home.helpCard, { timeout: 1e4 }).should("be.visible");
24
+ cy.getByTestId(home.helpCard).contains("No Package Published Yet.");
25
+ cy.getByTestId(home.helpCard).contains(`npm adduser --registry ${config.registryUrl}`);
26
+ cy.getByTestId(home.helpCard).contains(`npm publish --registry ${config.registryUrl}`);
27
+ cy.getByTestId(`${home.helpSetupTabPrefix}npm`).should("be.visible");
28
+ cy.getByTestId(home.helpCard).contains(`npm config set registry ${config.registryUrl}`);
29
+ cy.get(`[data-testid="${home.helpSetupTabPrefix}.npmrc"]`).click();
30
+ cy.getByTestId(home.helpCard).contains(`registry=${config.registryUrl}`);
31
+ cy.getByTestId(home.helpUplinksNote).should("be.visible");
32
+ });
33
+ });
34
+ describe("with packages from two scopes", () => {
35
+ const scopedPkg = "@verdaccio/ux-toolbar-fixture";
36
+ const unscopedPkg = "ux-toolbar-unscoped-fixture";
37
+ const tempFolders = [];
38
+ before(() => {
39
+ if (!flags.homeToolbar) return;
40
+ [scopedPkg, unscopedPkg].forEach((pkgName) => {
41
+ cy.task("publishPackage", {
42
+ pkgName,
43
+ version: "1.0.0",
44
+ unique: true
45
+ }).then((r) => {
46
+ if (r?.tempFolder) tempFolders.push(r.tempFolder);
47
+ });
48
+ });
49
+ });
50
+ after(() => {
51
+ if (!flags.homeToolbar) return;
52
+ [scopedPkg, unscopedPkg].forEach((pkgName) => cy.task("unpublishPackage", pkgName));
53
+ cy.then(() => tempFolders.forEach((folder) => cy.task("cleanupPublished", folder)));
54
+ });
55
+ beforeEach(() => {
56
+ cy.intercept("GET", "/-/verdaccio/data/packages").as("pkgs");
57
+ });
58
+ maybeIt(flags.homeToolbar)("should filter the list and keep the filter in ?q=", () => {
59
+ cy.visit(config.registryUrl);
60
+ cy.wait("@pkgs");
61
+ cy.getByTestId(home.packageListToolbar, { timeout: 1e4 }).should("be.visible");
62
+ cy.getByTestId(pkg.title).should("have.length", 2);
63
+ cy.getByTestId(home.packageListFilter).type("unscoped", { delay: 20 });
64
+ cy.getByTestId(pkg.title).should("have.length", 1).and("contain", unscopedPkg);
65
+ cy.location("search").should("eq", "?q=unscoped");
66
+ cy.reload();
67
+ cy.getByTestId(home.packageListFilter, { timeout: 1e4 }).should("have.value", "unscoped");
68
+ cy.getByTestId(pkg.title).should("have.length", 1);
69
+ });
70
+ maybeIt(flags.homeToolbar)("should restore the scope from ?scope=", () => {
71
+ cy.visit(`${config.registryUrl}/?scope=@verdaccio`);
72
+ cy.wait("@pkgs");
73
+ cy.getByTestId(home.packageListScope, { timeout: 1e4 }).should("have.value", "@verdaccio");
74
+ cy.getByTestId(pkg.title).should("have.length", 1).and("contain", scopedPkg);
75
+ });
76
+ maybeIt(flags.homeToolbar)("should say nothing matches instead of showing the onboarding card", () => {
77
+ cy.visit(`${config.registryUrl}/?q=no-package-matches-this`);
78
+ cy.wait("@pkgs");
79
+ cy.getByTestId(home.packageListNoMatch, { timeout: 1e4 }).should("be.visible");
80
+ cy.getByTestId(home.helpCard).should("not.exist");
81
+ });
82
+ });
83
+ describe("with a published package", () => {
84
+ const pkgName = "@verdaccio/ux-detail-fixture";
85
+ const detailUrl = `${config.registryUrl}/-/web/detail/${pkgName}`;
86
+ const needsPackage = flags.detailTabInUrl || flags.installBunAndNpmrc || flags.copyFeedback || flags.searchGrouping || flags.versionLink || flags.settingsPublishTab;
87
+ let tempFolder = null;
88
+ let version = "1.0.0";
89
+ before(() => {
90
+ if (!needsPackage) return;
91
+ cy.task("publishPackage", {
92
+ pkgName,
93
+ version: "1.0.0",
94
+ dependencies: { debug: "4.0.0" },
95
+ unique: true
96
+ }).then((result) => {
97
+ tempFolder = result?.tempFolder ?? null;
98
+ version = result?.version ?? version;
99
+ });
100
+ });
101
+ after(() => {
102
+ if (!needsPackage) return;
103
+ cy.task("unpublishPackage", {
104
+ pkgName,
105
+ tempFolder: tempFolder ?? void 0
106
+ });
107
+ cy.then(() => {
108
+ if (tempFolder) cy.task("cleanupPublished", tempFolder);
109
+ });
110
+ });
111
+ maybeIt(flags.settingsPublishTab)("should keep the publishing guide in the settings once packages exist", () => {
112
+ cy.visit(config.registryUrl);
113
+ cy.getByTestId(pkg.itemList, { timeout: 1e4 }).should("exist");
114
+ cy.getByTestId(header.settingsTooltip).click();
115
+ cy.getByTestId(header.settingsPublishTab).click();
116
+ cy.getByTestId(header.settingsPublishPanel).within(() => {
117
+ cy.getByTestId(home.helpGuide).should("be.visible");
118
+ cy.contains(`npm publish --registry ${config.registryUrl}`).scrollIntoView().should("be.visible");
119
+ });
120
+ });
121
+ maybeIt(flags.versionLink)("should open the version page from the home list", () => {
122
+ cy.visit(config.registryUrl);
123
+ cy.contains(`[data-testid="${pkg.itemList}"]`, pkgName, { timeout: 1e4 }).find(`[data-testid="${pkg.versionLink}"]`).click();
124
+ cy.location("pathname").should("eq", `/-/web/detail/${pkgName}/v/${version}`);
125
+ cy.getByTestId(pkg.sidebar, { timeout: 1e4 }).contains(version);
126
+ });
127
+ maybeIt(flags.detailTabInUrl)("should keep the selected tab in ?tab=", () => {
128
+ cy.visit(detailUrl);
129
+ cy.getByTestId(pkg.versionsTab, { timeout: 1e4 }).click();
130
+ cy.location("search").should("contain", "tab=versions");
131
+ cy.getByTestId(pkg.tagLatest).should("be.visible");
132
+ cy.getByTestId(pkg.readmeTab).click();
133
+ cy.location("search").should("not.contain", "tab=");
134
+ });
135
+ maybeIt(flags.detailTabInUrl)("should open the tab a link points to", () => {
136
+ cy.visit(`${detailUrl}?tab=dependencies`);
137
+ cy.getByTestId(pkg.dependenciesTab, { timeout: 1e4 }).should("have.attr", "aria-selected", "true");
138
+ cy.getByTestId(pkg.dependencies).contains("debug");
139
+ });
140
+ maybeIt(flags.installBunAndNpmrc)("should offer bun and the scoped .npmrc line in the install box", () => {
141
+ cy.visit(detailUrl);
142
+ cy.getByTestId(pkg.installBun, { timeout: 1e4 }).contains(`bun add ${pkgName}`);
143
+ cy.getByTestId(pkg.installNpmrc).contains(`@verdaccio:registry=${config.registryUrl}`);
144
+ });
145
+ maybeIt(flags.copyFeedback)("should confirm a copy on the copy button", () => {
146
+ cy.visit(detailUrl);
147
+ cy.getByTestId(pkg.installNpmrcCopy, { timeout: 1e4 }).click();
148
+ cy.getByTestId(pkg.installNpmrcCopy).should("have.attr", "aria-label", "Copied");
149
+ });
150
+ maybeIt(flags.searchGrouping)("should list a package published here under its own search group", () => {
151
+ cy.intercept("GET", "**/-/verdaccio/data/search/**").as("webSearch");
152
+ cy.visit(config.registryUrl);
153
+ cy.get("input[placeholder*=\"earch\"]", { timeout: 1e4 }).type("ux-detail-fixture", { delay: 30 });
154
+ cy.wait("@webSearch", { timeout: 1e4 });
155
+ cy.get("[role=\"listbox\"]", { timeout: 5e3 }).within(() => {
156
+ cy.getByTestId(`${search.groupPrefix}private`).should("be.visible");
157
+ cy.contains("[role=\"option\"]", pkgName).find(`[data-testid="${search.originPrefix}private"]`).should("exist");
158
+ });
159
+ });
160
+ });
161
+ });
162
+ }
163
+ //#endregion
164
+ export { homeDetailUxTests };
165
+
166
+ //# sourceMappingURL=home-detail-ux.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"home-detail-ux.js","names":[],"sources":["../../../src/tests/home-detail-ux.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Home and package page UX that ships with the 9.x web UI: the package\n * list toolbar, the tab in the address, the welcome guide, the install\n * box (bun and `.npmrc`), copy feedback and search grouped by origin.\n *\n * Every test is a probe (flag defaults to `false`): released lines do\n * not have these features yet.\n */\nexport function homeDetailUxTests(config: RegistryConfig) {\n const { features } = config;\n const flags = features.homeDetailUx;\n const { header, home, package: pkg, search } = config.testIds;\n\n describe('home and detail ux', () => {\n describe('with an empty registry', () => {\n beforeEach(() => {\n cy.intercept('GET', '/-/verdaccio/data/packages').as('pkgs');\n cy.visit(config.registryUrl);\n cy.wait('@pkgs');\n });\n\n maybeIt(flags.welcomeGuide)(\n 'should guide the first publish with a setup tab per package manager',\n () => {\n cy.getByTestId(home.helpCard, { timeout: 10000 }).should('be.visible');\n // the e2e contract of the old card still holds\n cy.getByTestId(home.helpCard).contains('No Package Published Yet.');\n cy.getByTestId(home.helpCard).contains(`npm adduser --registry ${config.registryUrl}`);\n cy.getByTestId(home.helpCard).contains(`npm publish --registry ${config.registryUrl}`);\n\n cy.getByTestId(`${home.helpSetupTabPrefix}npm`).should('be.visible');\n cy.getByTestId(home.helpCard).contains(`npm config set registry ${config.registryUrl}`);\n // quoted: getByTestId does not escape the dot in `.npmrc`\n cy.get(`[data-testid=\"${home.helpSetupTabPrefix}.npmrc\"]`).click();\n cy.getByTestId(home.helpCard).contains(`registry=${config.registryUrl}`);\n cy.getByTestId(home.helpUplinksNote).should('be.visible');\n }\n );\n });\n\n describe('with packages from two scopes', () => {\n const scopedPkg = '@verdaccio/ux-toolbar-fixture';\n const unscopedPkg = 'ux-toolbar-unscoped-fixture';\n const tempFolders: string[] = [];\n\n before(() => {\n if (!flags.homeToolbar) return;\n [scopedPkg, unscopedPkg].forEach((pkgName) => {\n cy.task('publishPackage', { pkgName, version: '1.0.0', unique: true }).then((r) => {\n if (r?.tempFolder) tempFolders.push(r.tempFolder);\n });\n });\n });\n\n after(() => {\n if (!flags.homeToolbar) return;\n [scopedPkg, unscopedPkg].forEach((pkgName) => cy.task('unpublishPackage', pkgName));\n cy.then(() => tempFolders.forEach((folder) => cy.task('cleanupPublished', folder)));\n });\n\n beforeEach(() => {\n cy.intercept('GET', '/-/verdaccio/data/packages').as('pkgs');\n });\n\n maybeIt(flags.homeToolbar)('should filter the list and keep the filter in ?q=', () => {\n cy.visit(config.registryUrl);\n cy.wait('@pkgs');\n cy.getByTestId(home.packageListToolbar, { timeout: 10000 }).should('be.visible');\n cy.getByTestId(pkg.title).should('have.length', 2);\n\n cy.getByTestId(home.packageListFilter).type('unscoped', { delay: 20 });\n cy.getByTestId(pkg.title).should('have.length', 1).and('contain', unscopedPkg);\n cy.location('search').should('eq', '?q=unscoped');\n\n // a reload keeps the filter: it lives in the address\n cy.reload();\n cy.getByTestId(home.packageListFilter, { timeout: 10000 }).should('have.value', 'unscoped');\n cy.getByTestId(pkg.title).should('have.length', 1);\n });\n\n maybeIt(flags.homeToolbar)('should restore the scope from ?scope=', () => {\n cy.visit(`${config.registryUrl}/?scope=@verdaccio`);\n cy.wait('@pkgs');\n cy.getByTestId(home.packageListScope, { timeout: 10000 }).should(\n 'have.value',\n '@verdaccio'\n );\n cy.getByTestId(pkg.title).should('have.length', 1).and('contain', scopedPkg);\n });\n\n maybeIt(flags.homeToolbar)(\n 'should say nothing matches instead of showing the onboarding card',\n () => {\n cy.visit(`${config.registryUrl}/?q=no-package-matches-this`);\n cy.wait('@pkgs');\n cy.getByTestId(home.packageListNoMatch, { timeout: 10000 }).should('be.visible');\n cy.getByTestId(home.helpCard).should('not.exist');\n }\n );\n });\n\n describe('with a published package', () => {\n const pkgName = '@verdaccio/ux-detail-fixture';\n const detailUrl = `${config.registryUrl}/-/web/detail/${pkgName}`;\n const needsPackage =\n flags.detailTabInUrl ||\n flags.installBunAndNpmrc ||\n flags.copyFeedback ||\n flags.searchGrouping ||\n flags.versionLink ||\n flags.settingsPublishTab;\n let tempFolder: string | null = null;\n let version = '1.0.0';\n\n before(() => {\n if (!needsPackage) return;\n cy.task('publishPackage', {\n pkgName,\n version: '1.0.0',\n dependencies: { debug: '4.0.0' },\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n // `unique: true` makes the version 1.0.0-t<timestamp>\n version = result?.version ?? version;\n });\n });\n\n after(() => {\n if (!needsPackage) return;\n cy.task('unpublishPackage', { pkgName, tempFolder: tempFolder ?? undefined });\n cy.then(() => {\n if (tempFolder) cy.task('cleanupPublished', tempFolder);\n });\n });\n\n maybeIt(flags.settingsPublishTab)(\n 'should keep the publishing guide in the settings once packages exist',\n () => {\n cy.visit(config.registryUrl);\n cy.getByTestId(pkg.itemList, { timeout: 10000 }).should('exist');\n cy.getByTestId(header.settingsTooltip).click();\n cy.getByTestId(header.settingsPublishTab).click();\n cy.getByTestId(header.settingsPublishPanel).within(() => {\n cy.getByTestId(home.helpGuide).should('be.visible');\n // the dialog scrolls; at the default viewport the command sits below the fold\n cy.contains(`npm publish --registry ${config.registryUrl}`)\n .scrollIntoView()\n .should('be.visible');\n });\n }\n );\n\n maybeIt(flags.versionLink)('should open the version page from the home list', () => {\n cy.visit(config.registryUrl);\n cy.contains(`[data-testid=\"${pkg.itemList}\"]`, pkgName, { timeout: 10000 })\n .find(`[data-testid=\"${pkg.versionLink}\"]`)\n .click();\n // scoped names keep the literal slash in the path\n cy.location('pathname').should('eq', `/-/web/detail/${pkgName}/v/${version}`);\n cy.getByTestId(pkg.sidebar, { timeout: 10000 }).contains(version);\n });\n\n maybeIt(flags.detailTabInUrl)('should keep the selected tab in ?tab=', () => {\n cy.visit(detailUrl);\n cy.getByTestId(pkg.versionsTab, { timeout: 10000 }).click();\n cy.location('search').should('contain', 'tab=versions');\n cy.getByTestId(pkg.tagLatest).should('be.visible');\n\n cy.getByTestId(pkg.readmeTab).click();\n cy.location('search').should('not.contain', 'tab=');\n });\n\n maybeIt(flags.detailTabInUrl)('should open the tab a link points to', () => {\n cy.visit(`${detailUrl}?tab=dependencies`);\n cy.getByTestId(pkg.dependenciesTab, { timeout: 10000 }).should(\n 'have.attr',\n 'aria-selected',\n 'true'\n );\n cy.getByTestId(pkg.dependencies).contains('debug');\n });\n\n maybeIt(flags.installBunAndNpmrc)(\n 'should offer bun and the scoped .npmrc line in the install box',\n () => {\n cy.visit(detailUrl);\n cy.getByTestId(pkg.installBun, { timeout: 10000 }).contains(`bun add ${pkgName}`);\n cy.getByTestId(pkg.installNpmrc).contains(`@verdaccio:registry=${config.registryUrl}`);\n }\n );\n\n maybeIt(flags.copyFeedback)('should confirm a copy on the copy button', () => {\n cy.visit(detailUrl);\n cy.getByTestId(pkg.installNpmrcCopy, { timeout: 10000 }).click();\n cy.getByTestId(pkg.installNpmrcCopy).should('have.attr', 'aria-label', 'Copied');\n });\n\n maybeIt(flags.searchGrouping)(\n 'should list a package published here under its own search group',\n () => {\n cy.intercept('GET', '**/-/verdaccio/data/search/**').as('webSearch');\n cy.visit(config.registryUrl);\n cy.get('input[placeholder*=\"earch\"]', { timeout: 10000 }).type('ux-detail-fixture', {\n delay: 30,\n });\n cy.wait('@webSearch', { timeout: 10000 });\n cy.get('[role=\"listbox\"]', { timeout: 5000 }).within(() => {\n cy.getByTestId(`${search.groupPrefix}private`).should('be.visible');\n cy.contains('[role=\"option\"]', pkgName)\n .find(`[data-testid=\"${search.originPrefix}private\"]`)\n .should('exist');\n });\n }\n );\n });\n });\n}\n"],"mappings":"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"}
@@ -11,4 +11,5 @@ import { i18nKeyTests } from "./i18n-keys.js";
11
11
  import { signupTests } from "./signup.js";
12
12
  import { manifestRenderingTests } from "./manifest-rendering.js";
13
13
  import { sessionTests } from "./session.js";
14
- export { changePasswordTests, detailTests, failureModeTests, homeTests, i18nKeyTests, layoutTests, manifestRenderingTests, publishTests, searchTests, sessionTests, settingsTests, signinTests, signupTests };
14
+ import { homeDetailUxTests } from "./home-detail-ux.js";
15
+ export { changePasswordTests, detailTests, failureModeTests, homeDetailUxTests, homeTests, i18nKeyTests, layoutTests, manifestRenderingTests, publishTests, searchTests, sessionTests, settingsTests, signinTests, signupTests };
@@ -91,7 +91,7 @@ function publishTests(config) {
91
91
  cy.wait("@pkgs");
92
92
  cy.getByTestId(pkg.title).first().click();
93
93
  cy.wait("@sidebar");
94
- cy.getByTestId(pkg.keywordList).should("be.visible");
94
+ cy.getByTestId(pkg.keywordList).scrollIntoView().should("be.visible");
95
95
  cy.getByTestId(pkg.keywordList).should("contain.text", "verdaccio");
96
96
  cy.getByTestId(pkg.keywordList).should("contain.text", "e2e");
97
97
  cy.getByTestId(pkg.keywordList).should("contain.text", "test");
@@ -1 +1 @@
1
- {"version":3,"file":"publish.js","names":[],"sources":["../../../src/tests/publish.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\nexport function publishTests(config: RegistryConfig) {\n const { header, package: pkg } = config.testIds;\n const { markdownBody, loginDialog } = config.selectors;\n const { features } = config;\n /**\n * Log in once, reuse the session across tests in the requested suite.\n * `cy.session` caches cookies + localStorage keyed on the first\n * argument, so subsequent calls restore without hitting the network.\n */\n const loginOnce = (sessionName: string) => {\n cy.session(\n [sessionName, config.credentials.user],\n () => {\n cy.visit(config.registryUrl);\n cy.login(config.credentials.user, config.credentials.password, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@sign');\n },\n {\n validate() {\n cy.request({\n url: `${config.registryUrl}/-/verdaccio/data/packages`,\n failOnStatusCode: false,\n })\n .its('status')\n .should('be.oneOf', [200, 304]);\n },\n cacheAcrossSpecs: true,\n }\n );\n };\n\n describe('publish', () => {\n const pkgName = '@verdaccio/pkg-scoped';\n // Single source of truth for the dependency the publish fixture\n // writes into its package.json. The dependencies-tab assertion\n // reads these back to verify the UI rendered both fields.\n const depName = 'debug';\n const depVersion = '4.0.0';\n // Per-test state so afterEach can clean up the specific publish\n // that this test created (temp folder + registry entry).\n let tempFolder: string | null = null;\n\n beforeEach(() => {\n cy.intercept('POST', '/-/verdaccio/sec/login').as('sign');\n cy.intercept('GET', '/-/verdaccio/data/packages').as('pkgs');\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}`).as('sidebar');\n cy.intercept('GET', `/-/verdaccio/data/package/readme/${pkgName}`).as('readme');\n\n // Publish a fresh copy for this test. `unique: true` appends a\n // timestamp suffix so the version is distinct per test even when\n // the registry briefly has a stale copy from a prior afterEach.\n cy.task('publishPackage', {\n pkgName,\n version: '1.0.0',\n dependencies: { [depName]: depVersion },\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n });\n\n loginOnce('publish-suite');\n cy.visit(config.registryUrl);\n });\n\n afterEach(() => {\n // Remove the package from the registry AND the local temp folder.\n // Run both regardless of test outcome so the next test starts\n // clean. `unpublishPackage` treats 404 as success, so a re-run\n // after a half-published state is still safe.\n cy.task('unpublishPackage', { pkgName, tempFolder: tempFolder ?? undefined });\n if (tempFolder) {\n cy.task('cleanupPublished', tempFolder);\n }\n tempFolder = null;\n });\n\n it('should have one published package', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).should('have.length.at.least', 1);\n });\n\n it('should navigate to page detail', () => {\n cy.wait('@pkgs');\n cy.wait(300);\n cy.getByTestId(pkg.title).first().click();\n });\n\n it('should have readme content', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@readme');\n cy.wait('@sidebar');\n cy.getByTestId(pkg.readme).should('be.visible');\n cy.get(markdownBody).should('have.length', 1);\n // publishPackage writes a README whose body contains \"e2e testing\".\n cy.contains(markdownBody, /test/);\n cy.contains(`${markdownBody} h1`, pkgName).should('be.visible');\n });\n\n it('should render the sidebar with install commands for npm, yarn, pnpm', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@sidebar');\n cy.getByTestId(pkg.sidebar).should('be.visible');\n cy.getByTestId(pkg.installList).within(() => {\n cy.getByTestId(pkg.installNpm).should('be.visible');\n cy.getByTestId(pkg.installYarn).should('be.visible');\n cy.getByTestId(pkg.installPnpm).should('be.visible');\n });\n cy.getByTestId(pkg.installNpm).should('contain.text', pkgName);\n });\n\n it('should render the sidebar keywords from the published manifest', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@sidebar');\n // publishPackage writes `keywords: ['verdaccio', 'e2e', 'test']`\n // into the generated package.json.\n cy.getByTestId(pkg.keywordList).should('be.visible');\n cy.getByTestId(pkg.keywordList).should('contain.text', 'verdaccio');\n cy.getByTestId(pkg.keywordList).should('contain.text', 'e2e');\n cy.getByTestId(pkg.keywordList).should('contain.text', 'test');\n });\n\n it('should click on dependencies tab', () => {\n cy.wait('@pkgs');\n cy.wait(300);\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@readme');\n cy.wait('@sidebar');\n cy.getByTestId(pkg.dependenciesTab).click();\n cy.wait(100);\n cy.getByTestId(pkg.dependencies).should('have.length', 1);\n\n // The dep Chip uses the dep name as its data-testid (dynamic\n // Verdaccio convention, see DependencyBlock.tsx:68), and its\n // label is `\"${name}: ${version}\"` via the `dependencies.\n // dependency-block` i18n key. Assert BOTH fields are rendered.\n cy.getByTestId(depName)\n .should('be.visible')\n .and('contain.text', depName)\n .and('contain.text', depVersion);\n\n // Also verify the Chip text matches the exact \"name: version\"\n // format so a regression in the label template would fail here\n // rather than pass via a loose substring match.\n cy.getByTestId(depName)\n .invoke('text')\n .should('match', new RegExp(`${depName}\\\\s*:\\\\s*${depVersion}`));\n });\n\n it('should click on versions tab', () => {\n cy.wait('@pkgs');\n cy.wait(300);\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@readme');\n cy.wait('@sidebar');\n cy.getByTestId(pkg.versionsTab).click();\n // With `unique: true` the version becomes `1.0.0-t<timestamp>`,\n // but \"1.0.0\" still appears as a substring — match loosely.\n cy.getByTestId(pkg.tagLatest)\n .children()\n .invoke('text')\n .should('match', /1\\.0\\.0/);\n });\n\n it('should click on uplinks tab', () => {\n cy.wait('@pkgs');\n cy.wait(300);\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@readme');\n cy.wait('@sidebar');\n cy.getByTestId(pkg.uplinksTab).click();\n cy.getByTestId(pkg.noUplinks).should('be.visible');\n });\n\n // ── Action-bar FABs: tarball download + raw viewer ─────────────\n // Both buttons live in the sidebar ActionBar. They're gated on\n // `web.showDownloadTarball` / `web.showRaw` (both default to true\n // in the ui-theme's AppConfigurationProvider) and each test is\n // also guarded by a feature flag so branches that ship a\n // different action bar can skip cleanly.\n\n maybeIt(features.publish.downloadTarball)(\n 'should fetch the tarball when the download button is clicked',\n () => {\n // The download provider hits the package manifest's dist.tarball\n // URL directly. For our published fixture the filename looks\n // like `pkg-scoped-1.0.0-t<ts>.tgz`, served from\n // `/<pkg>/-/<filename>.tgz`. Intercept before clicking.\n cy.intercept('GET', '**/pkg-scoped-*.tgz').as('tarballFetch');\n\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@sidebar');\n\n cy.getByTestId(pkg.downloadTarballBtn).should('be.visible').click();\n\n // The fetch should fire and return 200. We can't assert on the\n // actual file landing on disk — Cypress doesn't track OS-level\n // downloads — but a successful GET proves the end-to-end path\n // from click → download provider → registry.\n cy.wait('@tarballFetch', { timeout: 10000 }).its('response.statusCode').should('eq', 200);\n }\n );\n\n maybeIt(features.publish.rawViewer)(\n 'should open the raw manifest viewer when the raw button is clicked',\n () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@sidebar');\n\n // RawViewer is a full-screen MUI Dialog — initially unmounted\n // because `isOpen=false` keeps Dialog closed and Cypress won't\n // find it. Clicking the FAB flips `isOpen` to true.\n cy.getByTestId(pkg.rawBtn).should('be.visible').click();\n\n cy.getByTestId(pkg.rawViewerDialog, { timeout: 5000 }).should('be.visible');\n // The ReactJson viewer renders the package manifest — the\n // package name should appear somewhere in the serialized JSON.\n cy.getByTestId(pkg.rawViewerDialog).should('contain.text', pkgName);\n\n // Close via the X button and confirm the dialog goes away so\n // subsequent tests don't inherit an open overlay.\n cy.getByTestId(pkg.closeRawViewer).click();\n cy.getByTestId(pkg.rawViewerDialog).should('not.exist');\n }\n );\n });\n\n describe('private package tarball downloads', () => {\n const privatePkgName = '@private/tarball-fixture';\n let tempFolder: string | null = null;\n\n beforeEach(() => {\n cy.intercept('POST', '/-/verdaccio/sec/login').as('sign');\n cy.intercept('GET', '/-/verdaccio/data/packages').as('pkgs');\n cy.intercept('GET', '**/-/verdaccio/data/sidebar/@private/tarball-fixture*').as('sidebar');\n\n cy.task('publishPackage', {\n pkgName: privatePkgName,\n version: '1.0.0',\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n });\n\n loginOnce('private-tarball-suite');\n cy.visit(config.registryUrl);\n });\n\n afterEach(() => {\n cy.task('unpublishPackage', {\n pkgName: privatePkgName,\n tempFolder: tempFolder ?? undefined,\n });\n if (tempFolder) {\n cy.task('cleanupPublished', tempFolder);\n }\n tempFolder = null;\n });\n\n maybeIt(features.publish.privateDownloadTarball)(\n 'should fetch a private tarball from the home package list',\n () => {\n cy.intercept('GET', '**/tarball-fixture-*.tgz').as('privateTarballFetch');\n\n cy.wait('@pkgs');\n cy.contains(`[data-testid=\"${pkg.title}\"]`, privatePkgName)\n .should('be.visible')\n .closest(`[data-testid=\"${pkg.itemList}\"]`)\n .find(`[data-testid=\"${pkg.downloadTarball}\"]`)\n .should('be.visible')\n .click();\n\n cy.wait('@privateTarballFetch', { timeout: 10000 })\n .its('response.statusCode')\n .should('eq', 200);\n }\n );\n\n maybeIt(features.publish.privateDownloadTarball)(\n 'should fetch a private tarball from the package sidebar',\n () => {\n cy.intercept('GET', '**/tarball-fixture-*.tgz').as('privateTarballFetch');\n\n cy.wait('@pkgs');\n cy.contains(`[data-testid=\"${pkg.title}\"]`, privatePkgName).click();\n cy.wait('@sidebar');\n cy.getByTestId(pkg.downloadTarballBtn).should('be.visible').click();\n\n cy.wait('@privateTarballFetch', { timeout: 10000 })\n .its('response.statusCode')\n .should('eq', 200);\n }\n );\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"publish.js","names":[],"sources":["../../../src/tests/publish.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\nexport function publishTests(config: RegistryConfig) {\n const { header, package: pkg } = config.testIds;\n const { markdownBody, loginDialog } = config.selectors;\n const { features } = config;\n /**\n * Log in once, reuse the session across tests in the requested suite.\n * `cy.session` caches cookies + localStorage keyed on the first\n * argument, so subsequent calls restore without hitting the network.\n */\n const loginOnce = (sessionName: string) => {\n cy.session(\n [sessionName, config.credentials.user],\n () => {\n cy.visit(config.registryUrl);\n cy.login(config.credentials.user, config.credentials.password, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@sign');\n },\n {\n validate() {\n cy.request({\n url: `${config.registryUrl}/-/verdaccio/data/packages`,\n failOnStatusCode: false,\n })\n .its('status')\n .should('be.oneOf', [200, 304]);\n },\n cacheAcrossSpecs: true,\n }\n );\n };\n\n describe('publish', () => {\n const pkgName = '@verdaccio/pkg-scoped';\n // Single source of truth for the dependency the publish fixture\n // writes into its package.json. The dependencies-tab assertion\n // reads these back to verify the UI rendered both fields.\n const depName = 'debug';\n const depVersion = '4.0.0';\n // Per-test state so afterEach can clean up the specific publish\n // that this test created (temp folder + registry entry).\n let tempFolder: string | null = null;\n\n beforeEach(() => {\n cy.intercept('POST', '/-/verdaccio/sec/login').as('sign');\n cy.intercept('GET', '/-/verdaccio/data/packages').as('pkgs');\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}`).as('sidebar');\n cy.intercept('GET', `/-/verdaccio/data/package/readme/${pkgName}`).as('readme');\n\n // Publish a fresh copy for this test. `unique: true` appends a\n // timestamp suffix so the version is distinct per test even when\n // the registry briefly has a stale copy from a prior afterEach.\n cy.task('publishPackage', {\n pkgName,\n version: '1.0.0',\n dependencies: { [depName]: depVersion },\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n });\n\n loginOnce('publish-suite');\n cy.visit(config.registryUrl);\n });\n\n afterEach(() => {\n // Remove the package from the registry AND the local temp folder.\n // Run both regardless of test outcome so the next test starts\n // clean. `unpublishPackage` treats 404 as success, so a re-run\n // after a half-published state is still safe.\n cy.task('unpublishPackage', { pkgName, tempFolder: tempFolder ?? undefined });\n if (tempFolder) {\n cy.task('cleanupPublished', tempFolder);\n }\n tempFolder = null;\n });\n\n it('should have one published package', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).should('have.length.at.least', 1);\n });\n\n it('should navigate to page detail', () => {\n cy.wait('@pkgs');\n cy.wait(300);\n cy.getByTestId(pkg.title).first().click();\n });\n\n it('should have readme content', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@readme');\n cy.wait('@sidebar');\n cy.getByTestId(pkg.readme).should('be.visible');\n cy.get(markdownBody).should('have.length', 1);\n // publishPackage writes a README whose body contains \"e2e testing\".\n cy.contains(markdownBody, /test/);\n cy.contains(`${markdownBody} h1`, pkgName).should('be.visible');\n });\n\n it('should render the sidebar with install commands for npm, yarn, pnpm', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@sidebar');\n cy.getByTestId(pkg.sidebar).should('be.visible');\n cy.getByTestId(pkg.installList).within(() => {\n cy.getByTestId(pkg.installNpm).should('be.visible');\n cy.getByTestId(pkg.installYarn).should('be.visible');\n cy.getByTestId(pkg.installPnpm).should('be.visible');\n });\n cy.getByTestId(pkg.installNpm).should('contain.text', pkgName);\n });\n\n it('should render the sidebar keywords from the published manifest', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@sidebar');\n // publishPackage writes `keywords: ['verdaccio', 'e2e', 'test']`\n // into the generated package.json.\n // the sidebar scrolls; a taller install box can push the keywords below the fold\n cy.getByTestId(pkg.keywordList).scrollIntoView().should('be.visible');\n cy.getByTestId(pkg.keywordList).should('contain.text', 'verdaccio');\n cy.getByTestId(pkg.keywordList).should('contain.text', 'e2e');\n cy.getByTestId(pkg.keywordList).should('contain.text', 'test');\n });\n\n it('should click on dependencies tab', () => {\n cy.wait('@pkgs');\n cy.wait(300);\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@readme');\n cy.wait('@sidebar');\n cy.getByTestId(pkg.dependenciesTab).click();\n cy.wait(100);\n cy.getByTestId(pkg.dependencies).should('have.length', 1);\n\n // The dep Chip uses the dep name as its data-testid (dynamic\n // Verdaccio convention, see DependencyBlock.tsx:68), and its\n // label is `\"${name}: ${version}\"` via the `dependencies.\n // dependency-block` i18n key. Assert BOTH fields are rendered.\n cy.getByTestId(depName)\n .should('be.visible')\n .and('contain.text', depName)\n .and('contain.text', depVersion);\n\n // Also verify the Chip text matches the exact \"name: version\"\n // format so a regression in the label template would fail here\n // rather than pass via a loose substring match.\n cy.getByTestId(depName)\n .invoke('text')\n .should('match', new RegExp(`${depName}\\\\s*:\\\\s*${depVersion}`));\n });\n\n it('should click on versions tab', () => {\n cy.wait('@pkgs');\n cy.wait(300);\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@readme');\n cy.wait('@sidebar');\n cy.getByTestId(pkg.versionsTab).click();\n // With `unique: true` the version becomes `1.0.0-t<timestamp>`,\n // but \"1.0.0\" still appears as a substring — match loosely.\n cy.getByTestId(pkg.tagLatest)\n .children()\n .invoke('text')\n .should('match', /1\\.0\\.0/);\n });\n\n it('should click on uplinks tab', () => {\n cy.wait('@pkgs');\n cy.wait(300);\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@readme');\n cy.wait('@sidebar');\n cy.getByTestId(pkg.uplinksTab).click();\n cy.getByTestId(pkg.noUplinks).should('be.visible');\n });\n\n // ── Action-bar FABs: tarball download + raw viewer ─────────────\n // Both buttons live in the sidebar ActionBar. They're gated on\n // `web.showDownloadTarball` / `web.showRaw` (both default to true\n // in the ui-theme's AppConfigurationProvider) and each test is\n // also guarded by a feature flag so branches that ship a\n // different action bar can skip cleanly.\n\n maybeIt(features.publish.downloadTarball)(\n 'should fetch the tarball when the download button is clicked',\n () => {\n // The download provider hits the package manifest's dist.tarball\n // URL directly. For our published fixture the filename looks\n // like `pkg-scoped-1.0.0-t<ts>.tgz`, served from\n // `/<pkg>/-/<filename>.tgz`. Intercept before clicking.\n cy.intercept('GET', '**/pkg-scoped-*.tgz').as('tarballFetch');\n\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@sidebar');\n\n cy.getByTestId(pkg.downloadTarballBtn).should('be.visible').click();\n\n // The fetch should fire and return 200. We can't assert on the\n // actual file landing on disk — Cypress doesn't track OS-level\n // downloads — but a successful GET proves the end-to-end path\n // from click → download provider → registry.\n cy.wait('@tarballFetch', { timeout: 10000 }).its('response.statusCode').should('eq', 200);\n }\n );\n\n maybeIt(features.publish.rawViewer)(\n 'should open the raw manifest viewer when the raw button is clicked',\n () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@sidebar');\n\n // RawViewer is a full-screen MUI Dialog — initially unmounted\n // because `isOpen=false` keeps Dialog closed and Cypress won't\n // find it. Clicking the FAB flips `isOpen` to true.\n cy.getByTestId(pkg.rawBtn).should('be.visible').click();\n\n cy.getByTestId(pkg.rawViewerDialog, { timeout: 5000 }).should('be.visible');\n // The ReactJson viewer renders the package manifest — the\n // package name should appear somewhere in the serialized JSON.\n cy.getByTestId(pkg.rawViewerDialog).should('contain.text', pkgName);\n\n // Close via the X button and confirm the dialog goes away so\n // subsequent tests don't inherit an open overlay.\n cy.getByTestId(pkg.closeRawViewer).click();\n cy.getByTestId(pkg.rawViewerDialog).should('not.exist');\n }\n );\n });\n\n describe('private package tarball downloads', () => {\n const privatePkgName = '@private/tarball-fixture';\n let tempFolder: string | null = null;\n\n beforeEach(() => {\n cy.intercept('POST', '/-/verdaccio/sec/login').as('sign');\n cy.intercept('GET', '/-/verdaccio/data/packages').as('pkgs');\n cy.intercept('GET', '**/-/verdaccio/data/sidebar/@private/tarball-fixture*').as('sidebar');\n\n cy.task('publishPackage', {\n pkgName: privatePkgName,\n version: '1.0.0',\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n });\n\n loginOnce('private-tarball-suite');\n cy.visit(config.registryUrl);\n });\n\n afterEach(() => {\n cy.task('unpublishPackage', {\n pkgName: privatePkgName,\n tempFolder: tempFolder ?? undefined,\n });\n if (tempFolder) {\n cy.task('cleanupPublished', tempFolder);\n }\n tempFolder = null;\n });\n\n maybeIt(features.publish.privateDownloadTarball)(\n 'should fetch a private tarball from the home package list',\n () => {\n cy.intercept('GET', '**/tarball-fixture-*.tgz').as('privateTarballFetch');\n\n cy.wait('@pkgs');\n cy.contains(`[data-testid=\"${pkg.title}\"]`, privatePkgName)\n .should('be.visible')\n .closest(`[data-testid=\"${pkg.itemList}\"]`)\n .find(`[data-testid=\"${pkg.downloadTarball}\"]`)\n .should('be.visible')\n .click();\n\n cy.wait('@privateTarballFetch', { timeout: 10000 })\n .its('response.statusCode')\n .should('eq', 200);\n }\n );\n\n maybeIt(features.publish.privateDownloadTarball)(\n 'should fetch a private tarball from the package sidebar',\n () => {\n cy.intercept('GET', '**/tarball-fixture-*.tgz').as('privateTarballFetch');\n\n cy.wait('@pkgs');\n cy.contains(`[data-testid=\"${pkg.title}\"]`, privatePkgName).click();\n cy.wait('@sidebar');\n cy.getByTestId(pkg.downloadTarballBtn).should('be.visible').click();\n\n cy.wait('@privateTarballFetch', { timeout: 10000 })\n .its('response.statusCode')\n .should('eq', 200);\n }\n );\n });\n}\n"],"mappings":"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"}
@@ -33,7 +33,7 @@ function settingsTests(config) {
33
33
  it("should show both Package Managers and Translations tabs", () => {
34
34
  cy.getByTestId(header.settingsTooltip).click();
35
35
  cy.get("[role=\"dialog\"]").should("be.visible");
36
- cy.get("[role=\"dialog\"] [role=\"tab\"]").should("have.length", 2);
36
+ cy.get("[role=\"dialog\"] [role=\"tab\"]").should("have.length.at.least", 2);
37
37
  cy.contains("[role=\"dialog\"] [role=\"tab\"]", "Translations").should("be.visible");
38
38
  });
39
39
  it("should switch to the Translations tab and list languages", () => {
@@ -49,7 +49,10 @@ function settingsTests(config) {
49
49
  cy.get("[role=\"dialog\"]").should("be.visible");
50
50
  cy.contains("[role=\"dialog\"] [role=\"tab\"]", "Translations").click();
51
51
  cy.contains("[role=\"dialog\"]", "Configuration").invoke("text").then((englishTitle) => {
52
- cy.contains("[role=\"dialog\"] .MuiCard-root", "German").click({ force: true });
52
+ cy.contains("[role=\"dialog\"] .MuiCard-root", "German").then(($card) => {
53
+ const $area = $card.find(".MuiCardActionArea-root");
54
+ cy.wrap($area.length ? $area : $card).click({ force: true });
55
+ });
53
56
  cy.contains("[role=\"dialog\"] [role=\"tab\"]", "Translations").should("not.exist");
54
57
  cy.contains("[role=\"dialog\"]", englishTitle).should("not.exist");
55
58
  });
@@ -1 +1 @@
1
- {"version":3,"file":"settings.js","names":[],"sources":["../../../src/tests/settings.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Tests for the header Settings dialog and the Translations (language)\n * picker inside it.\n *\n * Requires `web.showSettings: true` in the registry config — the gear\n * icon only renders when the flag is on (see HeaderRight in Verdaccio's\n * ui-components package).\n *\n * Selector strategy: the language cards in LanguageSwitch do NOT have\n * data-testids, so this suite leans on:\n * - MUI's ARIA roles: `[role=\"dialog\"]`, `[role=\"tab\"]`\n * - The stable English labels \"Configuration\", \"Translations\",\n * \"English\", \"German\", and the language description text, which are\n * bundled into the ui-theme via `@verdaccio/ui-i18n`\n * - The settings trigger's testid from Verdaccio's HeaderToolTipIcon:\n * `header--tooltip-settings`\n */\nexport function settingsTests(config: RegistryConfig) {\n const { header } = config.testIds;\n const { features } = config;\n\n describe('settings & language', () => {\n beforeEach(() => {\n cy.visit(config.registryUrl);\n cy.get('body').should('be.visible');\n });\n\n it('should open the settings dialog from the header', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n // Dialog title key: dialog.settings.title → \"Configuration\" in EN.\n cy.contains('[role=\"dialog\"]', 'Configuration').should('be.visible');\n });\n\n it('should show both Package Managers and Translations tabs', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.get('[role=\"dialog\"] [role=\"tab\"]').should('have.length', 2);\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').should('be.visible');\n });\n\n it('should switch to the Translations tab and list languages', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').click();\n // language.description text is bundled in EN — good sentinel.\n cy.contains('[role=\"dialog\"]', /this is the configuration details for the language/i).should(\n 'be.visible'\n );\n // Sanity-check a couple of language cards are rendered.\n cy.contains('[role=\"dialog\"]', 'English').should('be.visible');\n cy.contains('[role=\"dialog\"]', 'German').should('be.visible');\n });\n\n maybeIt(features.settings.languageSwitcher)(\n 'should change the UI language when a language card is clicked',\n () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').click();\n\n // Before switching, capture the current dialog title so we can\n // assert it changes. This avoids hard-coding the German string.\n cy.contains('[role=\"dialog\"]', 'Configuration')\n .invoke('text')\n .then((englishTitle) => {\n // Click the German card. We scope to the dialog + the MUI\n // card class to avoid matching other \"German\" text.\n cy.contains('[role=\"dialog\"] .MuiCard-root', 'German').click({\n force: true,\n });\n\n // The English tab label \"Translations\" must no longer be\n // present anywhere in the dialog — strongest proof the\n // language actually switched.\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').should('not.exist');\n // And the original dialog title text should also be gone.\n cy.contains('[role=\"dialog\"]', englishTitle).should('not.exist');\n });\n }\n );\n\n it('should close the settings dialog with Escape', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.get('body').type('{esc}');\n cy.get('[role=\"dialog\"]').should('not.exist');\n });\n\n maybeIt(features.settings.localizedDates)(\n 'relative dates must follow the selected language (dayjs locale)',\n () => {\n // fresh fixture so \"published … ago\" is a relative date\n const pkgName = '@verdaccio/dates-fixture';\n let tempFolder: string | null = null;\n cy.task('publishPackage', { pkgName, version: '1.0.0', unique: true }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n });\n\n // switch the UI to German via the settings dialog\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').click();\n cy.contains('[role=\"dialog\"] .MuiCard-root', 'German').click({ force: true });\n cy.get('body').type('{esc}');\n\n // the language persists; a fresh page load must localize dayjs too\n const sidebar = `[data-testid=\"${config.testIds.package.sidebar}\"]`;\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);\n cy.get(sidebar, { timeout: 10000 }).should('be.visible');\n // dayjs de locale renders relative times as \"vor ein paar Sekunden\";\n // the released UIs kept English (\"a few seconds ago\") in every language\n cy.get(sidebar).contains(/vor /);\n cy.get(sidebar).contains(/ago\\b/).should('not.exist');\n\n cy.task('unpublishPackage', { pkgName, tempFolder: tempFolder ?? undefined });\n cy.then(() => {\n if (tempFolder) cy.task('cleanupPublished', tempFolder);\n });\n }\n );\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,cAAc,QAAwB;CACpD,MAAM,EAAE,WAAW,OAAO;CAC1B,MAAM,EAAE,aAAa;CAErB,SAAS,6BAA6B;EACpC,iBAAiB;GACf,GAAG,MAAM,OAAO,WAAW;GAC3B,GAAG,IAAI,MAAM,CAAC,CAAC,OAAO,YAAY;EACpC,CAAC;EAED,GAAG,yDAAyD;GAC1D,GAAG,YAAY,OAAO,eAAe,CAAC,CAAC,MAAM;GAC7C,GAAG,IAAI,mBAAiB,CAAC,CAAC,OAAO,YAAY;GAE7C,GAAG,SAAS,qBAAmB,eAAe,CAAC,CAAC,OAAO,YAAY;EACrE,CAAC;EAED,GAAG,iEAAiE;GAClE,GAAG,YAAY,OAAO,eAAe,CAAC,CAAC,MAAM;GAC7C,GAAG,IAAI,mBAAiB,CAAC,CAAC,OAAO,YAAY;GAC7C,GAAG,IAAI,kCAA8B,CAAC,CAAC,OAAO,eAAe,CAAC;GAC9D,GAAG,SAAS,oCAAgC,cAAc,CAAC,CAAC,OAAO,YAAY;EACjF,CAAC;EAED,GAAG,kEAAkE;GACnE,GAAG,YAAY,OAAO,eAAe,CAAC,CAAC,MAAM;GAC7C,GAAG,IAAI,mBAAiB,CAAC,CAAC,OAAO,YAAY;GAC7C,GAAG,SAAS,oCAAgC,cAAc,CAAC,CAAC,MAAM;GAElE,GAAG,SAAS,qBAAmB,qDAAqD,CAAC,CAAC,OACpF,YACF;GAEA,GAAG,SAAS,qBAAmB,SAAS,CAAC,CAAC,OAAO,YAAY;GAC7D,GAAG,SAAS,qBAAmB,QAAQ,CAAC,CAAC,OAAO,YAAY;EAC9D,CAAC;EAED,QAAQ,SAAS,SAAS,gBAAgB,CAAC,CACzC,uEACM;GACJ,GAAG,YAAY,OAAO,eAAe,CAAC,CAAC,MAAM;GAC7C,GAAG,IAAI,mBAAiB,CAAC,CAAC,OAAO,YAAY;GAC7C,GAAG,SAAS,oCAAgC,cAAc,CAAC,CAAC,MAAM;GAIlE,GAAG,SAAS,qBAAmB,eAAe,CAAC,CAC5C,OAAO,MAAM,CAAC,CACd,MAAM,iBAAiB;IAGtB,GAAG,SAAS,mCAAiC,QAAQ,CAAC,CAAC,MAAM,EAC3D,OAAO,KACT,CAAC;IAKD,GAAG,SAAS,oCAAgC,cAAc,CAAC,CAAC,OAAO,WAAW;IAE9E,GAAG,SAAS,qBAAmB,YAAY,CAAC,CAAC,OAAO,WAAW;GACjE,CAAC;EACL,CACF;EAEA,GAAG,sDAAsD;GACvD,GAAG,YAAY,OAAO,eAAe,CAAC,CAAC,MAAM;GAC7C,GAAG,IAAI,mBAAiB,CAAC,CAAC,OAAO,YAAY;GAC7C,GAAG,IAAI,MAAM,CAAC,CAAC,KAAK,OAAO;GAC3B,GAAG,IAAI,mBAAiB,CAAC,CAAC,OAAO,WAAW;EAC9C,CAAC;EAED,QAAQ,SAAS,SAAS,cAAc,CAAC,CACvC,yEACM;GAEJ,MAAM,UAAU;GAChB,IAAI,aAA4B;GAChC,GAAG,KAAK,kBAAkB;IAAE;IAAS,SAAS;IAAS,QAAQ;GAAK,CAAC,CAAC,CAAC,MAAM,WAAW;IACtF,aAAa,QAAQ,cAAc;GACrC,CAAC;GAGD,GAAG,YAAY,OAAO,eAAe,CAAC,CAAC,MAAM;GAC7C,GAAG,IAAI,mBAAiB,CAAC,CAAC,OAAO,YAAY;GAC7C,GAAG,SAAS,oCAAgC,cAAc,CAAC,CAAC,MAAM;GAClE,GAAG,SAAS,mCAAiC,QAAQ,CAAC,CAAC,MAAM,EAAE,OAAO,KAAK,CAAC;GAC5E,GAAG,IAAI,MAAM,CAAC,CAAC,KAAK,OAAO;GAG3B,MAAM,UAAU,iBAAiB,OAAO,QAAQ,QAAQ,QAAQ;GAChE,GAAG,MAAM,GAAG,OAAO,YAAY,gBAAgB,SAAS;GACxD,GAAG,IAAI,SAAS,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,OAAO,YAAY;GAGvD,GAAG,IAAI,OAAO,CAAC,CAAC,SAAS,MAAM;GAC/B,GAAG,IAAI,OAAO,CAAC,CAAC,SAAS,OAAO,CAAC,CAAC,OAAO,WAAW;GAEpD,GAAG,KAAK,oBAAoB;IAAE;IAAS,YAAY,cAAc,KAAA;GAAU,CAAC;GAC5E,GAAG,WAAW;IACZ,IAAI,YAAY,GAAG,KAAK,oBAAoB,UAAU;GACxD,CAAC;EACH,CACF;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"settings.js","names":[],"sources":["../../../src/tests/settings.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Tests for the header Settings dialog and the Translations (language)\n * picker inside it.\n *\n * Requires `web.showSettings: true` in the registry config — the gear\n * icon only renders when the flag is on (see HeaderRight in Verdaccio's\n * ui-components package).\n *\n * Selector strategy: the language cards in LanguageSwitch do NOT have\n * data-testids, so this suite leans on:\n * - MUI's ARIA roles: `[role=\"dialog\"]`, `[role=\"tab\"]`\n * - The stable English labels \"Configuration\", \"Translations\",\n * \"English\", \"German\", and the language description text, which are\n * bundled into the ui-theme via `@verdaccio/ui-i18n`\n * - The settings trigger's testid from Verdaccio's HeaderToolTipIcon:\n * `header--tooltip-settings`\n */\nexport function settingsTests(config: RegistryConfig) {\n const { header } = config.testIds;\n const { features } = config;\n\n describe('settings & language', () => {\n beforeEach(() => {\n cy.visit(config.registryUrl);\n cy.get('body').should('be.visible');\n });\n\n it('should open the settings dialog from the header', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n // Dialog title key: dialog.settings.title → \"Configuration\" in EN.\n cy.contains('[role=\"dialog\"]', 'Configuration').should('be.visible');\n });\n\n it('should show both Package Managers and Translations tabs', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n // 9.x adds a \"Publishing\" tab, so count the two this test is about as a minimum\n cy.get('[role=\"dialog\"] [role=\"tab\"]').should('have.length.at.least', 2);\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').should('be.visible');\n });\n\n it('should switch to the Translations tab and list languages', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').click();\n // language.description text is bundled in EN — good sentinel.\n cy.contains('[role=\"dialog\"]', /this is the configuration details for the language/i).should(\n 'be.visible'\n );\n // Sanity-check a couple of language cards are rendered.\n cy.contains('[role=\"dialog\"]', 'English').should('be.visible');\n cy.contains('[role=\"dialog\"]', 'German').should('be.visible');\n });\n\n maybeIt(features.settings.languageSwitcher)(\n 'should change the UI language when a language card is clicked',\n () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').click();\n\n // Before switching, capture the current dialog title so we can\n // assert it changes. This avoids hard-coding the German string.\n cy.contains('[role=\"dialog\"]', 'Configuration')\n .invoke('text')\n .then((englishTitle) => {\n // Click the German card. We scope to the dialog + the MUI\n // card class to avoid matching other \"German\" text.\n // released UIs handle the click on the card, 9.x on its CardActionArea\n cy.contains('[role=\"dialog\"] .MuiCard-root', 'German').then(($card) => {\n const $area = $card.find('.MuiCardActionArea-root');\n cy.wrap($area.length ? $area : $card).click({ force: true });\n });\n\n // The English tab label \"Translations\" must no longer be\n // present anywhere in the dialog — strongest proof the\n // language actually switched.\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').should('not.exist');\n // And the original dialog title text should also be gone.\n cy.contains('[role=\"dialog\"]', englishTitle).should('not.exist');\n });\n }\n );\n\n it('should close the settings dialog with Escape', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.get('body').type('{esc}');\n cy.get('[role=\"dialog\"]').should('not.exist');\n });\n\n maybeIt(features.settings.localizedDates)(\n 'relative dates must follow the selected language (dayjs locale)',\n () => {\n // fresh fixture so \"published … ago\" is a relative date\n const pkgName = '@verdaccio/dates-fixture';\n let tempFolder: string | null = null;\n cy.task('publishPackage', { pkgName, version: '1.0.0', unique: true }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n });\n\n // switch the UI to German via the settings dialog\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').click();\n cy.contains('[role=\"dialog\"] .MuiCard-root', 'German').click({ force: true });\n cy.get('body').type('{esc}');\n\n // the language persists; a fresh page load must localize dayjs too\n const sidebar = `[data-testid=\"${config.testIds.package.sidebar}\"]`;\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);\n cy.get(sidebar, { timeout: 10000 }).should('be.visible');\n // dayjs de locale renders relative times as \"vor ein paar Sekunden\";\n // the released UIs kept English (\"a few seconds ago\") in every language\n cy.get(sidebar).contains(/vor /);\n cy.get(sidebar).contains(/ago\\b/).should('not.exist');\n\n cy.task('unpublishPackage', { pkgName, tempFolder: tempFolder ?? undefined });\n cy.then(() => {\n if (tempFolder) cy.task('cleanupPublished', tempFolder);\n });\n }\n );\n });\n}\n"],"mappings":"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"}
@@ -226,6 +226,51 @@ export interface Features {
226
226
  */
227
227
  validation: boolean;
228
228
  };
229
+ homeDetailUx: {
230
+ /**
231
+ * Whether to run the home toolbar tests: filter narrows the list and
232
+ * lives in `?q=`, `?scope=` restores the scope, no-match message.
233
+ * Ships with the 9.x home toolbar — probe, defaults to `false`.
234
+ */
235
+ homeToolbar: boolean;
236
+ /**
237
+ * Whether to assert the package page tab lives in `?tab=` (click
238
+ * writes it, a link opens it, readme removes it) — probe.
239
+ */
240
+ detailTabInUrl: boolean;
241
+ /**
242
+ * Whether to assert the step-by-step welcome card (setup tabs per
243
+ * package manager, `.npmrc` tab, uplinks note) — probe.
244
+ */
245
+ welcomeGuide: boolean;
246
+ /**
247
+ * Whether to assert the sidebar install box lists `bun` and the
248
+ * `.npmrc` registry line. Needs `bun` in `web.pkgManagers` (default
249
+ * on 9.x) — probe.
250
+ */
251
+ installBunAndNpmrc: boolean;
252
+ /**
253
+ * Whether to assert that a copy button confirms the copy (its
254
+ * accessible name switches to "Copied") — probe.
255
+ */
256
+ copyFeedback: boolean;
257
+ /**
258
+ * Whether to assert search results are grouped by origin and a
259
+ * locally published package lands in the "published here" group.
260
+ * Needs `web.showUplinks` (default true) — probe.
261
+ */
262
+ searchGrouping: boolean;
263
+ /**
264
+ * Whether to assert that the version on each home list item links
265
+ * to its version page `/-/web/detail/<name>/v/<version>` — probe.
266
+ */
267
+ versionLink: boolean;
268
+ /**
269
+ * Whether to assert the settings dialog has a "Publishing" tab with
270
+ * the welcome guide, so it stays reachable once packages exist — probe.
271
+ */
272
+ settingsPublishTab: boolean;
273
+ };
229
274
  changePassword: {
230
275
  /**
231
276
  * Whether to run the happy-path test (submit valid change,
package/build/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export type { Features } from './features';
6
6
  export { DEFAULT_SELECTORS, DEFAULT_TEST_IDS } from './testIds';
7
7
  export { DEFAULT_FEATURES, maybeIt } from './features';
8
8
  export { publishPackage, cleanupPublished, unpublishPackage } from './tasks';
9
- export { homeTests, signinTests, publishTests, searchTests, settingsTests, layoutTests, changePasswordTests, detailTests, failureModeTests, i18nKeyTests, manifestRenderingTests, sessionTests, signupTests, } from './tests';
9
+ export { homeTests, signinTests, publishTests, searchTests, settingsTests, layoutTests, changePasswordTests, detailTests, failureModeTests, i18nKeyTests, manifestRenderingTests, sessionTests, signupTests, homeDetailUxTests, } from './tests';
10
10
  /**
11
11
  * Build a full RegistryConfig from user-provided options with defaults.
12
12
  *
@@ -21,6 +21,20 @@ export interface TestIds {
21
21
  helpCard: string;
22
22
  /** 404 "not found" container. */
23
23
  notFound: string;
24
+ /** Filter / scope / sort bar above the package list (9.x). */
25
+ packageListToolbar: string;
26
+ /** Text filter input inside the toolbar. */
27
+ packageListFilter: string;
28
+ /** Native input of the scope select (only with more than one scope). */
29
+ packageListScope: string;
30
+ /** Message shown when the filter matches no package. */
31
+ packageListNoMatch: string;
32
+ /** Prefix of the welcome card setup tabs: `<prefix><manager>`. */
33
+ helpSetupTabPrefix: string;
34
+ /** Note on the welcome card explaining cached uplink packages. */
35
+ helpUplinksNote: string;
36
+ /** Step list of the publishing guide, in the welcome card and the settings (9.x). */
37
+ helpGuide: string;
24
38
  };
25
39
  header: {
26
40
  /** Outermost `<NavBar>` element. */
@@ -39,6 +53,10 @@ export interface TestIds {
39
53
  loginButton: string;
40
54
  /** Gear icon that opens the settings dialog. */
41
55
  settingsTooltip: string;
56
+ /** "Publishing" tab of the settings dialog (9.x). */
57
+ settingsPublishTab: string;
58
+ /** Panel of the "Publishing" tab (9.x). */
59
+ settingsPublishPanel: string;
42
60
  /** Info icon that opens the registry info dialog. */
43
61
  infoTooltip: string;
44
62
  /**
@@ -79,6 +97,8 @@ export interface TestIds {
79
97
  itemList: string;
80
98
  /** Package name link in the package list (home + search results). */
81
99
  title: string;
100
+ /** Version link of a package list item, to its version page (9.x). */
101
+ versionLink: string;
82
102
  /** Readme container on the package detail page. */
83
103
  readme: string;
84
104
  /** Sidebar container on the package detail page. */
@@ -91,6 +111,14 @@ export interface TestIds {
91
111
  installYarn: string;
92
112
  /** Individual install line for pnpm. */
93
113
  installPnpm: string;
114
+ /** Individual install line for bun (9.x). */
115
+ installBun: string;
116
+ /** `.npmrc` registry line below the install commands (9.x). */
117
+ installNpmrc: string;
118
+ /** Copy button of the `.npmrc` registry line. */
119
+ installNpmrcCopy: string;
120
+ /** Tab that shows the readme. */
121
+ readmeTab: string;
94
122
  /** Keyword list below the install section. */
95
123
  keywordList: string;
96
124
  /** Tab that reveals the dependencies view. */
@@ -116,6 +144,12 @@ export interface TestIds {
116
144
  /** Close button inside the raw viewer dialog. */
117
145
  closeRawViewer: string;
118
146
  };
147
+ search: {
148
+ /** Prefix of the origin group headers: `<prefix>private|cached|remote`. */
149
+ groupPrefix: string;
150
+ /** Prefix of the origin icon on each result: `<prefix>private|...`. */
151
+ originPrefix: string;
152
+ };
119
153
  errors: {
120
154
  /**
121
155
  * Generic error page shown when a data request fails without a
@@ -0,0 +1,10 @@
1
+ import { RegistryConfig } from '../types';
2
+ /**
3
+ * Home and package page UX that ships with the 9.x web UI: the package
4
+ * list toolbar, the tab in the address, the welcome guide, the install
5
+ * box (bun and `.npmrc`), copy feedback and search grouped by origin.
6
+ *
7
+ * Every test is a probe (flag defaults to `false`): released lines do
8
+ * not have these features yet.
9
+ */
10
+ export declare function homeDetailUxTests(config: RegistryConfig): void;
@@ -11,3 +11,4 @@ export { i18nKeyTests } from './i18n-keys';
11
11
  export { signupTests } from './signup';
12
12
  export { manifestRenderingTests } from './manifest-rendering';
13
13
  export { sessionTests } from './session';
14
+ export { homeDetailUxTests } from './home-detail-ux';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verdaccio/e2e-ui",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Cypress plugin for Verdaccio UI e2e tests — run against any registry",