@verdaccio/e2e-ui 2.6.0 → 2.7.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 (45) hide show
  1. package/build/cjs/features.cjs +26 -3
  2. package/build/cjs/features.cjs.map +1 -1
  3. package/build/cjs/index.cjs +7 -0
  4. package/build/cjs/index.cjs.map +1 -1
  5. package/build/cjs/tasks/publish.cjs +5 -4
  6. package/build/cjs/tasks/publish.cjs.map +1 -1
  7. package/build/cjs/tests/change-password.cjs +1 -2
  8. package/build/cjs/tests/change-password.cjs.map +1 -1
  9. package/build/cjs/tests/detail.cjs +33 -0
  10. package/build/cjs/tests/detail.cjs.map +1 -1
  11. package/build/cjs/tests/failure-modes.cjs +22 -0
  12. package/build/cjs/tests/failure-modes.cjs.map +1 -1
  13. package/build/cjs/tests/index.cjs +4 -0
  14. package/build/cjs/tests/manifest-rendering.cjs +73 -0
  15. package/build/cjs/tests/manifest-rendering.cjs.map +1 -0
  16. package/build/cjs/tests/session.cjs +49 -0
  17. package/build/cjs/tests/session.cjs.map +1 -0
  18. package/build/cjs/tests/settings.cjs +28 -0
  19. package/build/cjs/tests/settings.cjs.map +1 -1
  20. package/build/esm/features.js +26 -3
  21. package/build/esm/features.js.map +1 -1
  22. package/build/esm/index.js +6 -1
  23. package/build/esm/index.js.map +1 -1
  24. package/build/esm/tasks/publish.js +5 -4
  25. package/build/esm/tasks/publish.js.map +1 -1
  26. package/build/esm/tests/change-password.js +1 -2
  27. package/build/esm/tests/change-password.js.map +1 -1
  28. package/build/esm/tests/detail.js +33 -0
  29. package/build/esm/tests/detail.js.map +1 -1
  30. package/build/esm/tests/failure-modes.js +22 -0
  31. package/build/esm/tests/failure-modes.js.map +1 -1
  32. package/build/esm/tests/index.js +3 -1
  33. package/build/esm/tests/manifest-rendering.js +73 -0
  34. package/build/esm/tests/manifest-rendering.js.map +1 -0
  35. package/build/esm/tests/session.js +49 -0
  36. package/build/esm/tests/session.js.map +1 -0
  37. package/build/esm/tests/settings.js +28 -0
  38. package/build/esm/tests/settings.js.map +1 -1
  39. package/build/features.d.ts +60 -0
  40. package/build/index.d.ts +1 -1
  41. package/build/tasks/publish.d.ts +9 -0
  42. package/build/tests/index.d.ts +2 -0
  43. package/build/tests/manifest-rendering.d.ts +14 -0
  44. package/build/tests/session.d.ts +2 -0
  45. 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 onSubmit catch block in ChangePassword.tsx sets a hardcoded\n // English string — if the upstream component ever localizes this,\n // this constant and the wrongOldPassword test will need updating.\n const GENERIC_FAILURE_TEXT = 'Failed to change password';\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 (htpasswd → plain Error → handler returns 4xx).\n cy.wait('@reset').its('response.statusCode').should('not.eq', 200);\n // onSubmit's catch sets errors.root → rendered via LoginDialogFormError.\n cy.getByTestId(login.error, { timeout: 5000 })\n .should('be.visible')\n .and('contain.text', GENERIC_FAILURE_TEXT);\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":"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"}
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":"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"}
@@ -56,6 +56,39 @@ function detailTests(config) {
56
56
  cy.getByTestId(pkg.sidebar).should("be.visible");
57
57
  cy.getByTestId(pkg.sidebar).contains(version);
58
58
  });
59
+ maybeIt(features.detail.staleVersionsNavigation)("the versions tab must show the CURRENT package after SPA navigation", () => {
60
+ const otherPkg = "@verdaccio/stale-nav-fixture";
61
+ let otherTemp = null;
62
+ cy.task("publishPackage", {
63
+ pkgName: otherPkg,
64
+ version: "2.0.0",
65
+ unique: true
66
+ }).then((result) => {
67
+ otherTemp = result?.tempFolder ?? null;
68
+ });
69
+ cy.task("publishPackage", {
70
+ pkgName,
71
+ version: "1.1.0",
72
+ unique: true
73
+ }).then((r) => {
74
+ if (r?.tempFolder) cy.task("cleanupPublished", r.tempFolder);
75
+ });
76
+ cy.visit(config.registryUrl);
77
+ cy.contains(`[data-testid="${pkg.title}"]`, pkgName).click();
78
+ cy.getByTestId(pkg.versionsTab, { timeout: 1e4 }).click();
79
+ cy.get("[data-testid=\"version-list-text\"]").should("have.length", 2);
80
+ cy.getByTestId(config.testIds.header.defaultLogo).first().click();
81
+ cy.contains(`[data-testid="${pkg.title}"]`, otherPkg).click();
82
+ cy.getByTestId(pkg.versionsTab, { timeout: 1e4 }).click();
83
+ cy.get("[data-testid=\"version-list-text\"]").should("have.length", 1);
84
+ cy.task("unpublishPackage", {
85
+ pkgName: otherPkg,
86
+ tempFolder: otherTemp ?? void 0
87
+ });
88
+ cy.then(() => {
89
+ if (otherTemp) cy.task("cleanupPublished", otherTemp);
90
+ });
91
+ });
59
92
  maybeIt(features.detail.versionNotFound)("should render Not Found for a version that does not exist", () => {
60
93
  cy.intercept("GET", `/-/verdaccio/data/sidebar/${pkgName}*`).as("sidebar");
61
94
  cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}/v/9.9.9`);
@@ -1 +1 @@
1
- {"version":3,"file":"detail.js","names":[],"sources":["../../../src/tests/detail.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Package detail page tests: version-pinned URLs and the wire format of\n * the UI's data requests.\n *\n * The wire format is an observable contract, not an implementation\n * detail: proxies, intercepts and every npm client expect scoped names\n * as literal `/@scope/name`. A ui-components refactor that\n * percent-encoded the `@` broke exactly these suites\n * (verdaccio/verdaccio#6210), which is why it is pinned here on purpose.\n */\nexport function detailTests(config: RegistryConfig) {\n const { features } = config;\n const { package: pkg, home } = config.testIds;\n\n describe('package detail', () => {\n describe('with a published package', () => {\n const pkgName = '@verdaccio/detail-fixture';\n let tempFolder: string | null = null;\n // `unique: true` uniquifies the VERSION (1.0.0-t<ts>), not the name\n let version = '1.0.0';\n\n beforeEach(() => {\n cy.task('publishPackage', {\n pkgName,\n version: '1.0.0',\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n version = result?.version ?? '1.0.0';\n });\n });\n\n afterEach(() => {\n cy.task('unpublishPackage', {\n pkgName,\n tempFolder: tempFolder ?? undefined,\n });\n if (tempFolder) {\n cy.task('cleanupPublished', tempFolder);\n }\n tempFolder = null;\n });\n\n maybeIt(features.detail.scopedWireFormat)(\n 'should request sidebar and readme with the scope literal (@, not %40)',\n () => {\n // Literal-path intercepts: if the UI ever encodes `@` or `/`,\n // these never match and the waits below time out.\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}*`).as('sidebar');\n cy.intercept('GET', `/-/verdaccio/data/package/readme/${pkgName}*`).as('readme');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);\n\n cy.wait('@sidebar', { timeout: 10000 }).then((interception: any) => {\n expect(interception.request.url).to.contain(`/sidebar/${pkgName}`);\n expect(interception.request.url).to.not.contain('%40');\n });\n cy.wait('@readme', { timeout: 10000 });\n cy.getByTestId(pkg.sidebar).should('be.visible');\n }\n );\n\n maybeIt(features.detail.versionPinned)(\n 'should pin the requested version via the ?v= query on /v/<version> urls',\n () => {\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}*`).as('sidebar');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}/v/${version}`);\n\n cy.wait('@sidebar', { timeout: 10000 }).then((interception: any) => {\n expect(interception.request.url).to.contain(`v=${version}`);\n });\n cy.getByTestId(pkg.sidebar).should('be.visible');\n cy.getByTestId(pkg.sidebar).contains(version);\n }\n );\n\n maybeIt(features.detail.versionNotFound)(\n 'should render Not Found for a version that does not exist',\n () => {\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}*`).as('sidebar');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}/v/9.9.9`);\n\n // The server answers 404 for unknown versions instead of\n // silently serving `latest` under the requested title.\n cy.wait('@sidebar', { timeout: 10000 }).then((interception: any) => {\n expect(interception.response?.statusCode).to.eq(404);\n });\n cy.getByTestId(home.notFound, { timeout: 10000 }).should('be.visible');\n }\n );\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;AAcA,SAAgB,YAAY,QAAwB;CAClD,MAAM,EAAE,aAAa;CACrB,MAAM,EAAE,SAAS,KAAK,SAAS,OAAO;CAEtC,SAAS,wBAAwB;EAC/B,SAAS,kCAAkC;GACzC,MAAM,UAAU;GAChB,IAAI,aAA4B;GAEhC,IAAI,UAAU;GAEd,iBAAiB;IACf,GAAG,KAAK,kBAAkB;KACxB;KACA,SAAS;KACT,QAAQ;IACV,CAAC,CAAC,CAAC,MAAM,WAAW;KAClB,aAAa,QAAQ,cAAc;KACnC,UAAU,QAAQ,WAAW;IAC/B,CAAC;GACH,CAAC;GAED,gBAAgB;IACd,GAAG,KAAK,oBAAoB;KAC1B;KACA,YAAY,cAAc,KAAA;IAC5B,CAAC;IACD,IAAI,YACF,GAAG,KAAK,oBAAoB,UAAU;IAExC,aAAa;GACf,CAAC;GAED,QAAQ,SAAS,OAAO,gBAAgB,CAAC,CACvC,+EACM;IAGJ,GAAG,UAAU,OAAO,6BAA6B,QAAQ,EAAE,CAAC,CAAC,GAAG,SAAS;IACzE,GAAG,UAAU,OAAO,oCAAoC,QAAQ,EAAE,CAAC,CAAC,GAAG,QAAQ;IAE/E,GAAG,MAAM,GAAG,OAAO,YAAY,gBAAgB,SAAS;IAExD,GAAG,KAAK,YAAY,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,MAAM,iBAAsB;KAClE,OAAO,aAAa,QAAQ,GAAG,CAAC,CAAC,GAAG,QAAQ,YAAY,SAAS;KACjE,OAAO,aAAa,QAAQ,GAAG,CAAC,CAAC,GAAG,IAAI,QAAQ,KAAK;IACvD,CAAC;IACD,GAAG,KAAK,WAAW,EAAE,SAAS,IAAM,CAAC;IACrC,GAAG,YAAY,IAAI,OAAO,CAAC,CAAC,OAAO,YAAY;GACjD,CACF;GAEA,QAAQ,SAAS,OAAO,aAAa,CAAC,CACpC,iFACM;IACJ,GAAG,UAAU,OAAO,6BAA6B,QAAQ,EAAE,CAAC,CAAC,GAAG,SAAS;IAEzE,GAAG,MAAM,GAAG,OAAO,YAAY,gBAAgB,QAAQ,KAAK,SAAS;IAErE,GAAG,KAAK,YAAY,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,MAAM,iBAAsB;KAClE,OAAO,aAAa,QAAQ,GAAG,CAAC,CAAC,GAAG,QAAQ,KAAK,SAAS;IAC5D,CAAC;IACD,GAAG,YAAY,IAAI,OAAO,CAAC,CAAC,OAAO,YAAY;IAC/C,GAAG,YAAY,IAAI,OAAO,CAAC,CAAC,SAAS,OAAO;GAC9C,CACF;GAEA,QAAQ,SAAS,OAAO,eAAe,CAAC,CACtC,mEACM;IACJ,GAAG,UAAU,OAAO,6BAA6B,QAAQ,EAAE,CAAC,CAAC,GAAG,SAAS;IAEzE,GAAG,MAAM,GAAG,OAAO,YAAY,gBAAgB,QAAQ,SAAS;IAIhE,GAAG,KAAK,YAAY,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,MAAM,iBAAsB;KAClE,OAAO,aAAa,UAAU,UAAU,CAAC,CAAC,GAAG,GAAG,GAAG;IACrD,CAAC;IACD,GAAG,YAAY,KAAK,UAAU,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,OAAO,YAAY;GACvE,CACF;EACF,CAAC;CACH,CAAC;AACH"}
1
+ {"version":3,"file":"detail.js","names":[],"sources":["../../../src/tests/detail.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Package detail page tests: version-pinned URLs and the wire format of\n * the UI's data requests.\n *\n * The wire format is an observable contract, not an implementation\n * detail: proxies, intercepts and every npm client expect scoped names\n * as literal `/@scope/name`. A ui-components refactor that\n * percent-encoded the `@` broke exactly these suites\n * (verdaccio/verdaccio#6210), which is why it is pinned here on purpose.\n */\nexport function detailTests(config: RegistryConfig) {\n const { features } = config;\n const { package: pkg, home } = config.testIds;\n\n describe('package detail', () => {\n describe('with a published package', () => {\n const pkgName = '@verdaccio/detail-fixture';\n let tempFolder: string | null = null;\n // `unique: true` uniquifies the VERSION (1.0.0-t<ts>), not the name\n let version = '1.0.0';\n\n beforeEach(() => {\n cy.task('publishPackage', {\n pkgName,\n version: '1.0.0',\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n version = result?.version ?? '1.0.0';\n });\n });\n\n afterEach(() => {\n cy.task('unpublishPackage', {\n pkgName,\n tempFolder: tempFolder ?? undefined,\n });\n if (tempFolder) {\n cy.task('cleanupPublished', tempFolder);\n }\n tempFolder = null;\n });\n\n maybeIt(features.detail.scopedWireFormat)(\n 'should request sidebar and readme with the scope literal (@, not %40)',\n () => {\n // Literal-path intercepts: if the UI ever encodes `@` or `/`,\n // these never match and the waits below time out.\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}*`).as('sidebar');\n cy.intercept('GET', `/-/verdaccio/data/package/readme/${pkgName}*`).as('readme');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);\n\n cy.wait('@sidebar', { timeout: 10000 }).then((interception: any) => {\n expect(interception.request.url).to.contain(`/sidebar/${pkgName}`);\n expect(interception.request.url).to.not.contain('%40');\n });\n cy.wait('@readme', { timeout: 10000 });\n cy.getByTestId(pkg.sidebar).should('be.visible');\n }\n );\n\n maybeIt(features.detail.versionPinned)(\n 'should pin the requested version via the ?v= query on /v/<version> urls',\n () => {\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}*`).as('sidebar');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}/v/${version}`);\n\n cy.wait('@sidebar', { timeout: 10000 }).then((interception: any) => {\n expect(interception.request.url).to.contain(`v=${version}`);\n });\n cy.getByTestId(pkg.sidebar).should('be.visible');\n cy.getByTestId(pkg.sidebar).contains(version);\n }\n );\n\n maybeIt(features.detail.staleVersionsNavigation)(\n 'the versions tab must show the CURRENT package after SPA navigation',\n () => {\n // second fixture with a different version count; the detail\n // routes reuse one mounted component, so navigating A -> B\n // must not leak A's cached versions into B's tab\n const otherPkg = '@verdaccio/stale-nav-fixture';\n let otherTemp: string | null = null;\n cy.task('publishPackage', { pkgName: otherPkg, version: '2.0.0', unique: true }).then(\n (result) => {\n otherTemp = result?.tempFolder ?? null;\n }\n );\n // give the first fixture a second version so the counts differ\n cy.task('publishPackage', { pkgName, version: '1.1.0', unique: true }).then((r) => {\n if (r?.tempFolder) cy.task('cleanupPublished', r.tempFolder);\n });\n\n cy.visit(config.registryUrl);\n cy.contains(`[data-testid=\"${pkg.title}\"]`, pkgName).click();\n cy.getByTestId(pkg.versionsTab, { timeout: 10000 }).click();\n cy.get('[data-testid=\"version-list-text\"]').should('have.length', 2);\n\n // SPA navigation back home via the logo (no full reload);\n // the logo can render more than once (desktop + mobile nav)\n cy.getByTestId(config.testIds.header.defaultLogo).first().click();\n cy.contains(`[data-testid=\"${pkg.title}\"]`, otherPkg).click();\n cy.getByTestId(pkg.versionsTab, { timeout: 10000 }).click();\n // stale-state bug: B showed A's list (length 2) here\n cy.get('[data-testid=\"version-list-text\"]').should('have.length', 1);\n\n cy.task('unpublishPackage', { pkgName: otherPkg, tempFolder: otherTemp ?? undefined });\n cy.then(() => {\n if (otherTemp) cy.task('cleanupPublished', otherTemp);\n });\n }\n );\n\n maybeIt(features.detail.versionNotFound)(\n 'should render Not Found for a version that does not exist',\n () => {\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}*`).as('sidebar');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}/v/9.9.9`);\n\n // The server answers 404 for unknown versions instead of\n // silently serving `latest` under the requested title.\n cy.wait('@sidebar', { timeout: 10000 }).then((interception: any) => {\n expect(interception.response?.statusCode).to.eq(404);\n });\n cy.getByTestId(home.notFound, { timeout: 10000 }).should('be.visible');\n }\n );\n });\n });\n}\n"],"mappings":"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"}
@@ -13,6 +13,17 @@ function failureModeTests(config) {
13
13
  const { features } = config;
14
14
  const { home, header, errors, package: pkg } = config.testIds;
15
15
  describe("failure modes", () => {
16
+ maybeIt(features.failureModes.searchError)("a search 5xx must show an error, not \"no results found\"", () => {
17
+ cy.intercept("GET", "**/-/verdaccio/data/search/**", {
18
+ statusCode: 500,
19
+ body: { error: "internal server error" }
20
+ }).as("search");
21
+ cy.visit(config.registryUrl);
22
+ cy.getByTestId(header.searchContainer).find("input").type("anything", { delay: 20 });
23
+ cy.wait("@search", { timeout: 1e4 });
24
+ cy.contains("Something went wrong while searching", { timeout: 1e4 }).should("be.visible");
25
+ cy.contains("No results found").should("not.exist");
26
+ });
16
27
  maybeIt(features.failureModes.homeServerError)("a 5xx on the package list must not render the empty-registry onboarding", () => {
17
28
  cy.intercept("GET", "/-/verdaccio/data/packages", {
18
29
  statusCode: 500,
@@ -51,6 +62,17 @@ function failureModeTests(config) {
51
62
  if (tempFolder) cy.task("cleanupPublished", tempFolder);
52
63
  tempFolder = null;
53
64
  });
65
+ maybeIt(features.failureModes.downloadError)("a failed tarball download must show an error snackbar, not silence", () => {
66
+ cy.intercept("GET", "**/*.tgz*", {
67
+ statusCode: 500,
68
+ body: "boom"
69
+ }).as("tarball");
70
+ cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);
71
+ cy.getByTestId(pkg.sidebar, { timeout: 1e4 }).should("be.visible");
72
+ cy.getByTestId(pkg.downloadTarballBtn).click();
73
+ cy.wait("@tarball", { timeout: 1e4 });
74
+ cy.contains("The tarball could not be downloaded", { timeout: 1e4 }).should("be.visible");
75
+ });
54
76
  maybeIt(features.failureModes.detailErrorState)("a 5xx on the detail data must render an error state, not a blank page", () => {
55
77
  cy.intercept("GET", `/-/verdaccio/data/sidebar/${pkgName}*`, {
56
78
  statusCode: 500,
@@ -1 +1 @@
1
- {"version":3,"file":"failure-modes.js","names":[],"sources":["../../../src/tests/failure-modes.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Failure-mode tests: what the UI shows when the backend misbehaves.\n *\n * A registry that is down must never look like a healthy empty registry\n * (that invites the user to publish into the void), and a failed detail\n * request must render an error state instead of a blank page. These are\n * the failure classes fixed by verdaccio/verdaccio#6210 — the strict\n * assertions are feature-gated probes until that ships.\n */\nexport function failureModeTests(config: RegistryConfig) {\n const { features } = config;\n const { home, header, errors, package: pkg } = config.testIds;\n\n describe('failure modes', () => {\n maybeIt(features.failureModes.homeServerError)(\n 'a 5xx on the package list must not render the empty-registry onboarding',\n () => {\n cy.intercept('GET', '/-/verdaccio/data/packages', {\n statusCode: 500,\n body: { error: 'internal server error' },\n }).as('pkgs');\n\n cy.visit(config.registryUrl);\n cy.wait('@pkgs', { timeout: 10000 });\n\n // The header must survive and the onboarding card must not\n // appear — a dead backend is not an empty registry.\n cy.getByTestId(header.container).should('be.visible');\n cy.getByTestId(home.helpCard).should('not.exist');\n }\n );\n\n maybeIt(features.failureModes.homeNetworkError)(\n 'an unreachable backend must not render the empty-registry onboarding',\n () => {\n cy.intercept('GET', '/-/verdaccio/data/packages', { forceNetworkError: true }).as('pkgs');\n\n cy.visit(config.registryUrl);\n cy.wait('@pkgs', { timeout: 10000 });\n\n cy.getByTestId(header.container).should('be.visible');\n cy.getByTestId(home.helpCard).should('not.exist');\n cy.getByTestId(errors.genericError, { timeout: 10000 }).should('be.visible');\n }\n );\n\n describe('with a published package', () => {\n const pkgName = '@verdaccio/failure-fixture';\n let tempFolder: string | null = null;\n\n beforeEach(() => {\n cy.task('publishPackage', {\n pkgName,\n version: '1.0.0',\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n });\n });\n\n afterEach(() => {\n cy.task('unpublishPackage', {\n pkgName,\n tempFolder: tempFolder ?? undefined,\n });\n if (tempFolder) {\n cy.task('cleanupPublished', tempFolder);\n }\n tempFolder = null;\n });\n\n maybeIt(features.failureModes.detailErrorState)(\n 'a 5xx on the detail data must render an error state, not a blank page',\n () => {\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}*`, {\n statusCode: 500,\n body: { error: 'internal server error' },\n }).as('sidebar');\n cy.intercept('GET', `/-/verdaccio/data/package/readme/${pkgName}*`, {\n statusCode: 500,\n body: { error: 'internal server error' },\n }).as('readme');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);\n cy.wait('@sidebar', { timeout: 10000 });\n\n // Error page instead of a blank layout whose tabs crash the\n // whole app; the header must survive.\n cy.getByTestId(errors.genericError, { timeout: 10000 }).should('be.visible');\n cy.getByTestId(header.container).should('be.visible');\n cy.getByTestId(pkg.sidebar).should('not.exist');\n }\n );\n });\n });\n}\n"],"mappings":";;;;;;;;;;;AAaA,SAAgB,iBAAiB,QAAwB;CACvD,MAAM,EAAE,aAAa;CACrB,MAAM,EAAE,MAAM,QAAQ,QAAQ,SAAS,QAAQ,OAAO;CAEtD,SAAS,uBAAuB;EAC9B,QAAQ,SAAS,aAAa,eAAe,CAAC,CAC5C,iFACM;GACJ,GAAG,UAAU,OAAO,8BAA8B;IAChD,YAAY;IACZ,MAAM,EAAE,OAAO,wBAAwB;GACzC,CAAC,CAAC,CAAC,GAAG,MAAM;GAEZ,GAAG,MAAM,OAAO,WAAW;GAC3B,GAAG,KAAK,SAAS,EAAE,SAAS,IAAM,CAAC;GAInC,GAAG,YAAY,OAAO,SAAS,CAAC,CAAC,OAAO,YAAY;GACpD,GAAG,YAAY,KAAK,QAAQ,CAAC,CAAC,OAAO,WAAW;EAClD,CACF;EAEA,QAAQ,SAAS,aAAa,gBAAgB,CAAC,CAC7C,8EACM;GACJ,GAAG,UAAU,OAAO,8BAA8B,EAAE,mBAAmB,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM;GAExF,GAAG,MAAM,OAAO,WAAW;GAC3B,GAAG,KAAK,SAAS,EAAE,SAAS,IAAM,CAAC;GAEnC,GAAG,YAAY,OAAO,SAAS,CAAC,CAAC,OAAO,YAAY;GACpD,GAAG,YAAY,KAAK,QAAQ,CAAC,CAAC,OAAO,WAAW;GAChD,GAAG,YAAY,OAAO,cAAc,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,OAAO,YAAY;EAC7E,CACF;EAEA,SAAS,kCAAkC;GACzC,MAAM,UAAU;GAChB,IAAI,aAA4B;GAEhC,iBAAiB;IACf,GAAG,KAAK,kBAAkB;KACxB;KACA,SAAS;KACT,QAAQ;IACV,CAAC,CAAC,CAAC,MAAM,WAAW;KAClB,aAAa,QAAQ,cAAc;IACrC,CAAC;GACH,CAAC;GAED,gBAAgB;IACd,GAAG,KAAK,oBAAoB;KAC1B;KACA,YAAY,cAAc,KAAA;IAC5B,CAAC;IACD,IAAI,YACF,GAAG,KAAK,oBAAoB,UAAU;IAExC,aAAa;GACf,CAAC;GAED,QAAQ,SAAS,aAAa,gBAAgB,CAAC,CAC7C,+EACM;IACJ,GAAG,UAAU,OAAO,6BAA6B,QAAQ,IAAI;KAC3D,YAAY;KACZ,MAAM,EAAE,OAAO,wBAAwB;IACzC,CAAC,CAAC,CAAC,GAAG,SAAS;IACf,GAAG,UAAU,OAAO,oCAAoC,QAAQ,IAAI;KAClE,YAAY;KACZ,MAAM,EAAE,OAAO,wBAAwB;IACzC,CAAC,CAAC,CAAC,GAAG,QAAQ;IAEd,GAAG,MAAM,GAAG,OAAO,YAAY,gBAAgB,SAAS;IACxD,GAAG,KAAK,YAAY,EAAE,SAAS,IAAM,CAAC;IAItC,GAAG,YAAY,OAAO,cAAc,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,OAAO,YAAY;IAC3E,GAAG,YAAY,OAAO,SAAS,CAAC,CAAC,OAAO,YAAY;IACpD,GAAG,YAAY,IAAI,OAAO,CAAC,CAAC,OAAO,WAAW;GAChD,CACF;EACF,CAAC;CACH,CAAC;AACH"}
1
+ {"version":3,"file":"failure-modes.js","names":[],"sources":["../../../src/tests/failure-modes.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Failure-mode tests: what the UI shows when the backend misbehaves.\n *\n * A registry that is down must never look like a healthy empty registry\n * (that invites the user to publish into the void), and a failed detail\n * request must render an error state instead of a blank page. These are\n * the failure classes fixed by verdaccio/verdaccio#6210 — the strict\n * assertions are feature-gated probes until that ships.\n */\nexport function failureModeTests(config: RegistryConfig) {\n const { features } = config;\n const { home, header, errors, package: pkg } = config.testIds;\n\n describe('failure modes', () => {\n maybeIt(features.failureModes.searchError)(\n 'a search 5xx must show an error, not \"no results found\"',\n () => {\n cy.intercept('GET', '**/-/verdaccio/data/search/**', {\n statusCode: 500,\n body: { error: 'internal server error' },\n }).as('search');\n\n cy.visit(config.registryUrl);\n cy.getByTestId(header.searchContainer).find('input').type('anything', { delay: 20 });\n cy.wait('@search', { timeout: 10000 });\n\n // en-US bundle: autoComplete.error\n cy.contains('Something went wrong while searching', { timeout: 10000 }).should(\n 'be.visible'\n );\n cy.contains('No results found').should('not.exist');\n }\n );\n\n maybeIt(features.failureModes.homeServerError)(\n 'a 5xx on the package list must not render the empty-registry onboarding',\n () => {\n cy.intercept('GET', '/-/verdaccio/data/packages', {\n statusCode: 500,\n body: { error: 'internal server error' },\n }).as('pkgs');\n\n cy.visit(config.registryUrl);\n cy.wait('@pkgs', { timeout: 10000 });\n\n // The header must survive and the onboarding card must not\n // appear — a dead backend is not an empty registry.\n cy.getByTestId(header.container).should('be.visible');\n cy.getByTestId(home.helpCard).should('not.exist');\n }\n );\n\n maybeIt(features.failureModes.homeNetworkError)(\n 'an unreachable backend must not render the empty-registry onboarding',\n () => {\n cy.intercept('GET', '/-/verdaccio/data/packages', { forceNetworkError: true }).as('pkgs');\n\n cy.visit(config.registryUrl);\n cy.wait('@pkgs', { timeout: 10000 });\n\n cy.getByTestId(header.container).should('be.visible');\n cy.getByTestId(home.helpCard).should('not.exist');\n cy.getByTestId(errors.genericError, { timeout: 10000 }).should('be.visible');\n }\n );\n\n describe('with a published package', () => {\n const pkgName = '@verdaccio/failure-fixture';\n let tempFolder: string | null = null;\n\n beforeEach(() => {\n cy.task('publishPackage', {\n pkgName,\n version: '1.0.0',\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n });\n });\n\n afterEach(() => {\n cy.task('unpublishPackage', {\n pkgName,\n tempFolder: tempFolder ?? undefined,\n });\n if (tempFolder) {\n cy.task('cleanupPublished', tempFolder);\n }\n tempFolder = null;\n });\n\n maybeIt(features.failureModes.downloadError)(\n 'a failed tarball download must show an error snackbar, not silence',\n () => {\n cy.intercept('GET', '**/*.tgz*', {\n statusCode: 500,\n body: 'boom',\n }).as('tarball');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);\n cy.getByTestId(pkg.sidebar, { timeout: 10000 }).should('be.visible');\n cy.getByTestId(pkg.downloadTarballBtn).click();\n cy.wait('@tarball', { timeout: 10000 });\n\n // en-US bundle: error.download-tarball\n cy.contains('The tarball could not be downloaded', { timeout: 10000 }).should(\n 'be.visible'\n );\n }\n );\n\n maybeIt(features.failureModes.detailErrorState)(\n 'a 5xx on the detail data must render an error state, not a blank page',\n () => {\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}*`, {\n statusCode: 500,\n body: { error: 'internal server error' },\n }).as('sidebar');\n cy.intercept('GET', `/-/verdaccio/data/package/readme/${pkgName}*`, {\n statusCode: 500,\n body: { error: 'internal server error' },\n }).as('readme');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);\n cy.wait('@sidebar', { timeout: 10000 });\n\n // Error page instead of a blank layout whose tabs crash the\n // whole app; the header must survive.\n cy.getByTestId(errors.genericError, { timeout: 10000 }).should('be.visible');\n cy.getByTestId(header.container).should('be.visible');\n cy.getByTestId(pkg.sidebar).should('not.exist');\n }\n );\n });\n });\n}\n"],"mappings":"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"}
@@ -9,4 +9,6 @@ import { detailTests } from "./detail.js";
9
9
  import { failureModeTests } from "./failure-modes.js";
10
10
  import { i18nKeyTests } from "./i18n-keys.js";
11
11
  import { signupTests } from "./signup.js";
12
- export { changePasswordTests, detailTests, failureModeTests, homeTests, i18nKeyTests, layoutTests, publishTests, searchTests, settingsTests, signinTests, signupTests };
12
+ import { manifestRenderingTests } from "./manifest-rendering.js";
13
+ import { sessionTests } from "./session.js";
14
+ export { changePasswordTests, detailTests, failureModeTests, homeTests, i18nKeyTests, layoutTests, manifestRenderingTests, publishTests, searchTests, sessionTests, settingsTests, signinTests, signupTests };
@@ -0,0 +1,73 @@
1
+ import { maybeIt } from "../features.js";
2
+ //#region src/tests/manifest-rendering.ts
3
+ /**
4
+ * Manifest-shape rendering tests.
5
+ *
6
+ * npm does not validate these fields on publish: `repository`, `funding`
7
+ * and `bugs` are all valid in string form, `funding` also as an array,
8
+ * and contributors commonly ship without an email. Real packuments carry
9
+ * all of these — the UI must render them instead of silently dropping
10
+ * whole sections (the bug family fixed in verdaccio/verdaccio#6210,
11
+ * where every probe here failed).
12
+ *
13
+ * Requires the `manifest` override support in the publishPackage task.
14
+ */
15
+ function manifestRenderingTests(config) {
16
+ const { features } = config;
17
+ const { package: pkg } = config.testIds;
18
+ describe("manifest rendering", () => {
19
+ const pkgName = "@verdaccio/manifest-fixture";
20
+ let tempFolder = null;
21
+ const publishWith = (manifest) => cy.task("publishPackage", {
22
+ pkgName,
23
+ version: "1.0.0",
24
+ unique: true,
25
+ manifest
26
+ }).then((result) => {
27
+ tempFolder = result?.tempFolder ?? null;
28
+ });
29
+ afterEach(() => {
30
+ cy.task("unpublishPackage", {
31
+ pkgName,
32
+ tempFolder: tempFolder ?? void 0
33
+ });
34
+ if (tempFolder) cy.task("cleanupPublished", tempFolder);
35
+ tempFolder = null;
36
+ });
37
+ maybeIt(features.manifestRendering.stringForms)("string repository, funding array and string bugs must all render", () => {
38
+ publishWith({
39
+ repository: "git://github.com/verdaccio/verdaccio.git",
40
+ funding: [{
41
+ type: "opencollective",
42
+ url: "https://opencollective.com/verdaccio"
43
+ }],
44
+ bugs: "https://github.com/verdaccio/verdaccio/issues"
45
+ });
46
+ cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);
47
+ cy.getByTestId(pkg.sidebar, { timeout: 1e4 }).should("be.visible");
48
+ cy.getByTestId(pkg.sidebar).find("a[href^=\"https://github.com/verdaccio/verdaccio\"]").should("be.visible");
49
+ cy.getByTestId(pkg.sidebar).find("a[href=\"https://opencollective.com/verdaccio\"]").should("exist");
50
+ cy.get("a[href=\"https://github.com/verdaccio/verdaccio/issues\"]").should("exist");
51
+ });
52
+ maybeIt(features.manifestRendering.gravatarAvatars)("maintainer emails must render gravatar avatars on the sidebar", () => {
53
+ publishWith({ contributors: [{
54
+ name: "Ava Tester",
55
+ email: "ava@example.com"
56
+ }] });
57
+ cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);
58
+ cy.getByTestId(pkg.sidebar, { timeout: 1e4 }).should("be.visible");
59
+ cy.getByTestId(pkg.sidebar).find("img[src*=\"gravatar.com/avatar\"]").should("have.length.at.least", 1);
60
+ });
61
+ maybeIt(features.manifestRendering.developersWithoutEmail)("contributors without email must not collapse into one", () => {
62
+ publishWith({ contributors: [{ name: "alice-noemail" }, { name: "bob-noemail" }] });
63
+ cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);
64
+ cy.getByTestId(pkg.sidebar, { timeout: 1e4 }).should("be.visible");
65
+ cy.getByTestId("alice-noemail").should("exist");
66
+ cy.getByTestId("bob-noemail").should("exist");
67
+ });
68
+ });
69
+ }
70
+ //#endregion
71
+ export { manifestRenderingTests };
72
+
73
+ //# sourceMappingURL=manifest-rendering.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-rendering.js","names":[],"sources":["../../../src/tests/manifest-rendering.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Manifest-shape rendering tests.\n *\n * npm does not validate these fields on publish: `repository`, `funding`\n * and `bugs` are all valid in string form, `funding` also as an array,\n * and contributors commonly ship without an email. Real packuments carry\n * all of these — the UI must render them instead of silently dropping\n * whole sections (the bug family fixed in verdaccio/verdaccio#6210,\n * where every probe here failed).\n *\n * Requires the `manifest` override support in the publishPackage task.\n */\nexport function manifestRenderingTests(config: RegistryConfig) {\n const { features } = config;\n const { package: pkg } = config.testIds;\n\n describe('manifest rendering', () => {\n const pkgName = '@verdaccio/manifest-fixture';\n let tempFolder: string | null = null;\n\n const publishWith = (manifest: Record<string, unknown>) =>\n cy\n .task('publishPackage', {\n pkgName,\n version: '1.0.0',\n unique: true,\n manifest,\n })\n .then((result) => {\n tempFolder = result?.tempFolder ?? null;\n });\n\n afterEach(() => {\n cy.task('unpublishPackage', {\n pkgName,\n tempFolder: tempFolder ?? undefined,\n });\n if (tempFolder) {\n cy.task('cleanupPublished', tempFolder);\n }\n tempFolder = null;\n });\n\n maybeIt(features.manifestRendering.stringForms)(\n 'string repository, funding array and string bugs must all render',\n () => {\n // NOTE: the npm CLI normalizes string `repository`/`bugs` into their\n // object forms on publish, so what reaches the registry here is the\n // normalized shape with the git:// protocol kept. The RAW string\n // forms only arrive via uplink packuments and stay pinned by the\n // ui-components unit tests; this spec pins what survives publish:\n // the git:// -> https rewrite, the funding array and the bugs url.\n publishWith({\n repository: 'git://github.com/verdaccio/verdaccio.git',\n funding: [{ type: 'opencollective', url: 'https://opencollective.com/verdaccio' }],\n bugs: 'https://github.com/verdaccio/verdaccio/issues',\n });\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);\n cy.getByTestId(pkg.sidebar, { timeout: 10000 }).should('be.visible');\n\n // repository: rendered, rewritten to a browsable https url\n // (prefix match: normalization may or may not keep the .git suffix)\n cy.getByTestId(pkg.sidebar)\n .find('a[href^=\"https://github.com/verdaccio/verdaccio\"]')\n .should('be.visible');\n // funding (array form): the fund button links to the first entry\n cy.getByTestId(pkg.sidebar)\n .find('a[href=\"https://opencollective.com/verdaccio\"]')\n .should('exist');\n // bugs (string form): open-an-issue action links to it\n cy.get('a[href=\"https://github.com/verdaccio/verdaccio/issues\"]').should('exist');\n }\n );\n\n maybeIt(features.manifestRendering.gravatarAvatars)(\n 'maintainer emails must render gravatar avatars on the sidebar',\n () => {\n publishWith({\n contributors: [{ name: 'Ava Tester', email: 'ava@example.com' }],\n });\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);\n cy.getByTestId(pkg.sidebar, { timeout: 10000 }).should('be.visible');\n\n // the sidebar endpoint computes the gravatar url server-side;\n // the avatar <img> must actually carry it\n cy.getByTestId(pkg.sidebar)\n .find('img[src*=\"gravatar.com/avatar\"]')\n .should('have.length.at.least', 1);\n }\n );\n\n maybeIt(features.manifestRendering.developersWithoutEmail)(\n 'contributors without email must not collapse into one',\n () => {\n publishWith({\n contributors: [{ name: 'alice-noemail' }, { name: 'bob-noemail' }],\n });\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);\n cy.getByTestId(pkg.sidebar, { timeout: 10000 }).should('be.visible');\n\n // each Person renders data-testid=<name>; both must survive dedupe\n cy.getByTestId('alice-noemail').should('exist');\n cy.getByTestId('bob-noemail').should('exist');\n }\n );\n });\n}\n"],"mappings":";;;;;;;;;;;;;;AAgBA,SAAgB,uBAAuB,QAAwB;CAC7D,MAAM,EAAE,aAAa;CACrB,MAAM,EAAE,SAAS,QAAQ,OAAO;CAEhC,SAAS,4BAA4B;EACnC,MAAM,UAAU;EAChB,IAAI,aAA4B;EAEhC,MAAM,eAAe,aACnB,GACG,KAAK,kBAAkB;GACtB;GACA,SAAS;GACT,QAAQ;GACR;EACF,CAAC,CAAC,CACD,MAAM,WAAW;GAChB,aAAa,QAAQ,cAAc;EACrC,CAAC;EAEL,gBAAgB;GACd,GAAG,KAAK,oBAAoB;IAC1B;IACA,YAAY,cAAc,KAAA;GAC5B,CAAC;GACD,IAAI,YACF,GAAG,KAAK,oBAAoB,UAAU;GAExC,aAAa;EACf,CAAC;EAED,QAAQ,SAAS,kBAAkB,WAAW,CAAC,CAC7C,0EACM;GAOJ,YAAY;IACV,YAAY;IACZ,SAAS,CAAC;KAAE,MAAM;KAAkB,KAAK;IAAuC,CAAC;IACjF,MAAM;GACR,CAAC;GAED,GAAG,MAAM,GAAG,OAAO,YAAY,gBAAgB,SAAS;GACxD,GAAG,YAAY,IAAI,SAAS,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,OAAO,YAAY;GAInE,GAAG,YAAY,IAAI,OAAO,CAAC,CACxB,KAAK,qDAAmD,CAAC,CACzD,OAAO,YAAY;GAEtB,GAAG,YAAY,IAAI,OAAO,CAAC,CACxB,KAAK,kDAAgD,CAAC,CACtD,OAAO,OAAO;GAEjB,GAAG,IAAI,2DAAyD,CAAC,CAAC,OAAO,OAAO;EAClF,CACF;EAEA,QAAQ,SAAS,kBAAkB,eAAe,CAAC,CACjD,uEACM;GACJ,YAAY,EACV,cAAc,CAAC;IAAE,MAAM;IAAc,OAAO;GAAkB,CAAC,EACjE,CAAC;GAED,GAAG,MAAM,GAAG,OAAO,YAAY,gBAAgB,SAAS;GACxD,GAAG,YAAY,IAAI,SAAS,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,OAAO,YAAY;GAInE,GAAG,YAAY,IAAI,OAAO,CAAC,CACxB,KAAK,mCAAiC,CAAC,CACvC,OAAO,wBAAwB,CAAC;EACrC,CACF;EAEA,QAAQ,SAAS,kBAAkB,sBAAsB,CAAC,CACxD,+DACM;GACJ,YAAY,EACV,cAAc,CAAC,EAAE,MAAM,gBAAgB,GAAG,EAAE,MAAM,cAAc,CAAC,EACnE,CAAC;GAED,GAAG,MAAM,GAAG,OAAO,YAAY,gBAAgB,SAAS;GACxD,GAAG,YAAY,IAAI,SAAS,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,OAAO,YAAY;GAGnE,GAAG,YAAY,eAAe,CAAC,CAAC,OAAO,OAAO;GAC9C,GAAG,YAAY,aAAa,CAAC,CAAC,OAAO,OAAO;EAC9C,CACF;CACF,CAAC;AACH"}
@@ -0,0 +1,49 @@
1
+ import { maybeIt } from "../features.js";
2
+ //#region src/tests/session.ts
3
+ /**
4
+ * Session lifecycle tests around the token stored in localStorage.
5
+ *
6
+ * A JWT that expired must never leave the UI half-logged-in: the header
7
+ * greeting a user whose token the server treats as anonymous was the
8
+ * "private packages vanish but I look logged in" bug fixed in
9
+ * verdaccio/verdaccio#6210.
10
+ */
11
+ /** Unsigned JWT-shaped token with the given exp (seconds since epoch). */
12
+ function fakeJwt(exp) {
13
+ const b64 = (obj) => btoa(JSON.stringify(obj)).replace(/=+$/, "");
14
+ return `${b64({
15
+ alg: "none",
16
+ typ: "JWT"
17
+ })}.${b64({
18
+ name: "e2e-expired",
19
+ exp
20
+ })}.sig`;
21
+ }
22
+ function sessionTests(config) {
23
+ const { features } = config;
24
+ const { header } = config.testIds;
25
+ describe("session token lifecycle", () => {
26
+ const visitWithExpiredToken = () => {
27
+ const expired = fakeJwt(Math.floor(Date.now() / 1e3) - 3600);
28
+ cy.visit(config.registryUrl, { onBeforeLoad(win) {
29
+ win.localStorage.setItem("username", "e2e-expired");
30
+ win.localStorage.setItem("token", expired);
31
+ } });
32
+ cy.getByTestId(header.container).should("be.visible");
33
+ };
34
+ maybeIt(features.session.expiredTokenLoggedOut)("an expired stored token must boot the UI logged out", () => {
35
+ visitWithExpiredToken();
36
+ cy.getByTestId(header.loginButton).should("be.visible");
37
+ cy.getByTestId(header.logInDialogIcon).should("not.exist");
38
+ });
39
+ maybeIt(features.session.expiredTokenPurged)("an expired stored token must also be purged from localStorage", () => {
40
+ visitWithExpiredToken();
41
+ cy.getByTestId(header.loginButton).should("be.visible");
42
+ cy.window().its("localStorage").invoke("getItem", "token").should("be.null");
43
+ });
44
+ });
45
+ }
46
+ //#endregion
47
+ export { sessionTests };
48
+
49
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","names":[],"sources":["../../../src/tests/session.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Session lifecycle tests around the token stored in localStorage.\n *\n * A JWT that expired must never leave the UI half-logged-in: the header\n * greeting a user whose token the server treats as anonymous was the\n * \"private packages vanish but I look logged in\" bug fixed in\n * verdaccio/verdaccio#6210.\n */\n\n/** Unsigned JWT-shaped token with the given exp (seconds since epoch). */\nfunction fakeJwt(exp: number): string {\n const b64 = (obj: unknown) => btoa(JSON.stringify(obj)).replace(/=+$/, '');\n return `${b64({ alg: 'none', typ: 'JWT' })}.${b64({ name: 'e2e-expired', exp })}.sig`;\n}\n\nexport function sessionTests(config: RegistryConfig) {\n const { features } = config;\n const { header } = config.testIds;\n\n describe('session token lifecycle', () => {\n const visitWithExpiredToken = () => {\n const expired = fakeJwt(Math.floor(Date.now() / 1000) - 3600);\n cy.visit(config.registryUrl, {\n onBeforeLoad(win) {\n win.localStorage.setItem('username', 'e2e-expired');\n win.localStorage.setItem('token', expired);\n },\n });\n cy.getByTestId(header.container).should('be.visible');\n };\n\n maybeIt(features.session.expiredTokenLoggedOut)(\n 'an expired stored token must boot the UI logged out',\n () => {\n visitWithExpiredToken();\n\n // login button visible, no greeting for the stale user\n cy.getByTestId(header.loginButton).should('be.visible');\n cy.getByTestId(header.logInDialogIcon).should('not.exist');\n }\n );\n\n maybeIt(features.session.expiredTokenPurged)(\n 'an expired stored token must also be purged from localStorage',\n () => {\n visitWithExpiredToken();\n cy.getByTestId(header.loginButton).should('be.visible');\n\n // otherwise the api client keeps attaching the stale bearer and\n // the server quietly treats every request as anonymous\n cy.window().its('localStorage').invoke('getItem', 'token').should('be.null');\n }\n );\n });\n}\n"],"mappings":";;;;;;;;;;;AAcA,SAAS,QAAQ,KAAqB;CACpC,MAAM,OAAO,QAAiB,KAAK,KAAK,UAAU,GAAG,CAAC,CAAC,CAAC,QAAQ,OAAO,EAAE;CACzE,OAAO,GAAG,IAAI;EAAE,KAAK;EAAQ,KAAK;CAAM,CAAC,EAAE,GAAG,IAAI;EAAE,MAAM;EAAe;CAAI,CAAC,EAAE;AAClF;AAEA,SAAgB,aAAa,QAAwB;CACnD,MAAM,EAAE,aAAa;CACrB,MAAM,EAAE,WAAW,OAAO;CAE1B,SAAS,iCAAiC;EACxC,MAAM,8BAA8B;GAClC,MAAM,UAAU,QAAQ,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAAI,IAAI;GAC5D,GAAG,MAAM,OAAO,aAAa,EAC3B,aAAa,KAAK;IAChB,IAAI,aAAa,QAAQ,YAAY,aAAa;IAClD,IAAI,aAAa,QAAQ,SAAS,OAAO;GAC3C,EACF,CAAC;GACD,GAAG,YAAY,OAAO,SAAS,CAAC,CAAC,OAAO,YAAY;EACtD;EAEA,QAAQ,SAAS,QAAQ,qBAAqB,CAAC,CAC7C,6DACM;GACJ,sBAAsB;GAGtB,GAAG,YAAY,OAAO,WAAW,CAAC,CAAC,OAAO,YAAY;GACtD,GAAG,YAAY,OAAO,eAAe,CAAC,CAAC,OAAO,WAAW;EAC3D,CACF;EAEA,QAAQ,SAAS,QAAQ,kBAAkB,CAAC,CAC1C,uEACM;GACJ,sBAAsB;GACtB,GAAG,YAAY,OAAO,WAAW,CAAC,CAAC,OAAO,YAAY;GAItD,GAAG,OAAO,CAAC,CAAC,IAAI,cAAc,CAAC,CAAC,OAAO,WAAW,OAAO,CAAC,CAAC,OAAO,SAAS;EAC7E,CACF;CACF,CAAC;AACH"}
@@ -60,6 +60,34 @@ function settingsTests(config) {
60
60
  cy.get("body").type("{esc}");
61
61
  cy.get("[role=\"dialog\"]").should("not.exist");
62
62
  });
63
+ maybeIt(features.settings.localizedDates)("relative dates must follow the selected language (dayjs locale)", () => {
64
+ const pkgName = "@verdaccio/dates-fixture";
65
+ let tempFolder = null;
66
+ cy.task("publishPackage", {
67
+ pkgName,
68
+ version: "1.0.0",
69
+ unique: true
70
+ }).then((result) => {
71
+ tempFolder = result?.tempFolder ?? null;
72
+ });
73
+ cy.getByTestId(header.settingsTooltip).click();
74
+ cy.get("[role=\"dialog\"]").should("be.visible");
75
+ cy.contains("[role=\"dialog\"] [role=\"tab\"]", "Translations").click();
76
+ cy.contains("[role=\"dialog\"] .MuiCard-root", "German").click({ force: true });
77
+ cy.get("body").type("{esc}");
78
+ const sidebar = `[data-testid="${config.testIds.package.sidebar}"]`;
79
+ cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);
80
+ cy.get(sidebar, { timeout: 1e4 }).should("be.visible");
81
+ cy.get(sidebar).contains(/vor /);
82
+ cy.get(sidebar).contains(/ago\b/).should("not.exist");
83
+ cy.task("unpublishPackage", {
84
+ pkgName,
85
+ tempFolder: tempFolder ?? void 0
86
+ });
87
+ cy.then(() => {
88
+ if (tempFolder) cy.task("cleanupPublished", tempFolder);
89
+ });
90
+ });
63
91
  });
64
92
  }
65
93
  //#endregion
@@ -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}\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;CACH,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 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":"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"}
@@ -46,6 +46,13 @@ export interface Features {
46
46
  * builds that lag behind the upstream translation file.
47
47
  */
48
48
  languageSwitcher: boolean;
49
+ /**
50
+ * Whether to assert that dates follow the selected language
51
+ * (dayjs locale). Released UIs never load the dayjs locale, so
52
+ * relative dates stay English in every language (fixed in
53
+ * verdaccio/verdaccio#6210) — probe.
54
+ */
55
+ localizedDates: boolean;
49
56
  };
50
57
  signin: {
51
58
  /**
@@ -108,6 +115,47 @@ export interface Features {
108
115
  * `latest`, so this defaults to `false` as a probe.
109
116
  */
110
117
  versionNotFound: boolean;
118
+ /**
119
+ * Whether to run the "versions tab shows the CURRENT package after
120
+ * SPA navigation" test. The detail routes reuse one mounted
121
+ * component, and released UIs cache the previous package's versions
122
+ * in local state (fixed in verdaccio/verdaccio#6210) — defaults to
123
+ * `false` as a probe.
124
+ */
125
+ staleVersionsNavigation: boolean;
126
+ };
127
+ manifestRendering: {
128
+ /**
129
+ * Whether to run the string-form manifest tests: `repository`,
130
+ * `funding` and `bugs` in their string/array forms are valid npm
131
+ * and must render (fixed in verdaccio/verdaccio#6210) — probe.
132
+ */
133
+ stringForms: boolean;
134
+ /**
135
+ * Whether to assert gravatar avatars render on the detail sidebar.
136
+ * Released UIs read `avatar` while the sidebar endpoint sends
137
+ * `_avatar` (fixed in verdaccio/verdaccio#6210) — probe.
138
+ */
139
+ gravatarAvatars: boolean;
140
+ /**
141
+ * Whether to assert contributors without email don't collapse into
142
+ * one (fixed in verdaccio/verdaccio#6210) — probe.
143
+ */
144
+ developersWithoutEmail: boolean;
145
+ };
146
+ session: {
147
+ /**
148
+ * Whether to assert that an expired token in localStorage boots the
149
+ * UI logged out (header shows the login button). Holds on every
150
+ * released line.
151
+ */
152
+ expiredTokenLoggedOut: boolean;
153
+ /**
154
+ * Whether to assert the expired token is also PURGED from
155
+ * localStorage on boot — otherwise the api client keeps attaching
156
+ * it (fixed in verdaccio/verdaccio#6210) — probe.
157
+ */
158
+ expiredTokenPurged: boolean;
111
159
  };
112
160
  failureModes: {
113
161
  /**
@@ -131,6 +179,18 @@ export interface Features {
131
179
  * so this defaults to `false` as a probe.
132
180
  */
133
181
  detailErrorState: boolean;
182
+ /**
183
+ * Whether to run the "search 5xx shows an error, not 'no results
184
+ * found'" test. The error state ships with verdaccio/verdaccio#6210
185
+ * — probe.
186
+ */
187
+ searchError: boolean;
188
+ /**
189
+ * Whether to run the "failed tarball download shows an error
190
+ * snackbar" test — clicking download and having nothing happen sent
191
+ * users retrying blindly (fixed in verdaccio/verdaccio#6210) — probe.
192
+ */
193
+ downloadError: boolean;
134
194
  };
135
195
  i18n: {
136
196
  /**
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, signupTests, } from './tests';
9
+ export { homeTests, signinTests, publishTests, searchTests, settingsTests, layoutTests, changePasswordTests, detailTests, failureModeTests, i18nKeyTests, manifestRenderingTests, sessionTests, signupTests, } from './tests';
10
10
  /**
11
11
  * Build a full RegistryConfig from user-provided options with defaults.
12
12
  *
@@ -8,6 +8,15 @@ export interface PublishPackageInput {
8
8
  };
9
9
  dependencies?: Record<string, string>;
10
10
  devDependencies?: Record<string, string>;
11
+ /**
12
+ * Extra fields merged into the generated package.json AFTER the
13
+ * defaults, so fixtures can carry arbitrary manifest shapes: a string
14
+ * `repository`, a `funding` array, `contributors` without email, a
15
+ * non-string `homepage`… — exactly the field forms npm accepts on
16
+ * publish but UIs tend to mishandle. `name`, `version` and
17
+ * `publishConfig` always win over this override.
18
+ */
19
+ manifest?: Record<string, unknown>;
11
20
  /**
12
21
  * When true, append a timestamp suffix to the version so reruns against
13
22
  * a persistent registry don't collide on 403. The suffix is a valid
@@ -9,3 +9,5 @@ export { detailTests } from './detail';
9
9
  export { failureModeTests } from './failure-modes';
10
10
  export { i18nKeyTests } from './i18n-keys';
11
11
  export { signupTests } from './signup';
12
+ export { manifestRenderingTests } from './manifest-rendering';
13
+ export { sessionTests } from './session';
@@ -0,0 +1,14 @@
1
+ import { RegistryConfig } from '../types';
2
+ /**
3
+ * Manifest-shape rendering tests.
4
+ *
5
+ * npm does not validate these fields on publish: `repository`, `funding`
6
+ * and `bugs` are all valid in string form, `funding` also as an array,
7
+ * and contributors commonly ship without an email. Real packuments carry
8
+ * all of these — the UI must render them instead of silently dropping
9
+ * whole sections (the bug family fixed in verdaccio/verdaccio#6210,
10
+ * where every probe here failed).
11
+ *
12
+ * Requires the `manifest` override support in the publishPackage task.
13
+ */
14
+ export declare function manifestRenderingTests(config: RegistryConfig): void;
@@ -0,0 +1,2 @@
1
+ import { RegistryConfig } from '../types';
2
+ export declare function sessionTests(config: RegistryConfig): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verdaccio/e2e-ui",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Cypress plugin for Verdaccio UI e2e tests — run against any registry",