@verdaccio/e2e-ui 2.5.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 (62) hide show
  1. package/build/cjs/features.cjs +58 -1
  2. package/build/cjs/features.cjs.map +1 -1
  3. package/build/cjs/index.cjs +20 -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 +16 -1
  8. package/build/cjs/testIds.cjs.map +1 -1
  9. package/build/cjs/tests/change-password.cjs +1 -2
  10. package/build/cjs/tests/change-password.cjs.map +1 -1
  11. package/build/cjs/tests/detail.cjs +106 -0
  12. package/build/cjs/tests/detail.cjs.map +1 -0
  13. package/build/cjs/tests/failure-modes.cjs +97 -0
  14. package/build/cjs/tests/failure-modes.cjs.map +1 -0
  15. package/build/cjs/tests/i18n-keys.cjs +74 -0
  16. package/build/cjs/tests/i18n-keys.cjs.map +1 -0
  17. package/build/cjs/tests/index.cjs +26 -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/session.cjs +49 -0
  21. package/build/cjs/tests/session.cjs.map +1 -0
  22. package/build/cjs/tests/settings.cjs +28 -0
  23. package/build/cjs/tests/settings.cjs.map +1 -1
  24. package/build/cjs/tests/signup.cjs +56 -0
  25. package/build/cjs/tests/signup.cjs.map +1 -0
  26. package/build/esm/features.js +58 -1
  27. package/build/esm/features.js.map +1 -1
  28. package/build/esm/index.js +15 -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 +16 -1
  33. package/build/esm/testIds.js.map +1 -1
  34. package/build/esm/tests/change-password.js +1 -2
  35. package/build/esm/tests/change-password.js.map +1 -1
  36. package/build/esm/tests/detail.js +106 -0
  37. package/build/esm/tests/detail.js.map +1 -0
  38. package/build/esm/tests/failure-modes.js +97 -0
  39. package/build/esm/tests/failure-modes.js.map +1 -0
  40. package/build/esm/tests/i18n-keys.js +74 -0
  41. package/build/esm/tests/i18n-keys.js.map +1 -0
  42. package/build/esm/tests/index.js +14 -0
  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/session.js +49 -0
  46. package/build/esm/tests/session.js.map +1 -0
  47. package/build/esm/tests/settings.js +28 -0
  48. package/build/esm/tests/settings.js.map +1 -1
  49. package/build/esm/tests/signup.js +56 -0
  50. package/build/esm/tests/signup.js.map +1 -0
  51. package/build/features.d.ts +139 -0
  52. package/build/index.d.ts +1 -1
  53. package/build/tasks/publish.d.ts +9 -0
  54. package/build/testIds.d.ts +18 -0
  55. package/build/tests/detail.d.ts +12 -0
  56. package/build/tests/failure-modes.d.ts +11 -0
  57. package/build/tests/i18n-keys.d.ts +2 -0
  58. package/build/tests/index.d.ts +6 -0
  59. package/build/tests/manifest-rendering.d.ts +14 -0
  60. package/build/tests/session.d.ts +2 -0
  61. package/build/tests/signup.d.ts +12 -0
  62. package/package.json +1 -1
@@ -0,0 +1,106 @@
1
+ import { maybeIt } from "../features.js";
2
+ //#region src/tests/detail.ts
3
+ /**
4
+ * Package detail page tests: version-pinned URLs and the wire format of
5
+ * the UI's data requests.
6
+ *
7
+ * The wire format is an observable contract, not an implementation
8
+ * detail: proxies, intercepts and every npm client expect scoped names
9
+ * as literal `/@scope/name`. A ui-components refactor that
10
+ * percent-encoded the `@` broke exactly these suites
11
+ * (verdaccio/verdaccio#6210), which is why it is pinned here on purpose.
12
+ */
13
+ function detailTests(config) {
14
+ const { features } = config;
15
+ const { package: pkg, home } = config.testIds;
16
+ describe("package detail", () => {
17
+ describe("with a published package", () => {
18
+ const pkgName = "@verdaccio/detail-fixture";
19
+ let tempFolder = null;
20
+ let version = "1.0.0";
21
+ beforeEach(() => {
22
+ cy.task("publishPackage", {
23
+ pkgName,
24
+ version: "1.0.0",
25
+ unique: true
26
+ }).then((result) => {
27
+ tempFolder = result?.tempFolder ?? null;
28
+ version = result?.version ?? "1.0.0";
29
+ });
30
+ });
31
+ afterEach(() => {
32
+ cy.task("unpublishPackage", {
33
+ pkgName,
34
+ tempFolder: tempFolder ?? void 0
35
+ });
36
+ if (tempFolder) cy.task("cleanupPublished", tempFolder);
37
+ tempFolder = null;
38
+ });
39
+ maybeIt(features.detail.scopedWireFormat)("should request sidebar and readme with the scope literal (@, not %40)", () => {
40
+ cy.intercept("GET", `/-/verdaccio/data/sidebar/${pkgName}*`).as("sidebar");
41
+ cy.intercept("GET", `/-/verdaccio/data/package/readme/${pkgName}*`).as("readme");
42
+ cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);
43
+ cy.wait("@sidebar", { timeout: 1e4 }).then((interception) => {
44
+ expect(interception.request.url).to.contain(`/sidebar/${pkgName}`);
45
+ expect(interception.request.url).to.not.contain("%40");
46
+ });
47
+ cy.wait("@readme", { timeout: 1e4 });
48
+ cy.getByTestId(pkg.sidebar).should("be.visible");
49
+ });
50
+ maybeIt(features.detail.versionPinned)("should pin the requested version via the ?v= query on /v/<version> urls", () => {
51
+ cy.intercept("GET", `/-/verdaccio/data/sidebar/${pkgName}*`).as("sidebar");
52
+ cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}/v/${version}`);
53
+ cy.wait("@sidebar", { timeout: 1e4 }).then((interception) => {
54
+ expect(interception.request.url).to.contain(`v=${version}`);
55
+ });
56
+ cy.getByTestId(pkg.sidebar).should("be.visible");
57
+ cy.getByTestId(pkg.sidebar).contains(version);
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
+ });
92
+ maybeIt(features.detail.versionNotFound)("should render Not Found for a version that does not exist", () => {
93
+ cy.intercept("GET", `/-/verdaccio/data/sidebar/${pkgName}*`).as("sidebar");
94
+ cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}/v/9.9.9`);
95
+ cy.wait("@sidebar", { timeout: 1e4 }).then((interception) => {
96
+ expect(interception.response?.statusCode).to.eq(404);
97
+ });
98
+ cy.getByTestId(home.notFound, { timeout: 1e4 }).should("be.visible");
99
+ });
100
+ });
101
+ });
102
+ }
103
+ //#endregion
104
+ export { detailTests };
105
+
106
+ //# sourceMappingURL=detail.js.map
@@ -0,0 +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.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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"}
@@ -0,0 +1,97 @@
1
+ import { maybeIt } from "../features.js";
2
+ //#region src/tests/failure-modes.ts
3
+ /**
4
+ * Failure-mode tests: what the UI shows when the backend misbehaves.
5
+ *
6
+ * A registry that is down must never look like a healthy empty registry
7
+ * (that invites the user to publish into the void), and a failed detail
8
+ * request must render an error state instead of a blank page. These are
9
+ * the failure classes fixed by verdaccio/verdaccio#6210 — the strict
10
+ * assertions are feature-gated probes until that ships.
11
+ */
12
+ function failureModeTests(config) {
13
+ const { features } = config;
14
+ const { home, header, errors, package: pkg } = config.testIds;
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
+ });
27
+ maybeIt(features.failureModes.homeServerError)("a 5xx on the package list must not render the empty-registry onboarding", () => {
28
+ cy.intercept("GET", "/-/verdaccio/data/packages", {
29
+ statusCode: 500,
30
+ body: { error: "internal server error" }
31
+ }).as("pkgs");
32
+ cy.visit(config.registryUrl);
33
+ cy.wait("@pkgs", { timeout: 1e4 });
34
+ cy.getByTestId(header.container).should("be.visible");
35
+ cy.getByTestId(home.helpCard).should("not.exist");
36
+ });
37
+ maybeIt(features.failureModes.homeNetworkError)("an unreachable backend must not render the empty-registry onboarding", () => {
38
+ cy.intercept("GET", "/-/verdaccio/data/packages", { forceNetworkError: true }).as("pkgs");
39
+ cy.visit(config.registryUrl);
40
+ cy.wait("@pkgs", { timeout: 1e4 });
41
+ cy.getByTestId(header.container).should("be.visible");
42
+ cy.getByTestId(home.helpCard).should("not.exist");
43
+ cy.getByTestId(errors.genericError, { timeout: 1e4 }).should("be.visible");
44
+ });
45
+ describe("with a published package", () => {
46
+ const pkgName = "@verdaccio/failure-fixture";
47
+ let tempFolder = null;
48
+ beforeEach(() => {
49
+ cy.task("publishPackage", {
50
+ pkgName,
51
+ version: "1.0.0",
52
+ unique: true
53
+ }).then((result) => {
54
+ tempFolder = result?.tempFolder ?? null;
55
+ });
56
+ });
57
+ afterEach(() => {
58
+ cy.task("unpublishPackage", {
59
+ pkgName,
60
+ tempFolder: tempFolder ?? void 0
61
+ });
62
+ if (tempFolder) cy.task("cleanupPublished", tempFolder);
63
+ tempFolder = null;
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
+ });
76
+ maybeIt(features.failureModes.detailErrorState)("a 5xx on the detail data must render an error state, not a blank page", () => {
77
+ cy.intercept("GET", `/-/verdaccio/data/sidebar/${pkgName}*`, {
78
+ statusCode: 500,
79
+ body: { error: "internal server error" }
80
+ }).as("sidebar");
81
+ cy.intercept("GET", `/-/verdaccio/data/package/readme/${pkgName}*`, {
82
+ statusCode: 500,
83
+ body: { error: "internal server error" }
84
+ }).as("readme");
85
+ cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);
86
+ cy.wait("@sidebar", { timeout: 1e4 });
87
+ cy.getByTestId(errors.genericError, { timeout: 1e4 }).should("be.visible");
88
+ cy.getByTestId(header.container).should("be.visible");
89
+ cy.getByTestId(pkg.sidebar).should("not.exist");
90
+ });
91
+ });
92
+ });
93
+ }
94
+ //#endregion
95
+ export { failureModeTests };
96
+
97
+ //# sourceMappingURL=failure-modes.js.map
@@ -0,0 +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.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":";;;;;;;;;;;AAaA,SAAgB,iBAAiB,QAAwB;CACvD,MAAM,EAAE,aAAa;CACrB,MAAM,EAAE,MAAM,QAAQ,QAAQ,SAAS,QAAQ,OAAO;CAEtD,SAAS,uBAAuB;EAC9B,QAAQ,SAAS,aAAa,WAAW,CAAC,CACxC,mEACM;GACJ,GAAG,UAAU,OAAO,iCAAiC;IACnD,YAAY;IACZ,MAAM,EAAE,OAAO,wBAAwB;GACzC,CAAC,CAAC,CAAC,GAAG,QAAQ;GAEd,GAAG,MAAM,OAAO,WAAW;GAC3B,GAAG,YAAY,OAAO,eAAe,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,KAAK,YAAY,EAAE,OAAO,GAAG,CAAC;GACnF,GAAG,KAAK,WAAW,EAAE,SAAS,IAAM,CAAC;GAGrC,GAAG,SAAS,wCAAwC,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,OACtE,YACF;GACA,GAAG,SAAS,kBAAkB,CAAC,CAAC,OAAO,WAAW;EACpD,CACF;EAEA,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,aAAa,CAAC,CAC1C,4EACM;IACJ,GAAG,UAAU,OAAO,aAAa;KAC/B,YAAY;KACZ,MAAM;IACR,CAAC,CAAC,CAAC,GAAG,SAAS;IAEf,GAAG,MAAM,GAAG,OAAO,YAAY,gBAAgB,SAAS;IACxD,GAAG,YAAY,IAAI,SAAS,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,OAAO,YAAY;IACnE,GAAG,YAAY,IAAI,kBAAkB,CAAC,CAAC,MAAM;IAC7C,GAAG,KAAK,YAAY,EAAE,SAAS,IAAM,CAAC;IAGtC,GAAG,SAAS,uCAAuC,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,OACrE,YACF;GACF,CACF;GAEA,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"}
@@ -0,0 +1,74 @@
1
+ import { maybeIt } from "../features.js";
2
+ //#region src/tests/i18n-keys.ts
3
+ /**
4
+ * Raw-i18n-key detector: no page may render untranslated keys like
5
+ * `security.addUser.title` as visible text.
6
+ *
7
+ * This pins the whole "namespace missing from the shipped bundle" class
8
+ * (the `security.*` namespace was absent from every published ui-theme
9
+ * until verdaccio/verdaccio#6210): a missing key makes i18next return
10
+ * the key itself, which this scan catches regardless of which key
11
+ * regressed.
12
+ */
13
+ /**
14
+ * Namespaces of the ui-theme translation file. Restricting the scan to
15
+ * known prefixes keeps false positives out (package names like
16
+ * `lodash.merge` or readme prose with dots must not trip it).
17
+ */
18
+ var I18N_NAMESPACES = [
19
+ "security",
20
+ "dialog",
21
+ "header",
22
+ "sidebar",
23
+ "package",
24
+ "versions",
25
+ "search",
26
+ "form",
27
+ "form-placeholder",
28
+ "form-validation",
29
+ "button",
30
+ "error",
31
+ "uplinks",
32
+ "dependencies",
33
+ "autoComplete",
34
+ "footer",
35
+ "stage",
36
+ "help"
37
+ ].join("|");
38
+ var RAW_KEY_PATTERN = new RegExp(`(?:^|[\\s>])(?:${I18N_NAMESPACES})\\.[a-zA-Z][a-zA-Z.-]+`);
39
+ function assertNoRawI18nKeys(context) {
40
+ cy.get("body").invoke("text").then((text) => {
41
+ const match = text.match(RAW_KEY_PATTERN);
42
+ expect(match, `raw i18n key rendered on ${context}: ${match?.[0] ?? ""}`).to.be.null;
43
+ });
44
+ }
45
+ function i18nKeyTests(config) {
46
+ const { features } = config;
47
+ const { header } = config.testIds;
48
+ describe("i18n keys", () => {
49
+ maybeIt(features.i18n.noRawKeysCorePages)("home page must not render raw i18n keys", () => {
50
+ cy.visit(config.registryUrl);
51
+ cy.getByTestId(header.container).should("be.visible");
52
+ assertNoRawI18nKeys("home");
53
+ });
54
+ maybeIt(features.i18n.noRawKeysSecurityPages)("login dialog and security pages must not render raw i18n keys", () => {
55
+ cy.visit(config.registryUrl);
56
+ cy.getByTestId(config.testIds.header.loginButton).click();
57
+ cy.getByTestId(config.testIds.login.dialogContent).should("be.visible");
58
+ assertNoRawI18nKeys("login dialog");
59
+ for (const path of [
60
+ "/-/web/login",
61
+ "/-/web/add-user",
62
+ "/-/web/change-password"
63
+ ]) {
64
+ cy.visit(`${config.registryUrl}${path}`, { failOnStatusCode: false });
65
+ cy.get("body").should("be.visible");
66
+ assertNoRawI18nKeys(path);
67
+ }
68
+ });
69
+ });
70
+ }
71
+ //#endregion
72
+ export { i18nKeyTests };
73
+
74
+ //# sourceMappingURL=i18n-keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n-keys.js","names":[],"sources":["../../../src/tests/i18n-keys.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Raw-i18n-key detector: no page may render untranslated keys like\n * `security.addUser.title` as visible text.\n *\n * This pins the whole \"namespace missing from the shipped bundle\" class\n * (the `security.*` namespace was absent from every published ui-theme\n * until verdaccio/verdaccio#6210): a missing key makes i18next return\n * the key itself, which this scan catches regardless of which key\n * regressed.\n */\n\n/**\n * Namespaces of the ui-theme translation file. Restricting the scan to\n * known prefixes keeps false positives out (package names like\n * `lodash.merge` or readme prose with dots must not trip it).\n */\nconst I18N_NAMESPACES = [\n 'security',\n 'dialog',\n 'header',\n 'sidebar',\n 'package',\n 'versions',\n 'search',\n 'form',\n 'form-placeholder',\n 'form-validation',\n 'button',\n 'error',\n 'uplinks',\n 'dependencies',\n 'autoComplete',\n 'footer',\n 'stage',\n 'help',\n].join('|');\n\nconst RAW_KEY_PATTERN = new RegExp(`(?:^|[\\\\s>])(?:${I18N_NAMESPACES})\\\\.[a-zA-Z][a-zA-Z.-]+`);\n\nfunction assertNoRawI18nKeys(context: string) {\n cy.get('body')\n .invoke('text')\n .then((text: string) => {\n const match = text.match(RAW_KEY_PATTERN);\n expect(match, `raw i18n key rendered on ${context}: ${match?.[0] ?? ''}`).to.be.null;\n });\n}\n\nexport function i18nKeyTests(config: RegistryConfig) {\n const { features } = config;\n const { header } = config.testIds;\n\n describe('i18n keys', () => {\n maybeIt(features.i18n.noRawKeysCorePages)('home page must not render raw i18n keys', () => {\n cy.visit(config.registryUrl);\n cy.getByTestId(header.container).should('be.visible');\n assertNoRawI18nKeys('home');\n });\n\n maybeIt(features.i18n.noRawKeysSecurityPages)(\n 'login dialog and security pages must not render raw i18n keys',\n () => {\n // login dialog\n cy.visit(config.registryUrl);\n cy.getByTestId(config.testIds.header.loginButton).click();\n cy.getByTestId(config.testIds.login.dialogContent).should('be.visible');\n assertNoRawI18nKeys('login dialog');\n\n // standalone security pages (render regardless of the `next`\n // param except /-/web/login, which needs a valid one to show\n // the form — the NotFound fallback is translated too, so the\n // scan still applies)\n for (const path of ['/-/web/login', '/-/web/add-user', '/-/web/change-password']) {\n cy.visit(`${config.registryUrl}${path}`, { failOnStatusCode: false });\n cy.get('body').should('be.visible');\n assertNoRawI18nKeys(path);\n }\n }\n );\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAoBA,IAAM,kBAAkB;CACtB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG;AAEV,IAAM,kBAAkB,IAAI,OAAO,kBAAkB,gBAAgB,wBAAwB;AAE7F,SAAS,oBAAoB,SAAiB;CAC5C,GAAG,IAAI,MAAM,CAAC,CACX,OAAO,MAAM,CAAC,CACd,MAAM,SAAiB;EACtB,MAAM,QAAQ,KAAK,MAAM,eAAe;EACxC,OAAO,OAAO,4BAA4B,QAAQ,IAAI,QAAQ,MAAM,IAAI,CAAC,CAAC,GAAG,GAAG;CAClF,CAAC;AACL;AAEA,SAAgB,aAAa,QAAwB;CACnD,MAAM,EAAE,aAAa;CACrB,MAAM,EAAE,WAAW,OAAO;CAE1B,SAAS,mBAAmB;EAC1B,QAAQ,SAAS,KAAK,kBAAkB,CAAC,CAAC,iDAAiD;GACzF,GAAG,MAAM,OAAO,WAAW;GAC3B,GAAG,YAAY,OAAO,SAAS,CAAC,CAAC,OAAO,YAAY;GACpD,oBAAoB,MAAM;EAC5B,CAAC;EAED,QAAQ,SAAS,KAAK,sBAAsB,CAAC,CAC3C,uEACM;GAEJ,GAAG,MAAM,OAAO,WAAW;GAC3B,GAAG,YAAY,OAAO,QAAQ,OAAO,WAAW,CAAC,CAAC,MAAM;GACxD,GAAG,YAAY,OAAO,QAAQ,MAAM,aAAa,CAAC,CAAC,OAAO,YAAY;GACtE,oBAAoB,cAAc;GAMlC,KAAK,MAAM,QAAQ;IAAC;IAAgB;IAAmB;GAAwB,GAAG;IAChF,GAAG,MAAM,GAAG,OAAO,cAAc,QAAQ,EAAE,kBAAkB,MAAM,CAAC;IACpE,GAAG,IAAI,MAAM,CAAC,CAAC,OAAO,YAAY;IAClC,oBAAoB,IAAI;GAC1B;EACF,CACF;CACF,CAAC;AACH"}
@@ -0,0 +1,14 @@
1
+ import { homeTests } from "./home.js";
2
+ import { signinTests } from "./signin.js";
3
+ import { publishTests } from "./publish.js";
4
+ import { searchTests } from "./search.js";
5
+ import { settingsTests } from "./settings.js";
6
+ import { layoutTests } from "./layout.js";
7
+ import { changePasswordTests } from "./change-password.js";
8
+ import { detailTests } from "./detail.js";
9
+ import { failureModeTests } from "./failure-modes.js";
10
+ import { i18nKeyTests } from "./i18n-keys.js";
11
+ import { signupTests } from "./signup.js";
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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"}
@@ -0,0 +1,56 @@
1
+ import { maybeIt } from "../features.js";
2
+ //#region src/tests/signup.ts
3
+ /**
4
+ * Create-user (web signup) tests.
5
+ *
6
+ * Requires the server flag `createUser: true` (the /-/web/add-user page
7
+ * redirects home without it) and the fixed signup form from
8
+ * verdaccio/verdaccio#6210 — every published ui-theme posted to
9
+ * `PUT /-/web/add-user:<user>`, a URL that only serves the SPA, so the
10
+ * form could never succeed. That is exactly why this suite exists: the
11
+ * bug lived for months because no e2e ever clicked through the flow.
12
+ */
13
+ function signupTests(config) {
14
+ const { features } = config;
15
+ const { signup } = config.selectors;
16
+ describe("signup (create user)", () => {
17
+ beforeEach(() => {
18
+ cy.visit(`${config.registryUrl}/-/web/add-user`);
19
+ cy.get("body").should("be.visible");
20
+ });
21
+ maybeIt(features.signup.happyPath)("should create a user through the real signup endpoint and land on success", () => {
22
+ const username = `e2e-signup-${Date.now()}`;
23
+ cy.intercept("PUT", "/-/verdaccio/sec/signup").as("signup");
24
+ cy.get(signup.usernameInput).type(username);
25
+ cy.get(signup.passwordInput).type("e2e-password");
26
+ cy.get(signup.emailInput).type(`${username}@example.com`);
27
+ cy.get(signup.submitButton).should("not.be.disabled").click();
28
+ cy.wait("@signup", { timeout: 1e4 }).then((interception) => {
29
+ const body = interception.request.body;
30
+ expect(body.name).to.eq(username);
31
+ expect(body.email).to.eq(`${username}@example.com`);
32
+ expect(body.sessionId).to.be.a("string").with.length(36);
33
+ expect(interception.response?.statusCode).to.be.oneOf([200, 201]);
34
+ });
35
+ cy.location("pathname", { timeout: 1e4 }).should("contain", "/-/web/success");
36
+ });
37
+ maybeIt(features.signup.validation)("an invalid email must show a visible message and keep submit disabled", () => {
38
+ cy.get(signup.usernameInput).type("e2e-validation-user");
39
+ cy.get(signup.passwordInput).type("e2e-password");
40
+ cy.get(signup.emailInput).type("not-an-email");
41
+ cy.get(signup.submitButton).should("be.disabled");
42
+ cy.get(signup.emailInput).closest("div.MuiFormControl-root").find(".MuiFormHelperText-root").should("be.visible").and("not.be.empty");
43
+ });
44
+ maybeIt(features.signup.validation)("a username with spaces must block submit with a visible message", () => {
45
+ cy.get(signup.usernameInput).type("user name");
46
+ cy.get(signup.passwordInput).type("e2e-password");
47
+ cy.get(signup.emailInput).type("valid@example.com");
48
+ cy.get(signup.submitButton).should("be.disabled");
49
+ cy.get(signup.usernameInput).closest("div.MuiFormControl-root").find(".MuiFormHelperText-root").should("be.visible").and("not.be.empty");
50
+ });
51
+ });
52
+ }
53
+ //#endregion
54
+ export { signupTests };
55
+
56
+ //# sourceMappingURL=signup.js.map