@verdaccio/e2e-ui 2.6.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 (59) hide show
  1. package/build/cjs/features.cjs +40 -3
  2. package/build/cjs/features.cjs.map +1 -1
  3. package/build/cjs/index.cjs +10 -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/testIds.cjs +23 -1
  8. package/build/cjs/testIds.cjs.map +1 -1
  9. package/build/cjs/tests/change-password.cjs +20 -7
  10. package/build/cjs/tests/change-password.cjs.map +1 -1
  11. package/build/cjs/tests/detail.cjs +33 -0
  12. package/build/cjs/tests/detail.cjs.map +1 -1
  13. package/build/cjs/tests/failure-modes.cjs +22 -0
  14. package/build/cjs/tests/failure-modes.cjs.map +1 -1
  15. package/build/cjs/tests/home-detail-ux.cjs +166 -0
  16. package/build/cjs/tests/home-detail-ux.cjs.map +1 -0
  17. package/build/cjs/tests/index.cjs +6 -0
  18. package/build/cjs/tests/manifest-rendering.cjs +73 -0
  19. package/build/cjs/tests/manifest-rendering.cjs.map +1 -0
  20. package/build/cjs/tests/publish.cjs +1 -1
  21. package/build/cjs/tests/publish.cjs.map +1 -1
  22. package/build/cjs/tests/session.cjs +49 -0
  23. package/build/cjs/tests/session.cjs.map +1 -0
  24. package/build/cjs/tests/settings.cjs +33 -2
  25. package/build/cjs/tests/settings.cjs.map +1 -1
  26. package/build/esm/features.js +40 -3
  27. package/build/esm/features.js.map +1 -1
  28. package/build/esm/index.js +8 -1
  29. package/build/esm/index.js.map +1 -1
  30. package/build/esm/tasks/publish.js +5 -4
  31. package/build/esm/tasks/publish.js.map +1 -1
  32. package/build/esm/testIds.js +23 -1
  33. package/build/esm/testIds.js.map +1 -1
  34. package/build/esm/tests/change-password.js +20 -7
  35. package/build/esm/tests/change-password.js.map +1 -1
  36. package/build/esm/tests/detail.js +33 -0
  37. package/build/esm/tests/detail.js.map +1 -1
  38. package/build/esm/tests/failure-modes.js +22 -0
  39. package/build/esm/tests/failure-modes.js.map +1 -1
  40. package/build/esm/tests/home-detail-ux.js +166 -0
  41. package/build/esm/tests/home-detail-ux.js.map +1 -0
  42. package/build/esm/tests/index.js +4 -1
  43. package/build/esm/tests/manifest-rendering.js +73 -0
  44. package/build/esm/tests/manifest-rendering.js.map +1 -0
  45. package/build/esm/tests/publish.js +1 -1
  46. package/build/esm/tests/publish.js.map +1 -1
  47. package/build/esm/tests/session.js +49 -0
  48. package/build/esm/tests/session.js.map +1 -0
  49. package/build/esm/tests/settings.js +33 -2
  50. package/build/esm/tests/settings.js.map +1 -1
  51. package/build/features.d.ts +105 -0
  52. package/build/index.d.ts +1 -1
  53. package/build/tasks/publish.d.ts +9 -0
  54. package/build/testIds.d.ts +34 -0
  55. package/build/tests/home-detail-ux.d.ts +10 -0
  56. package/build/tests/index.d.ts +3 -0
  57. package/build/tests/manifest-rendering.d.ts +14 -0
  58. package/build/tests/session.d.ts +2 -0
  59. package/package.json +1 -1
@@ -9,4 +9,7 @@ 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
+ import { homeDetailUxTests } from "./home-detail-ux.js";
15
+ export { changePasswordTests, detailTests, failureModeTests, homeDetailUxTests, 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"}
@@ -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":";;AAIA,SAAgB,aAAa,QAAwB;CACnD,MAAM,EAAE,QAAQ,SAAS,QAAQ,OAAO;CACxC,MAAM,EAAE,cAAc,gBAAgB,OAAO;CAC7C,MAAM,EAAE,aAAa;;;;;;CAMrB,MAAM,aAAa,gBAAwB;EACzC,GAAG,QACD,CAAC,aAAa,OAAO,YAAY,IAAI,SAC/B;GACJ,GAAG,MAAM,OAAO,WAAW;GAC3B,GAAG,MAAM,OAAO,YAAY,MAAM,OAAO,YAAY,UAAU;IAC7D,aAAa,OAAO;IACpB,GAAG;GACL,CAAC;GACD,GAAG,KAAK,OAAO;EACjB,GACA;GACE,WAAW;IACT,GAAG,QAAQ;KACT,KAAK,GAAG,OAAO,YAAY;KAC3B,kBAAkB;IACpB,CAAC,CAAC,CACC,IAAI,QAAQ,CAAC,CACb,OAAO,YAAY,CAAC,KAAK,GAAG,CAAC;GAClC;GACA,kBAAkB;EACpB,CACF;CACF;CAEA,SAAS,iBAAiB;EACxB,MAAM,UAAU;EAIhB,MAAM,UAAU;EAChB,MAAM,aAAa;EAGnB,IAAI,aAA4B;EAEhC,iBAAiB;GACf,GAAG,UAAU,QAAQ,wBAAwB,CAAC,CAAC,GAAG,MAAM;GACxD,GAAG,UAAU,OAAO,4BAA4B,CAAC,CAAC,GAAG,MAAM;GAC3D,GAAG,UAAU,OAAO,6BAA6B,SAAS,CAAC,CAAC,GAAG,SAAS;GACxE,GAAG,UAAU,OAAO,oCAAoC,SAAS,CAAC,CAAC,GAAG,QAAQ;GAK9E,GAAG,KAAK,kBAAkB;IACxB;IACA,SAAS;IACT,cAAc,GAAG,UAAU,WAAW;IACtC,QAAQ;GACV,CAAC,CAAC,CAAC,MAAM,WAAW;IAClB,aAAa,QAAQ,cAAc;GACrC,CAAC;GAED,UAAU,eAAe;GACzB,GAAG,MAAM,OAAO,WAAW;EAC7B,CAAC;EAED,gBAAgB;GAKd,GAAG,KAAK,oBAAoB;IAAE;IAAS,YAAY,cAAc,KAAA;GAAU,CAAC;GAC5E,IAAI,YACF,GAAG,KAAK,oBAAoB,UAAU;GAExC,aAAa;EACf,CAAC;EAED,GAAG,2CAA2C;GAC5C,GAAG,KAAK,OAAO;GACf,GAAG,YAAY,IAAI,KAAK,CAAC,CAAC,OAAO,wBAAwB,CAAC;EAC5D,CAAC;EAED,GAAG,wCAAwC;GACzC,GAAG,KAAK,OAAO;GACf,GAAG,KAAK,GAAG;GACX,GAAG,YAAY,IAAI,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM;EAC1C,CAAC;EAED,GAAG,oCAAoC;GACrC,GAAG,KAAK,OAAO;GACf,GAAG,YAAY,IAAI,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM;GACxC,GAAG,KAAK,SAAS;GACjB,GAAG,KAAK,UAAU;GAClB,GAAG,YAAY,IAAI,MAAM,CAAC,CAAC,OAAO,YAAY;GAC9C,GAAG,IAAI,YAAY,CAAC,CAAC,OAAO,eAAe,CAAC;GAE5C,GAAG,SAAS,cAAc,MAAM;GAChC,GAAG,SAAS,GAAG,aAAa,MAAM,OAAO,CAAC,CAAC,OAAO,YAAY;EAChE,CAAC;EAED,GAAG,6EAA6E;GAC9E,GAAG,KAAK,OAAO;GACf,GAAG,YAAY,IAAI,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM;GACxC,GAAG,KAAK,UAAU;GAClB,GAAG,YAAY,IAAI,OAAO,CAAC,CAAC,OAAO,YAAY;GAC/C,GAAG,YAAY,IAAI,WAAW,CAAC,CAAC,aAAa;IAC3C,GAAG,YAAY,IAAI,UAAU,CAAC,CAAC,OAAO,YAAY;IAClD,GAAG,YAAY,IAAI,WAAW,CAAC,CAAC,OAAO,YAAY;IACnD,GAAG,YAAY,IAAI,WAAW,CAAC,CAAC,OAAO,YAAY;GACrD,CAAC;GACD,GAAG,YAAY,IAAI,UAAU,CAAC,CAAC,OAAO,gBAAgB,OAAO;EAC/D,CAAC;EAED,GAAG,wEAAwE;GACzE,GAAG,KAAK,OAAO;GACf,GAAG,YAAY,IAAI,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM;GACxC,GAAG,KAAK,UAAU;GAGlB,GAAG,YAAY,IAAI,WAAW,CAAC,CAAC,OAAO,YAAY;GACnD,GAAG,YAAY,IAAI,WAAW,CAAC,CAAC,OAAO,gBAAgB,WAAW;GAClE,GAAG,YAAY,IAAI,WAAW,CAAC,CAAC,OAAO,gBAAgB,KAAK;GAC5D,GAAG,YAAY,IAAI,WAAW,CAAC,CAAC,OAAO,gBAAgB,MAAM;EAC/D,CAAC;EAED,GAAG,0CAA0C;GAC3C,GAAG,KAAK,OAAO;GACf,GAAG,KAAK,GAAG;GACX,GAAG,YAAY,IAAI,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM;GACxC,GAAG,KAAK,SAAS;GACjB,GAAG,KAAK,UAAU;GAClB,GAAG,YAAY,IAAI,eAAe,CAAC,CAAC,MAAM;GAC1C,GAAG,KAAK,GAAG;GACX,GAAG,YAAY,IAAI,YAAY,CAAC,CAAC,OAAO,eAAe,CAAC;GAMxD,GAAG,YAAY,OAAO,CAAC,CACpB,OAAO,YAAY,CAAC,CACpB,IAAI,gBAAgB,OAAO,CAAC,CAC5B,IAAI,gBAAgB,UAAU;GAKjC,GAAG,YAAY,OAAO,CAAC,CACpB,OAAO,MAAM,CAAC,CACd,OAAO,SAAS,IAAI,OAAO,GAAG,QAAQ,WAAW,YAAY,CAAC;EACnE,CAAC;EAED,GAAG,sCAAsC;GACvC,GAAG,KAAK,OAAO;GACf,GAAG,KAAK,GAAG;GACX,GAAG,YAAY,IAAI,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM;GACxC,GAAG,KAAK,SAAS;GACjB,GAAG,KAAK,UAAU;GAClB,GAAG,YAAY,IAAI,WAAW,CAAC,CAAC,MAAM;GAGtC,GAAG,YAAY,IAAI,SAAS,CAAC,CAC1B,SAAS,CAAC,CACV,OAAO,MAAM,CAAC,CACd,OAAO,SAAS,SAAS;EAC9B,CAAC;EAED,GAAG,qCAAqC;GACtC,GAAG,KAAK,OAAO;GACf,GAAG,KAAK,GAAG;GACX,GAAG,YAAY,IAAI,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM;GACxC,GAAG,KAAK,SAAS;GACjB,GAAG,KAAK,UAAU;GAClB,GAAG,YAAY,IAAI,UAAU,CAAC,CAAC,MAAM;GACrC,GAAG,YAAY,IAAI,SAAS,CAAC,CAAC,OAAO,YAAY;EACnD,CAAC;EASD,QAAQ,SAAS,QAAQ,eAAe,CAAC,CACvC,sEACM;GAKJ,GAAG,UAAU,OAAO,qBAAqB,CAAC,CAAC,GAAG,cAAc;GAE5D,GAAG,KAAK,OAAO;GACf,GAAG,YAAY,IAAI,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM;GACxC,GAAG,KAAK,UAAU;GAElB,GAAG,YAAY,IAAI,kBAAkB,CAAC,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM;GAMlE,GAAG,KAAK,iBAAiB,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,IAAI,qBAAqB,CAAC,CAAC,OAAO,MAAM,GAAG;EAC1F,CACF;EAEA,QAAQ,SAAS,QAAQ,SAAS,CAAC,CACjC,4EACM;GACJ,GAAG,KAAK,OAAO;GACf,GAAG,YAAY,IAAI,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM;GACxC,GAAG,KAAK,UAAU;GAKlB,GAAG,YAAY,IAAI,MAAM,CAAC,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM;GAEtD,GAAG,YAAY,IAAI,iBAAiB,EAAE,SAAS,IAAK,CAAC,CAAC,CAAC,OAAO,YAAY;GAG1E,GAAG,YAAY,IAAI,eAAe,CAAC,CAAC,OAAO,gBAAgB,OAAO;GAIlE,GAAG,YAAY,IAAI,cAAc,CAAC,CAAC,MAAM;GACzC,GAAG,YAAY,IAAI,eAAe,CAAC,CAAC,OAAO,WAAW;EACxD,CACF;CACF,CAAC;CAED,SAAS,2CAA2C;EAClD,MAAM,iBAAiB;EACvB,IAAI,aAA4B;EAEhC,iBAAiB;GACf,GAAG,UAAU,QAAQ,wBAAwB,CAAC,CAAC,GAAG,MAAM;GACxD,GAAG,UAAU,OAAO,4BAA4B,CAAC,CAAC,GAAG,MAAM;GAC3D,GAAG,UAAU,OAAO,uDAAuD,CAAC,CAAC,GAAG,SAAS;GAEzF,GAAG,KAAK,kBAAkB;IACxB,SAAS;IACT,SAAS;IACT,QAAQ;GACV,CAAC,CAAC,CAAC,MAAM,WAAW;IAClB,aAAa,QAAQ,cAAc;GACrC,CAAC;GAED,UAAU,uBAAuB;GACjC,GAAG,MAAM,OAAO,WAAW;EAC7B,CAAC;EAED,gBAAgB;GACd,GAAG,KAAK,oBAAoB;IAC1B,SAAS;IACT,YAAY,cAAc,KAAA;GAC5B,CAAC;GACD,IAAI,YACF,GAAG,KAAK,oBAAoB,UAAU;GAExC,aAAa;EACf,CAAC;EAED,QAAQ,SAAS,QAAQ,sBAAsB,CAAC,CAC9C,mEACM;GACJ,GAAG,UAAU,OAAO,0BAA0B,CAAC,CAAC,GAAG,qBAAqB;GAExE,GAAG,KAAK,OAAO;GACf,GAAG,SAAS,iBAAiB,IAAI,MAAM,KAAK,cAAc,CAAC,CACxD,OAAO,YAAY,CAAC,CACpB,QAAQ,iBAAiB,IAAI,SAAS,GAAG,CAAC,CAC1C,KAAK,iBAAiB,IAAI,gBAAgB,GAAG,CAAC,CAC9C,OAAO,YAAY,CAAC,CACpB,MAAM;GAET,GAAG,KAAK,wBAAwB,EAAE,SAAS,IAAM,CAAC,CAAC,CAChD,IAAI,qBAAqB,CAAC,CAC1B,OAAO,MAAM,GAAG;EACrB,CACF;EAEA,QAAQ,SAAS,QAAQ,sBAAsB,CAAC,CAC9C,iEACM;GACJ,GAAG,UAAU,OAAO,0BAA0B,CAAC,CAAC,GAAG,qBAAqB;GAExE,GAAG,KAAK,OAAO;GACf,GAAG,SAAS,iBAAiB,IAAI,MAAM,KAAK,cAAc,CAAC,CAAC,MAAM;GAClE,GAAG,KAAK,UAAU;GAClB,GAAG,YAAY,IAAI,kBAAkB,CAAC,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM;GAElE,GAAG,KAAK,wBAAwB,EAAE,SAAS,IAAM,CAAC,CAAC,CAChD,IAAI,qBAAqB,CAAC,CAC1B,OAAO,MAAM,GAAG;EACrB,CACF;CACF,CAAC;AACH"}
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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"}
@@ -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"}
@@ -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
  });
@@ -60,6 +63,34 @@ function settingsTests(config) {
60
63
  cy.get("body").type("{esc}");
61
64
  cy.get("[role=\"dialog\"]").should("not.exist");
62
65
  });
66
+ maybeIt(features.settings.localizedDates)("relative dates must follow the selected language (dayjs locale)", () => {
67
+ const pkgName = "@verdaccio/dates-fixture";
68
+ let tempFolder = null;
69
+ cy.task("publishPackage", {
70
+ pkgName,
71
+ version: "1.0.0",
72
+ unique: true
73
+ }).then((result) => {
74
+ tempFolder = result?.tempFolder ?? null;
75
+ });
76
+ cy.getByTestId(header.settingsTooltip).click();
77
+ cy.get("[role=\"dialog\"]").should("be.visible");
78
+ cy.contains("[role=\"dialog\"] [role=\"tab\"]", "Translations").click();
79
+ cy.contains("[role=\"dialog\"] .MuiCard-root", "German").click({ force: true });
80
+ cy.get("body").type("{esc}");
81
+ const sidebar = `[data-testid="${config.testIds.package.sidebar}"]`;
82
+ cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);
83
+ cy.get(sidebar, { timeout: 1e4 }).should("be.visible");
84
+ cy.get(sidebar).contains(/vor /);
85
+ cy.get(sidebar).contains(/ago\b/).should("not.exist");
86
+ cy.task("unpublishPackage", {
87
+ pkgName,
88
+ tempFolder: tempFolder ?? void 0
89
+ });
90
+ cy.then(() => {
91
+ if (tempFolder) cy.task("cleanupPublished", tempFolder);
92
+ });
93
+ });
63
94
  });
64
95
  }
65
96
  //#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 // 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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"}
@@ -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
  /**
@@ -166,6 +226,51 @@ export interface Features {
166
226
  */
167
227
  validation: boolean;
168
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
+ };
169
274
  changePassword: {
170
275
  /**
171
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, 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
  *
@@ -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
@@ -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;
@@ -9,3 +9,6 @@ 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';
14
+ export { homeDetailUxTests } from './home-detail-ux';
@@ -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;