@verdaccio/e2e-ui 2.1.1 → 2.3.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 (57) hide show
  1. package/README.md +134 -0
  2. package/build/cjs/commands/index.cjs +21 -5
  3. package/build/cjs/commands/index.cjs.map +1 -1
  4. package/build/cjs/features.cjs +64 -0
  5. package/build/cjs/features.cjs.map +1 -0
  6. package/build/cjs/index.cjs +69 -10
  7. package/build/cjs/index.cjs.map +1 -1
  8. package/build/cjs/tasks/publish.cjs +245 -0
  9. package/build/cjs/tasks/publish.cjs.map +1 -0
  10. package/build/cjs/testIds.cjs +119 -0
  11. package/build/cjs/testIds.cjs.map +1 -0
  12. package/build/cjs/tests/home.cjs +63 -7
  13. package/build/cjs/tests/home.cjs.map +1 -1
  14. package/build/cjs/tests/layout.cjs +81 -0
  15. package/build/cjs/tests/layout.cjs.map +1 -0
  16. package/build/cjs/tests/publish.cjs +106 -33
  17. package/build/cjs/tests/publish.cjs.map +1 -1
  18. package/build/cjs/tests/search.cjs +122 -0
  19. package/build/cjs/tests/search.cjs.map +1 -0
  20. package/build/cjs/tests/settings.cjs +68 -0
  21. package/build/cjs/tests/settings.cjs.map +1 -0
  22. package/build/cjs/tests/signin.cjs +39 -9
  23. package/build/cjs/tests/signin.cjs.map +1 -1
  24. package/build/commands/index.d.ts +15 -3
  25. package/build/esm/commands/index.js +21 -5
  26. package/build/esm/commands/index.js.map +1 -1
  27. package/build/esm/features.js +62 -0
  28. package/build/esm/features.js.map +1 -0
  29. package/build/esm/index.js +57 -8
  30. package/build/esm/index.js.map +1 -1
  31. package/build/esm/tasks/publish.js +243 -0
  32. package/build/esm/tasks/publish.js.map +1 -0
  33. package/build/esm/testIds.js +116 -0
  34. package/build/esm/testIds.js.map +1 -0
  35. package/build/esm/tests/home.js +63 -7
  36. package/build/esm/tests/home.js.map +1 -1
  37. package/build/esm/tests/layout.js +81 -0
  38. package/build/esm/tests/layout.js.map +1 -0
  39. package/build/esm/tests/publish.js +106 -33
  40. package/build/esm/tests/publish.js.map +1 -1
  41. package/build/esm/tests/search.js +122 -0
  42. package/build/esm/tests/search.js.map +1 -0
  43. package/build/esm/tests/settings.js +68 -0
  44. package/build/esm/tests/settings.js.map +1 -0
  45. package/build/esm/tests/signin.js +39 -9
  46. package/build/esm/tests/signin.js.map +1 -1
  47. package/build/features.d.ts +94 -0
  48. package/build/index.d.ts +19 -2
  49. package/build/tasks/index.d.ts +2 -0
  50. package/build/tasks/publish.d.ts +94 -0
  51. package/build/testIds.d.ts +158 -0
  52. package/build/tests/index.d.ts +3 -0
  53. package/build/tests/layout.d.ts +12 -0
  54. package/build/tests/search.d.ts +10 -0
  55. package/build/tests/settings.d.ts +19 -0
  56. package/build/types.d.ts +92 -0
  57. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # @verdaccio/e2e-ui
2
+
3
+ Reusable Cypress test suites for the Verdaccio web UI. Targets Verdaccio.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add -D @verdaccio/e2e-ui cypress
9
+ ```
10
+
11
+ ## Wire up
12
+
13
+ `cypress.config.ts`:
14
+
15
+ ```ts
16
+ import { defineConfig } from 'cypress';
17
+ import { setupVerdaccioTasks } from '@verdaccio/e2e-ui';
18
+
19
+ const registryUrl = process.env.VERDACCIO_URL || 'http://localhost:4873';
20
+
21
+ export default defineConfig({
22
+ e2e: {
23
+ baseUrl: registryUrl,
24
+ supportFile: 'cypress/support/e2e.ts',
25
+ specPattern: 'cypress/e2e/**/*.cy.ts',
26
+ setupNodeEvents(on) {
27
+ setupVerdaccioTasks(on, { registryUrl });
28
+ },
29
+ },
30
+ env: { VERDACCIO_URL: registryUrl },
31
+ });
32
+ ```
33
+
34
+ `cypress/support/e2e.ts`:
35
+
36
+ ```ts
37
+ import '@verdaccio/e2e-ui/commands';
38
+ ```
39
+
40
+ Spec file (one per suite, or use `registerAllTests`):
41
+
42
+ ```ts
43
+ import { createRegistryConfig, homeTests } from '@verdaccio/e2e-ui';
44
+
45
+ const config = createRegistryConfig({
46
+ registryUrl: Cypress.env('VERDACCIO_URL'),
47
+ });
48
+
49
+ homeTests(config);
50
+ ```
51
+
52
+ ## Available suites
53
+
54
+ | Export | Covers |
55
+ |---|---|
56
+ | `homeTests` | Empty-registry landing page, help card, 404 |
57
+ | `signinTests` | Login dialog, greeting, logout |
58
+ | `layoutTests` | `/-/static/ui-options.js` health, header chrome, footer |
59
+ | `searchTests` | Search input, query fires, empty state, clear |
60
+ | `settingsTests` | Settings dialog, language picker, language switch |
61
+ | `publishTests` | Publishes via `cy.task`, asserts readme / sidebar / tabs |
62
+
63
+ ## Configuration
64
+
65
+ ```ts
66
+ createRegistryConfig({
67
+ registryUrl: 'http://localhost:4873',
68
+ credentials: { user: 'test', password: 'test' }, // optional
69
+ testIds: { header: { settingsTooltip: 'my-btn' } }, // optional per-field override
70
+ selectors: { loginDialog: { submitButton: '#go' } },// optional per-field override
71
+ });
72
+ ```
73
+
74
+ Every data-testid referenced by the suites is configurable. See
75
+ [`src/testIds.ts`](./src/testIds.ts) for the full `TestIds` + `Selectors`
76
+ shapes and defaults. Overrides are merged per section — unspecified fields
77
+ inherit from `DEFAULT_TEST_IDS` / `DEFAULT_SELECTORS`.
78
+
79
+ ## `publishPackage` task
80
+
81
+ Publishes a throwaway package so downstream specs have something to assert on.
82
+
83
+ ```ts
84
+ cy.task('publishPackage', {
85
+ pkgName: '@verdaccio/pkg-scoped',
86
+ version: '1.0.0',
87
+ dependencies: { debug: '4.0.0' },
88
+ unique: true, // appends -t<timestamp> so reruns don't collide on 403
89
+ }).then((result) => {
90
+ // result: { pkgName, version, tempFolder, stdout, stderr, exitCode }
91
+ });
92
+
93
+ // in after():
94
+ cy.task('cleanupPublished', tempFolder);
95
+ ```
96
+
97
+ Under the hood: creates a throwaway user via `PUT /-/user/...`, scaffolds a
98
+ temp project with an `.npmrc` carrying the legacy auth token, spawns
99
+ `npm publish --tag latest`.
100
+
101
+ ## Verdaccio config requirements
102
+
103
+ ```yaml
104
+ web:
105
+ enable: true
106
+ login: true
107
+ showSettings: true # required by settingsTests
108
+
109
+ userRateLimit:
110
+ windowMs: 1000
111
+ max: 10000 # default (1000 / 15min) is too tight for a full suite run
112
+
113
+ packages:
114
+ '**':
115
+ access: $all
116
+ publish: $anonymous $authenticated
117
+ unpublish: $anonymous $authenticated
118
+ proxy: npmjs
119
+ ```
120
+
121
+ Restart Verdaccio after changing these — it reads them at startup.
122
+
123
+ ## Custom commands
124
+
125
+ - `cy.getByTestId(selector)` — shortcut for `cy.get('[data-testid=<selector>]')`
126
+ - `cy.login(user, password, selectors?)` — fills + submits the login dialog;
127
+ pass `selectors` to override form field IDs for non-default builds
128
+
129
+ Task calls are strongly typed — `cy.task('publishPackage', …)` returns
130
+ `Chainable<PublishPackageResult>`, unknown task names fail at compile time.
131
+
132
+ ## License
133
+
134
+ MIT
@@ -1,15 +1,31 @@
1
1
  //#region src/commands/index.ts
2
+ /**
3
+ * Default login form selectors. Kept in sync with `DEFAULT_SELECTORS`
4
+ * in ../testIds.ts — duplicating them here (as plain constants) lets
5
+ * `cy.login` call sites that don't pass an explicit selectors object
6
+ * still work without importing anything.
7
+ */
8
+ var DEFAULT_LOGIN_SELECTORS = {
9
+ loginButton: "header--button-login",
10
+ usernameInput: "#login--dialog-username",
11
+ passwordInput: "#login--dialog-password",
12
+ submitButton: "#login--dialog-button-submit"
13
+ };
2
14
  Cypress.Commands.add("getByTestId", (selector, ...args) => {
3
15
  return cy.get(`[data-testid=${selector}]`, ...args);
4
16
  });
5
- Cypress.Commands.add("login", (user, password) => {
6
- cy.getByTestId("header--button-login").click();
17
+ Cypress.Commands.add("login", (user, password, selectors = {}) => {
18
+ const loginButton = selectors.loginButton ?? DEFAULT_LOGIN_SELECTORS.loginButton;
19
+ const usernameInput = selectors.usernameInput ?? DEFAULT_LOGIN_SELECTORS.usernameInput;
20
+ const passwordInput = selectors.passwordInput ?? DEFAULT_LOGIN_SELECTORS.passwordInput;
21
+ const submitButton = selectors.submitButton ?? DEFAULT_LOGIN_SELECTORS.submitButton;
22
+ cy.getByTestId(loginButton).click();
7
23
  cy.wait(300);
8
- cy.get("#login--dialog-username").type(user);
24
+ cy.get(usernameInput).type(user);
9
25
  cy.wait(200);
10
- cy.get("#login--dialog-password").type(password);
26
+ cy.get(passwordInput).type(password);
11
27
  cy.wait(500);
12
- cy.get("#login--dialog-button-submit").click();
28
+ cy.get(submitButton).click();
13
29
  });
14
30
  //#endregion
15
31
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../../src/commands/index.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\ndeclare global {\n namespace Cypress {\n interface Chainable {\n /**\n * Find element by data-testid attribute\n */\n getByTestId(selector: string, ...args: any[]): Chainable<JQuery<HTMLElement>>;\n /**\n * Login to Verdaccio UI\n */\n login(user: string, password: string): Chainable<void>;\n }\n }\n}\n\nCypress.Commands.add('getByTestId', (selector: string, ...args: any[]) => {\n return cy.get(`[data-testid=${selector}]`, ...args);\n});\n\nCypress.Commands.add('login', (user: string, password: string) => {\n cy.getByTestId('header--button-login').click();\n cy.wait(300);\n cy.get('#login--dialog-username').type(user);\n cy.wait(200);\n cy.get('#login--dialog-password').type(password);\n cy.wait(500);\n cy.get('#login--dialog-button-submit').click();\n});\n\nexport {};\n"],"mappings":";AAiBA,QAAQ,SAAS,IAAI,gBAAgB,UAAkB,GAAG,SAAgB;AACxE,QAAO,GAAG,IAAI,gBAAgB,SAAS,IAAI,GAAG,KAAK;EACnD;AAEF,QAAQ,SAAS,IAAI,UAAU,MAAc,aAAqB;AAChE,IAAG,YAAY,uBAAuB,CAAC,OAAO;AAC9C,IAAG,KAAK,IAAI;AACZ,IAAG,IAAI,0BAA0B,CAAC,KAAK,KAAK;AAC5C,IAAG,KAAK,IAAI;AACZ,IAAG,IAAI,0BAA0B,CAAC,KAAK,SAAS;AAChD,IAAG,KAAK,IAAI;AACZ,IAAG,IAAI,+BAA+B,CAAC,OAAO;EAC9C"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../../src/commands/index.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\n/**\n * Default login form selectors. Kept in sync with `DEFAULT_SELECTORS`\n * in ../testIds.ts — duplicating them here (as plain constants) lets\n * `cy.login` call sites that don't pass an explicit selectors object\n * still work without importing anything.\n */\nconst DEFAULT_LOGIN_SELECTORS = {\n loginButton: 'header--button-login',\n usernameInput: '#login--dialog-username',\n passwordInput: '#login--dialog-password',\n submitButton: '#login--dialog-button-submit',\n};\n\nexport interface LoginSelectors {\n /** data-testid of the header \"Login\" button that opens the dialog. */\n loginButton?: string;\n /** CSS selector for the username input. */\n usernameInput?: string;\n /** CSS selector for the password input. */\n passwordInput?: string;\n /** CSS selector for the submit button. */\n submitButton?: string;\n}\n\ndeclare global {\n namespace Cypress {\n interface Chainable {\n /**\n * Find element by data-testid attribute.\n */\n getByTestId(selector: string, ...args: any[]): Chainable<JQuery<HTMLElement>>;\n /**\n * Login to Verdaccio UI. Selectors default to Verdaccio 6.x\n * conventions; pass `selectors` to override any subset for\n * non-default builds.\n */\n login(\n user: string,\n password: string,\n selectors?: LoginSelectors\n ): Chainable<void>;\n }\n }\n}\n\nCypress.Commands.add('getByTestId', (selector: string, ...args: any[]) => {\n return cy.get(`[data-testid=${selector}]`, ...args);\n});\n\nCypress.Commands.add(\n 'login',\n (user: string, password: string, selectors: LoginSelectors = {}) => {\n const loginButton = selectors.loginButton ?? DEFAULT_LOGIN_SELECTORS.loginButton;\n const usernameInput =\n selectors.usernameInput ?? DEFAULT_LOGIN_SELECTORS.usernameInput;\n const passwordInput =\n selectors.passwordInput ?? DEFAULT_LOGIN_SELECTORS.passwordInput;\n const submitButton =\n selectors.submitButton ?? DEFAULT_LOGIN_SELECTORS.submitButton;\n\n cy.getByTestId(loginButton).click();\n cy.wait(300);\n cy.get(usernameInput).type(user);\n cy.wait(200);\n cy.get(passwordInput).type(password);\n cy.wait(500);\n cy.get(submitButton).click();\n }\n);\n\nexport {};\n"],"mappings":";;;;;;;AAQA,IAAM,0BAA0B;CAC9B,aAAa;CACb,eAAe;CACf,eAAe;CACf,cAAc;CACf;AAkCD,QAAQ,SAAS,IAAI,gBAAgB,UAAkB,GAAG,SAAgB;AACxE,QAAO,GAAG,IAAI,gBAAgB,SAAS,IAAI,GAAG,KAAK;EACnD;AAEF,QAAQ,SAAS,IACf,UACC,MAAc,UAAkB,YAA4B,EAAE,KAAK;CAClE,MAAM,cAAc,UAAU,eAAe,wBAAwB;CACrE,MAAM,gBACJ,UAAU,iBAAiB,wBAAwB;CACrD,MAAM,gBACJ,UAAU,iBAAiB,wBAAwB;CACrD,MAAM,eACJ,UAAU,gBAAgB,wBAAwB;AAEpD,IAAG,YAAY,YAAY,CAAC,OAAO;AACnC,IAAG,KAAK,IAAI;AACZ,IAAG,IAAI,cAAc,CAAC,KAAK,KAAK;AAChC,IAAG,KAAK,IAAI;AACZ,IAAG,IAAI,cAAc,CAAC,KAAK,SAAS;AACpC,IAAG,KAAK,IAAI;AACZ,IAAG,IAAI,aAAa,CAAC,OAAO;EAE/B"}
@@ -0,0 +1,64 @@
1
+ //#region src/features.ts
2
+ /** Defaults: all flags on. */
3
+ var DEFAULT_FEATURES = {
4
+ search: {
5
+ resultsDropdown: true,
6
+ resultClickNavigation: true
7
+ },
8
+ home: { publishedPackageRendering: true },
9
+ settings: { languageSwitcher: true },
10
+ signin: { validationTests: true },
11
+ layout: { themeSwitch: true },
12
+ publish: {
13
+ downloadTarball: true,
14
+ rawViewer: true
15
+ }
16
+ };
17
+ /**
18
+ * Merge user overrides into the default feature flags. Per-section,
19
+ * one level deep — matching the style of `mergeTestIds`.
20
+ */
21
+ function mergeFeatures(defaults, overrides) {
22
+ if (!overrides) return defaults;
23
+ return {
24
+ search: {
25
+ ...defaults.search,
26
+ ...overrides.search
27
+ },
28
+ home: {
29
+ ...defaults.home,
30
+ ...overrides.home
31
+ },
32
+ settings: {
33
+ ...defaults.settings,
34
+ ...overrides.settings
35
+ },
36
+ signin: {
37
+ ...defaults.signin,
38
+ ...overrides.signin
39
+ },
40
+ layout: {
41
+ ...defaults.layout,
42
+ ...overrides.layout
43
+ },
44
+ publish: {
45
+ ...defaults.publish,
46
+ ...overrides.publish
47
+ }
48
+ };
49
+ }
50
+ /**
51
+ * Helper that returns either `it` or `it.skip` depending on an
52
+ * enabled flag. Usage:
53
+ *
54
+ * maybeIt(features.search.resultsDropdown)('…', () => { … });
55
+ */
56
+ function maybeIt(enabled) {
57
+ return enabled ? it : it.skip;
58
+ }
59
+ //#endregion
60
+ exports.DEFAULT_FEATURES = DEFAULT_FEATURES;
61
+ exports.maybeIt = maybeIt;
62
+ exports.mergeFeatures = mergeFeatures;
63
+
64
+ //# sourceMappingURL=features.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features.cjs","names":[],"sources":["../../src/features.ts"],"sourcesContent":["/**\n * Per-test feature flags for enabling/disabling individual test cases\n * in the e2e-ui suites.\n *\n * The Verdaccio UI behaves differently across branches — search result\n * payload shape differs between the `/-/v1/search` and\n * `/-/verdaccio/data/search/*` endpoints, the language picker was\n * added in a specific minor, etc. Rather than forking the suite per\n * branch, consumers can disable individual tests via\n * `createRegistryConfig({ features: { … } })`.\n *\n * Every flag defaults to `true` (test runs). Override to `false` to\n * convert the test into a Mocha `it.skip` call — the suite still\n * reports the test but marks it as pending.\n */\nexport interface Features {\n search: {\n /**\n * Whether to run the \"results dropdown renders a matching\n * package\" test. Disable on builds where the search result shape\n * or the Autocomplete rendering differs from the default assumption.\n */\n resultsDropdown: boolean;\n /**\n * Whether to run the \"clicking a result navigates to detail\" test.\n * Depends on the Autocomplete `onSelectItem` → router wiring.\n */\n resultClickNavigation: boolean;\n };\n home: {\n /**\n * Whether to run the `with a published package` sub-block inside\n * `homeTests` (publishes a throwaway package and asserts it\n * renders in the package list).\n */\n publishedPackageRendering: boolean;\n };\n settings: {\n /**\n * Whether to run the \"change language via card click\" test.\n * Depends on the `LanguageSwitch` component layout and its\n * translation sentinels (\"Translations\", \"German\"). Skip on\n * builds that lag behind the upstream translation file.\n */\n languageSwitcher: boolean;\n };\n signin: {\n /**\n * Whether to run the three validation tests (disabled submit\n * button, invalid credentials error banner). Depends on the\n * current yup schema and the hard-coded \"Invalid username or\n * password\" error string.\n */\n validationTests: boolean;\n };\n layout: {\n /**\n * Whether to run the dark/light theme switch test. Depends on\n * `web.showThemeSwitch` (defaults to true in ui-theme) and the\n * `header--button--light` / `header--button--dark` testids.\n */\n themeSwitch: boolean;\n };\n publish: {\n /**\n * Whether to run the \"tarball download button fires a GET\" test.\n * Depends on `web.showDownloadTarball` (defaults to true) and\n * the published package manifest having a valid `dist.tarball`.\n */\n downloadTarball: boolean;\n /**\n * Whether to run the \"raw viewer dialog opens + closes\" test.\n * Depends on `web.showRaw` (defaults to true).\n */\n rawViewer: boolean;\n };\n}\n\n/** Defaults: all flags on. */\nexport const DEFAULT_FEATURES: Features = {\n search: {\n resultsDropdown: true,\n resultClickNavigation: true,\n },\n home: {\n publishedPackageRendering: true,\n },\n settings: {\n languageSwitcher: true,\n },\n signin: {\n validationTests: true,\n },\n layout: {\n themeSwitch: true,\n },\n publish: {\n downloadTarball: true,\n rawViewer: true,\n },\n};\n\nimport type { DeepPartial } from './testIds';\n\n/**\n * Merge user overrides into the default feature flags. Per-section,\n * one level deep — matching the style of `mergeTestIds`.\n */\nexport function mergeFeatures(\n defaults: Features,\n overrides?: DeepPartial<Features>\n): Features {\n if (!overrides) return defaults;\n return {\n search: { ...defaults.search, ...overrides.search },\n home: { ...defaults.home, ...overrides.home },\n settings: { ...defaults.settings, ...overrides.settings },\n signin: { ...defaults.signin, ...overrides.signin },\n layout: { ...defaults.layout, ...overrides.layout },\n publish: { ...defaults.publish, ...overrides.publish },\n };\n}\n\n/**\n * Helper that returns either `it` or `it.skip` depending on an\n * enabled flag. Usage:\n *\n * maybeIt(features.search.resultsDropdown)('…', () => { … });\n */\nexport function maybeIt(enabled: boolean): Mocha.TestFunction | Mocha.PendingTestFunction {\n return enabled ? it : it.skip;\n}\n"],"mappings":";;AA+EA,IAAa,mBAA6B;CACxC,QAAQ;EACN,iBAAiB;EACjB,uBAAuB;EACxB;CACD,MAAM,EACJ,2BAA2B,MAC5B;CACD,UAAU,EACR,kBAAkB,MACnB;CACD,QAAQ,EACN,iBAAiB,MAClB;CACD,QAAQ,EACN,aAAa,MACd;CACD,SAAS;EACP,iBAAiB;EACjB,WAAW;EACZ;CACF;;;;;AAQD,SAAgB,cACd,UACA,WACU;AACV,KAAI,CAAC,UAAW,QAAO;AACvB,QAAO;EACL,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;GAAQ;EACnD,MAAM;GAAE,GAAG,SAAS;GAAM,GAAG,UAAU;GAAM;EAC7C,UAAU;GAAE,GAAG,SAAS;GAAU,GAAG,UAAU;GAAU;EACzD,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;GAAQ;EACnD,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;GAAQ;EACnD,SAAS;GAAE,GAAG,SAAS;GAAS,GAAG,UAAU;GAAS;EACvD;;;;;;;;AASH,SAAgB,QAAQ,SAAkE;AACxF,QAAO,UAAU,KAAK,GAAG"}
@@ -1,10 +1,27 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_publish = require("./tasks/publish.cjs");
3
+ const require_features = require("./features.cjs");
4
+ const require_testIds = require("./testIds.cjs");
2
5
  const require_home = require("./tests/home.cjs");
3
6
  const require_signin = require("./tests/signin.cjs");
4
- const require_publish = require("./tests/publish.cjs");
7
+ const require_publish$1 = require("./tests/publish.cjs");
8
+ const require_search = require("./tests/search.cjs");
9
+ const require_settings = require("./tests/settings.cjs");
10
+ const require_layout = require("./tests/layout.cjs");
5
11
  //#region src/index.ts
6
12
  /**
7
13
  * Build a full RegistryConfig from user-provided options with defaults.
14
+ *
15
+ * `testIds` and `selectors` are deep-merged per section with the
16
+ * defaults in `./testIds`. Consumers targeting a non-default Verdaccio
17
+ * build can override just the fields that drifted:
18
+ *
19
+ * createRegistryConfig({
20
+ * registryUrl: 'http://localhost:4873',
21
+ * testIds: {
22
+ * header: { settingsTooltip: 'my-new-settings-btn' },
23
+ * },
24
+ * });
8
25
  */
9
26
  function createRegistryConfig(options) {
10
27
  const url = new URL(options.registryUrl);
@@ -15,7 +32,10 @@ function createRegistryConfig(options) {
15
32
  user: "test",
16
33
  password: "test"
17
34
  },
18
- title: options.title ?? "Verdaccio"
35
+ title: options.title ?? "Verdaccio",
36
+ testIds: require_testIds.mergeTestIds(require_testIds.DEFAULT_TEST_IDS, options.testIds),
37
+ selectors: require_testIds.mergeSelectors(require_testIds.DEFAULT_SELECTORS, options.selectors),
38
+ features: require_features.mergeFeatures(require_features.DEFAULT_FEATURES, options.features)
19
39
  };
20
40
  }
21
41
  /**
@@ -34,12 +54,38 @@ function createRegistryConfig(options) {
34
54
  */
35
55
  function setupVerdaccioTasks(on, options) {
36
56
  const config = createRegistryConfig(options);
37
- on("task", { registry() {
38
- return {
39
- registryUrl: config.registryUrl,
40
- port: config.port
41
- };
42
- } });
57
+ on("task", {
58
+ registry() {
59
+ return {
60
+ registryUrl: config.registryUrl,
61
+ port: config.port
62
+ };
63
+ },
64
+ async publishPackage(input) {
65
+ return require_publish.publishPackage({
66
+ registryUrl: input.registryUrl ?? config.registryUrl,
67
+ credentials: input.credentials ?? config.credentials,
68
+ pkgName: input.pkgName,
69
+ version: input.version,
70
+ dependencies: input.dependencies,
71
+ devDependencies: input.devDependencies,
72
+ unique: input.unique
73
+ });
74
+ },
75
+ async cleanupPublished(tempFolder) {
76
+ await require_publish.cleanupPublished(tempFolder);
77
+ return null;
78
+ },
79
+ async unpublishPackage(input) {
80
+ return require_publish.unpublishPackage(typeof input === "string" ? {
81
+ pkgName: input,
82
+ registryUrl: config.registryUrl
83
+ } : {
84
+ ...input,
85
+ registryUrl: input.registryUrl ?? config.registryUrl
86
+ });
87
+ }
88
+ });
43
89
  }
44
90
  /**
45
91
  * Register all Verdaccio UI tests.
@@ -60,14 +106,27 @@ function setupVerdaccioTasks(on, options) {
60
106
  function registerAllTests(config) {
61
107
  require_home.homeTests(config);
62
108
  require_signin.signinTests(config);
63
- require_publish.publishTests(config);
109
+ require_layout.layoutTests(config);
110
+ require_search.searchTests(config);
111
+ require_settings.settingsTests(config);
112
+ require_publish$1.publishTests(config);
64
113
  }
65
114
  //#endregion
115
+ exports.DEFAULT_FEATURES = require_features.DEFAULT_FEATURES;
116
+ exports.DEFAULT_SELECTORS = require_testIds.DEFAULT_SELECTORS;
117
+ exports.DEFAULT_TEST_IDS = require_testIds.DEFAULT_TEST_IDS;
118
+ exports.cleanupPublished = require_publish.cleanupPublished;
66
119
  exports.createRegistryConfig = createRegistryConfig;
67
120
  exports.homeTests = require_home.homeTests;
68
- exports.publishTests = require_publish.publishTests;
121
+ exports.layoutTests = require_layout.layoutTests;
122
+ exports.maybeIt = require_features.maybeIt;
123
+ exports.publishPackage = require_publish.publishPackage;
124
+ exports.publishTests = require_publish$1.publishTests;
69
125
  exports.registerAllTests = registerAllTests;
126
+ exports.searchTests = require_search.searchTests;
127
+ exports.settingsTests = require_settings.settingsTests;
70
128
  exports.setupVerdaccioTasks = setupVerdaccioTasks;
71
129
  exports.signinTests = require_signin.signinTests;
130
+ exports.unpublishPackage = require_publish.unpublishPackage;
72
131
 
73
132
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import { RegistryConfig, VerdaccioUiOptions } from './types';\n\nexport type { RegistryConfig, VerdaccioUiOptions } from './types';\nexport { homeTests, signinTests, publishTests } from './tests';\n\n/**\n * Build a full RegistryConfig from user-provided options with defaults.\n */\nexport function createRegistryConfig(options: VerdaccioUiOptions): RegistryConfig {\n const url = new URL(options.registryUrl);\n return {\n registryUrl: options.registryUrl,\n port: options.port ?? (parseInt(url.port, 10) || 4873),\n credentials: options.credentials ?? { user: 'test', password: 'test' },\n title: options.title ?? 'Verdaccio',\n };\n}\n\n/**\n * Register Verdaccio Cypress tasks in setupNodeEvents.\n *\n * Usage in cypress.config.ts:\n * import { setupVerdaccioTasks } from '@verdaccio/e2e-ui';\n *\n * export default defineConfig({\n * e2e: {\n * setupNodeEvents(on) {\n * setupVerdaccioTasks(on, { registryUrl: 'http://localhost:4873' });\n * },\n * },\n * });\n */\nexport function setupVerdaccioTasks(\n on: Cypress.PluginEvents,\n options: VerdaccioUiOptions\n): void {\n const config = createRegistryConfig(options);\n\n on('task', {\n registry() {\n return {\n registryUrl: config.registryUrl,\n port: config.port,\n };\n },\n });\n}\n\n/**\n * Register all Verdaccio UI tests.\n *\n * Usage in a spec file:\n * import { registerAllTests, createRegistryConfig } from '@verdaccio/e2e-ui';\n *\n * const config = createRegistryConfig({ registryUrl: 'http://localhost:4873' });\n * registerAllTests(config);\n *\n * Or pick individual suites:\n * import { homeTests, signinTests } from '@verdaccio/e2e-ui';\n *\n * const config = createRegistryConfig({ registryUrl: 'http://localhost:4873' });\n * homeTests(config);\n * signinTests(config);\n */\nexport function registerAllTests(config: RegistryConfig): void {\n homeTests(config);\n signinTests(config);\n publishTests(config);\n}\n\n// Re-export for convenience\nimport { homeTests, signinTests, publishTests } from './tests';\n"],"mappings":";;;;;;;;AAQA,SAAgB,qBAAqB,SAA6C;CAChF,MAAM,MAAM,IAAI,IAAI,QAAQ,YAAY;AACxC,QAAO;EACL,aAAa,QAAQ;EACrB,MAAM,QAAQ,SAAS,SAAS,IAAI,MAAM,GAAG,IAAI;EACjD,aAAa,QAAQ,eAAe;GAAE,MAAM;GAAQ,UAAU;GAAQ;EACtE,OAAO,QAAQ,SAAS;EACzB;;;;;;;;;;;;;;;;AAiBH,SAAgB,oBACd,IACA,SACM;CACN,MAAM,SAAS,qBAAqB,QAAQ;AAE5C,IAAG,QAAQ,EACT,WAAW;AACT,SAAO;GACL,aAAa,OAAO;GACpB,MAAM,OAAO;GACd;IAEJ,CAAC;;;;;;;;;;;;;;;;;;AAmBJ,SAAgB,iBAAiB,QAA8B;AAC7D,cAAA,UAAU,OAAO;AACjB,gBAAA,YAAY,OAAO;AACnB,iBAAA,aAAa,OAAO"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import {\n cleanupPublished,\n publishPackage,\n PublishPackageResult,\n PublishPackageTaskInput,\n unpublishPackage,\n UnpublishPackageInput,\n UnpublishPackageResult,\n} from './tasks';\nimport { DEFAULT_FEATURES, mergeFeatures } from './features';\nimport {\n DEFAULT_SELECTORS,\n DEFAULT_TEST_IDS,\n mergeSelectors,\n mergeTestIds,\n} from './testIds';\nimport { RegistryConfig, RegistryTaskResult, VerdaccioUiOptions } from './types';\n\nexport type {\n RegistryConfig,\n RegistryTaskResult,\n VerdaccioUiOptions,\n} from './types';\nexport type {\n PublishPackageInput,\n PublishPackageTaskInput,\n PublishPackageResult,\n UnpublishPackageInput,\n UnpublishPackageResult,\n} from './tasks';\nexport type { DeepPartial, Selectors, TestIds } from './testIds';\nexport type { Features } from './features';\nexport { DEFAULT_SELECTORS, DEFAULT_TEST_IDS } from './testIds';\nexport { DEFAULT_FEATURES, maybeIt } from './features';\nexport { publishPackage, cleanupPublished, unpublishPackage } from './tasks';\nexport {\n homeTests,\n signinTests,\n publishTests,\n searchTests,\n settingsTests,\n layoutTests,\n} from './tests';\n\n/**\n * Build a full RegistryConfig from user-provided options with defaults.\n *\n * `testIds` and `selectors` are deep-merged per section with the\n * defaults in `./testIds`. Consumers targeting a non-default Verdaccio\n * build can override just the fields that drifted:\n *\n * createRegistryConfig({\n * registryUrl: 'http://localhost:4873',\n * testIds: {\n * header: { settingsTooltip: 'my-new-settings-btn' },\n * },\n * });\n */\nexport function createRegistryConfig(options: VerdaccioUiOptions): RegistryConfig {\n const url = new URL(options.registryUrl);\n return {\n registryUrl: options.registryUrl,\n port: options.port ?? (parseInt(url.port, 10) || 4873),\n credentials: options.credentials ?? { user: 'test', password: 'test' },\n title: options.title ?? 'Verdaccio',\n testIds: mergeTestIds(DEFAULT_TEST_IDS, options.testIds),\n selectors: mergeSelectors(DEFAULT_SELECTORS, options.selectors),\n features: mergeFeatures(DEFAULT_FEATURES, options.features),\n };\n}\n\n/**\n * Register Verdaccio Cypress tasks in setupNodeEvents.\n *\n * Usage in cypress.config.ts:\n * import { setupVerdaccioTasks } from '@verdaccio/e2e-ui';\n *\n * export default defineConfig({\n * e2e: {\n * setupNodeEvents(on) {\n * setupVerdaccioTasks(on, { registryUrl: 'http://localhost:4873' });\n * },\n * },\n * });\n */\nexport function setupVerdaccioTasks(\n on: Cypress.PluginEvents,\n options: VerdaccioUiOptions\n): void {\n const config = createRegistryConfig(options);\n\n on('task', {\n registry(): RegistryTaskResult {\n return {\n registryUrl: config.registryUrl,\n port: config.port,\n };\n },\n /**\n * Publish a throwaway package to the registry.\n *\n * Usage from a Cypress spec:\n *\n * cy.task('publishPackage', { pkgName: '@verdaccio/pkg-scoped' });\n *\n * Any field not provided falls back to the values configured here\n * (registryUrl, credentials). The task returns a PublishPackageResult.\n */\n async publishPackage(\n input: PublishPackageTaskInput\n ): Promise<PublishPackageResult> {\n return publishPackage({\n registryUrl: input.registryUrl ?? config.registryUrl,\n credentials: input.credentials ?? config.credentials,\n pkgName: input.pkgName,\n version: input.version,\n dependencies: input.dependencies,\n devDependencies: input.devDependencies,\n unique: input.unique,\n });\n },\n /**\n * Remove a temp project folder returned by a previous publishPackage\n * call. Returns null (Cypress tasks must return something serializable).\n *\n * cy.task('cleanupPublished', result.tempFolder);\n */\n async cleanupPublished(tempFolder: string): Promise<null> {\n await cleanupPublished(tempFolder);\n return null;\n },\n /**\n * Unpublish a package from the registry so the next test starts\n * from a clean slate. Accepts either a package name or a full input\n * object (to reuse a prior tempFolder's token).\n *\n * cy.task('unpublishPackage', '@verdaccio/pkg-scoped');\n * cy.task('unpublishPackage', { pkgName, tempFolder });\n */\n async unpublishPackage(\n input: string | (Omit<UnpublishPackageInput, 'registryUrl'> & {\n registryUrl?: string;\n })\n ): Promise<UnpublishPackageResult> {\n const normalized =\n typeof input === 'string'\n ? { pkgName: input, registryUrl: config.registryUrl }\n : { ...input, registryUrl: input.registryUrl ?? config.registryUrl };\n return unpublishPackage(normalized);\n },\n });\n}\n\n/**\n * Register all Verdaccio UI tests.\n *\n * Usage in a spec file:\n * import { registerAllTests, createRegistryConfig } from '@verdaccio/e2e-ui';\n *\n * const config = createRegistryConfig({ registryUrl: 'http://localhost:4873' });\n * registerAllTests(config);\n *\n * Or pick individual suites:\n * import { homeTests, signinTests } from '@verdaccio/e2e-ui';\n *\n * const config = createRegistryConfig({ registryUrl: 'http://localhost:4873' });\n * homeTests(config);\n * signinTests(config);\n */\nexport function registerAllTests(config: RegistryConfig): void {\n homeTests(config);\n signinTests(config);\n layoutTests(config);\n searchTests(config);\n settingsTests(config);\n publishTests(config);\n}\n\n// Re-export for convenience\nimport {\n homeTests,\n signinTests,\n publishTests,\n searchTests,\n settingsTests,\n layoutTests,\n} from './tests';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,SAAgB,qBAAqB,SAA6C;CAChF,MAAM,MAAM,IAAI,IAAI,QAAQ,YAAY;AACxC,QAAO;EACL,aAAa,QAAQ;EACrB,MAAM,QAAQ,SAAS,SAAS,IAAI,MAAM,GAAG,IAAI;EACjD,aAAa,QAAQ,eAAe;GAAE,MAAM;GAAQ,UAAU;GAAQ;EACtE,OAAO,QAAQ,SAAS;EACxB,SAAS,gBAAA,aAAa,gBAAA,kBAAkB,QAAQ,QAAQ;EACxD,WAAW,gBAAA,eAAe,gBAAA,mBAAmB,QAAQ,UAAU;EAC/D,UAAU,iBAAA,cAAc,iBAAA,kBAAkB,QAAQ,SAAS;EAC5D;;;;;;;;;;;;;;;;AAiBH,SAAgB,oBACd,IACA,SACM;CACN,MAAM,SAAS,qBAAqB,QAAQ;AAE5C,IAAG,QAAQ;EACT,WAA+B;AAC7B,UAAO;IACL,aAAa,OAAO;IACpB,MAAM,OAAO;IACd;;EAYH,MAAM,eACJ,OAC+B;AAC/B,UAAO,gBAAA,eAAe;IACpB,aAAa,MAAM,eAAe,OAAO;IACzC,aAAa,MAAM,eAAe,OAAO;IACzC,SAAS,MAAM;IACf,SAAS,MAAM;IACf,cAAc,MAAM;IACpB,iBAAiB,MAAM;IACvB,QAAQ,MAAM;IACf,CAAC;;EAQJ,MAAM,iBAAiB,YAAmC;AACxD,SAAM,gBAAA,iBAAiB,WAAW;AAClC,UAAO;;EAUT,MAAM,iBACJ,OAGiC;AAKjC,UAAO,gBAAA,iBAHL,OAAO,UAAU,WACb;IAAE,SAAS;IAAO,aAAa,OAAO;IAAa,GACnD;IAAE,GAAG;IAAO,aAAa,MAAM,eAAe,OAAO;IAAa,CACrC;;EAEtC,CAAC;;;;;;;;;;;;;;;;;;AAmBJ,SAAgB,iBAAiB,QAA8B;AAC7D,cAAA,UAAU,OAAO;AACjB,gBAAA,YAAY,OAAO;AACnB,gBAAA,YAAY,OAAO;AACnB,gBAAA,YAAY,OAAO;AACnB,kBAAA,cAAc,OAAO;AACrB,mBAAA,aAAa,OAAO"}
@@ -0,0 +1,245 @@
1
+ let child_process = require("child_process");
2
+ let fs_promises = require("fs/promises");
3
+ let os = require("os");
4
+ let path = require("path");
5
+ //#region src/tasks/publish.ts
6
+ function sanitizeFolderName(name) {
7
+ return name.replace(/[^a-zA-Z0-9-_]/g, "-");
8
+ }
9
+ /**
10
+ * Obtain a registry-API-compatible (legacy) auth token for publish.
11
+ *
12
+ * Strategy: create a throwaway user per call. `PUT /-/user/org.couchdb.user:<name>`
13
+ * only returns a token on CREATE (and 409s on existing users), so we
14
+ * guarantee success by generating a unique username each time.
15
+ *
16
+ * We need a legacy token specifically because:
17
+ * - Verdaccio's default API middleware accepts legacy tokens but NOT
18
+ * JWTs from `/-/verdaccio/sec/login`
19
+ * - Modern npm (>= 10.x) refuses to run `npm publish` at all without
20
+ * an `_authToken` entry in `.npmrc`, even against a registry that
21
+ * allows `$anonymous` publish — it errors out client-side
22
+ *
23
+ * The throwaway user stays in the test registry's htpasswd store after
24
+ * the run, which is fine for ephemeral CI environments and local temp
25
+ * setups (both wipe storage between runs).
26
+ */
27
+ async function obtainLegacyToken(registryUrl) {
28
+ const user = `e2e-bot-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
29
+ const password = "e2e-bot-password";
30
+ const url = `${registryUrl.replace(/\/$/, "")}/-/user/org.couchdb.user:${encodeURIComponent(user)}`;
31
+ const res = await fetch(url, {
32
+ method: "PUT",
33
+ headers: { "content-type": "application/json" },
34
+ body: JSON.stringify({
35
+ name: user,
36
+ password,
37
+ _id: `org.couchdb.user:${user}`,
38
+ type: "user",
39
+ roles: []
40
+ })
41
+ });
42
+ if (!res.ok) {
43
+ const body = await res.text();
44
+ throw new Error(`[publishPackage] failed to create throwaway user "${user}" (HTTP ${res.status}): ${body}`);
45
+ }
46
+ const json = await res.json();
47
+ if (!json.token) throw new Error(`[publishPackage] user creation response did not contain a token: ${JSON.stringify(json)}`);
48
+ return {
49
+ user,
50
+ token: json.token
51
+ };
52
+ }
53
+ async function createTempProject(pkgName, version, registryUrl, token, dependencies, devDependencies) {
54
+ const tempFolder = await (0, fs_promises.mkdtemp)((0, path.join)((0, os.tmpdir)(), `verdaccio-e2e-ui-${sanitizeFolderName(pkgName)}-`));
55
+ const manifest = {
56
+ name: pkgName,
57
+ version,
58
+ description: `e2e test fixture ${pkgName}`,
59
+ main: "index.js",
60
+ dependencies,
61
+ devDependencies,
62
+ keywords: [
63
+ "verdaccio",
64
+ "e2e",
65
+ "test"
66
+ ],
67
+ author: "Verdaccio E2E <verdaccio@example.org>",
68
+ license: "MIT",
69
+ publishConfig: {
70
+ access: "public",
71
+ registry: registryUrl
72
+ }
73
+ };
74
+ await (0, fs_promises.writeFile)((0, path.join)(tempFolder, "package.json"), JSON.stringify(manifest, null, 2));
75
+ await (0, fs_promises.writeFile)((0, path.join)(tempFolder, "README.md"), `# ${pkgName}\n\nPublished by @verdaccio/e2e-ui for e2e testing.\n`);
76
+ await (0, fs_promises.writeFile)((0, path.join)(tempFolder, "index.js"), `module.exports = ${JSON.stringify(pkgName)};\n`);
77
+ const registryHost = registryUrl.replace(/^https?:/, "");
78
+ const npmrc = [
79
+ `registry=${registryUrl}`,
80
+ `${registryHost}/:_authToken=${token}`,
81
+ "access=public",
82
+ ""
83
+ ].join("\n");
84
+ await (0, fs_promises.writeFile)((0, path.join)(tempFolder, ".npmrc"), npmrc);
85
+ return tempFolder;
86
+ }
87
+ function spawnNpmPublish(cwd, registryUrl) {
88
+ return new Promise((resolvePromise, rejectPromise) => {
89
+ const proc = (0, child_process.spawn)("npm", [
90
+ "publish",
91
+ "--registry",
92
+ registryUrl,
93
+ "--tag",
94
+ "latest",
95
+ "--loglevel=error"
96
+ ], {
97
+ cwd,
98
+ env: { ...process.env }
99
+ });
100
+ let stdout = "";
101
+ let stderr = "";
102
+ proc.stdout.on("data", (chunk) => {
103
+ stdout += chunk.toString();
104
+ });
105
+ proc.stderr.on("data", (chunk) => {
106
+ stderr += chunk.toString();
107
+ });
108
+ proc.on("error", rejectPromise);
109
+ proc.on("close", (code) => {
110
+ resolvePromise({
111
+ stdout,
112
+ stderr,
113
+ exitCode: code ?? -1
114
+ });
115
+ });
116
+ });
117
+ }
118
+ /**
119
+ * Publish a throwaway npm package to the target Verdaccio registry.
120
+ *
121
+ * Flow:
122
+ * 1. Create a throwaway user via `PUT /-/user/...` and capture its
123
+ * legacy auth token. See `obtainLegacyToken` for why.
124
+ * 2. Scaffold a temp project with `package.json`, `README.md`,
125
+ * `index.js`, and an `.npmrc` that includes the token.
126
+ * 3. Spawn `npm publish` from that temp dir.
127
+ *
128
+ * `input.credentials` is kept on the signature for forward-compat but
129
+ * is currently unused — each call mints its own throwaway user.
130
+ *
131
+ * Throws on non-zero npm exit. Returns the temp folder path on success
132
+ * so callers can inspect or clean up.
133
+ */
134
+ async function publishPackage(input) {
135
+ const baseVersion = input.version ?? "1.0.0";
136
+ const version = input.unique ? `${baseVersion}-t${Date.now()}` : baseVersion;
137
+ const { token } = await obtainLegacyToken(input.registryUrl);
138
+ const tempFolder = await createTempProject(input.pkgName, version, input.registryUrl, token, input.dependencies ?? {}, input.devDependencies ?? {});
139
+ const { stdout, stderr, exitCode } = await spawnNpmPublish(tempFolder, input.registryUrl);
140
+ if (exitCode !== 0) throw new Error(`[publishPackage] npm publish failed for ${input.pkgName}@${version} (exit ${exitCode}):\n${stderr || stdout}`);
141
+ return {
142
+ pkgName: input.pkgName,
143
+ version,
144
+ tempFolder,
145
+ stdout,
146
+ stderr,
147
+ exitCode
148
+ };
149
+ }
150
+ /**
151
+ * Remove a temp project folder previously created by publishPackage.
152
+ * Safe to call with a missing path or a path outside the OS tmp dir —
153
+ * in the latter case it refuses rather than rm-rf'ing arbitrary paths.
154
+ */
155
+ async function cleanupPublished(tempFolder) {
156
+ if (!tempFolder) return;
157
+ const tmpRoot = (0, os.tmpdir)();
158
+ if (!tempFolder.startsWith(tmpRoot)) throw new Error(`[cleanupPublished] refusing to remove "${tempFolder}" — not under ${tmpRoot}`);
159
+ await (0, fs_promises.rm)(tempFolder, {
160
+ recursive: true,
161
+ force: true
162
+ });
163
+ }
164
+ function spawnNpmUnpublish(cwd, registryUrl, pkgSpec) {
165
+ return new Promise((resolvePromise, rejectPromise) => {
166
+ const proc = (0, child_process.spawn)("npm", [
167
+ "unpublish",
168
+ pkgSpec,
169
+ "--force",
170
+ "--registry",
171
+ registryUrl,
172
+ "--loglevel=error"
173
+ ], {
174
+ cwd,
175
+ env: { ...process.env }
176
+ });
177
+ let stdout = "";
178
+ let stderr = "";
179
+ proc.stdout.on("data", (chunk) => {
180
+ stdout += chunk.toString();
181
+ });
182
+ proc.stderr.on("data", (chunk) => {
183
+ stderr += chunk.toString();
184
+ });
185
+ proc.on("error", rejectPromise);
186
+ proc.on("close", (code) => {
187
+ resolvePromise({
188
+ stdout,
189
+ stderr,
190
+ exitCode: code ?? -1
191
+ });
192
+ });
193
+ });
194
+ }
195
+ /**
196
+ * Unpublish a package from the target registry so subsequent tests
197
+ * start from a clean slate.
198
+ *
199
+ * If `tempFolder` is provided (typically from a prior `publishPackage`
200
+ * result), its `.npmrc` is reused so no extra throwaway user is
201
+ * minted. Otherwise this function creates its own temp folder + token.
202
+ * Either way the temp folder used by THIS call is removed on exit
203
+ * (but callers still own the lifecycle of a tempFolder they passed in).
204
+ *
205
+ * Treats HTTP 404 / "tarball does not exist" as success so reruns and
206
+ * parallel teardowns don't flap.
207
+ */
208
+ async function unpublishPackage(input) {
209
+ let workingFolder = input.tempFolder;
210
+ let ownsWorkingFolder = false;
211
+ if (!workingFolder) {
212
+ ownsWorkingFolder = true;
213
+ const { token } = await obtainLegacyToken(input.registryUrl);
214
+ workingFolder = await (0, fs_promises.mkdtemp)((0, path.join)((0, os.tmpdir)(), `verdaccio-e2e-ui-unpublish-`));
215
+ const registryHost = input.registryUrl.replace(/^https?:/, "");
216
+ await (0, fs_promises.writeFile)((0, path.join)(workingFolder, ".npmrc"), [
217
+ `registry=${input.registryUrl}`,
218
+ `${registryHost}/:_authToken=${token}`,
219
+ ""
220
+ ].join("\n"));
221
+ }
222
+ try {
223
+ const { stdout, stderr, exitCode } = await spawnNpmUnpublish(workingFolder, input.registryUrl, input.pkgName);
224
+ const alreadyGone = /404|not found|no such package|does not (exist|match)/i.test(`${stderr}\n${stdout}`);
225
+ if (exitCode !== 0 && !alreadyGone) throw new Error(`[unpublishPackage] npm unpublish failed for ${input.pkgName} (exit ${exitCode}):\n${stderr || stdout}`);
226
+ return {
227
+ pkgName: input.pkgName,
228
+ stdout,
229
+ stderr,
230
+ exitCode,
231
+ alreadyGone
232
+ };
233
+ } finally {
234
+ if (ownsWorkingFolder && workingFolder) await (0, fs_promises.rm)(workingFolder, {
235
+ recursive: true,
236
+ force: true
237
+ }).catch(() => void 0);
238
+ }
239
+ }
240
+ //#endregion
241
+ exports.cleanupPublished = cleanupPublished;
242
+ exports.publishPackage = publishPackage;
243
+ exports.unpublishPackage = unpublishPackage;
244
+
245
+ //# sourceMappingURL=publish.cjs.map