@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
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","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,WAAU,OAAO;AACjB,aAAY,OAAO;AACnB,cAAa,OAAO"}
1
+ {"version":3,"file":"index.js","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,aAAa,kBAAkB,QAAQ,QAAQ;EACxD,WAAW,eAAe,mBAAmB,QAAQ,UAAU;EAC/D,UAAU,cAAc,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,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,iBAAiB,WAAW;AAClC,UAAO;;EAUT,MAAM,iBACJ,OAGiC;AAKjC,UAAO,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,WAAU,OAAO;AACjB,aAAY,OAAO;AACnB,aAAY,OAAO;AACnB,aAAY,OAAO;AACnB,eAAc,OAAO;AACrB,cAAa,OAAO"}
@@ -0,0 +1,243 @@
1
+ import { spawn } from "child_process";
2
+ import { mkdtemp, rm, writeFile } from "fs/promises";
3
+ import { tmpdir } from "os";
4
+ import { join } from "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 mkdtemp(join(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 writeFile(join(tempFolder, "package.json"), JSON.stringify(manifest, null, 2));
75
+ await writeFile(join(tempFolder, "README.md"), `# ${pkgName}\n\nPublished by @verdaccio/e2e-ui for e2e testing.\n`);
76
+ await writeFile(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 writeFile(join(tempFolder, ".npmrc"), npmrc);
85
+ return tempFolder;
86
+ }
87
+ function spawnNpmPublish(cwd, registryUrl) {
88
+ return new Promise((resolvePromise, rejectPromise) => {
89
+ const proc = 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 = tmpdir();
158
+ if (!tempFolder.startsWith(tmpRoot)) throw new Error(`[cleanupPublished] refusing to remove "${tempFolder}" — not under ${tmpRoot}`);
159
+ await 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 = 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 mkdtemp(join(tmpdir(), `verdaccio-e2e-ui-unpublish-`));
215
+ const registryHost = input.registryUrl.replace(/^https?:/, "");
216
+ await writeFile(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 rm(workingFolder, {
235
+ recursive: true,
236
+ force: true
237
+ }).catch(() => void 0);
238
+ }
239
+ }
240
+ //#endregion
241
+ export { cleanupPublished, publishPackage, unpublishPackage };
242
+
243
+ //# sourceMappingURL=publish.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish.js","names":[],"sources":["../../../src/tasks/publish.ts"],"sourcesContent":["import { spawn } from 'child_process';\nimport { mkdtemp, rm, writeFile } from 'fs/promises';\nimport { tmpdir } from 'os';\nimport { join } from 'path';\n\nexport interface PublishPackageInput {\n pkgName: string;\n version?: string;\n registryUrl: string;\n credentials: { user: string; password: string };\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n /**\n * When true, append a timestamp suffix to the version so reruns against\n * a persistent registry don't collide on 403. The suffix is a valid\n * semver prerelease, e.g. `1.0.0-t1712345678`. Defaults to false.\n */\n unique?: boolean;\n}\n\n/**\n * Shape of the argument passed to `cy.task('publishPackage', ...)`.\n *\n * `registryUrl` and `credentials` are optional here (they fall back to\n * whatever was configured in `setupVerdaccioTasks`), but `pkgName` is\n * still required — every publish needs a name.\n */\nexport type PublishPackageTaskInput = Partial<\n Omit<PublishPackageInput, 'pkgName'>\n> & {\n pkgName: string;\n};\n\nexport interface PublishPackageResult {\n pkgName: string;\n version: string;\n tempFolder: string;\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\nfunction sanitizeFolderName(name: string): string {\n return name.replace(/[^a-zA-Z0-9-_]/g, '-');\n}\n\n/**\n * Obtain a registry-API-compatible (legacy) auth token for publish.\n *\n * Strategy: create a throwaway user per call. `PUT /-/user/org.couchdb.user:<name>`\n * only returns a token on CREATE (and 409s on existing users), so we\n * guarantee success by generating a unique username each time.\n *\n * We need a legacy token specifically because:\n * - Verdaccio's default API middleware accepts legacy tokens but NOT\n * JWTs from `/-/verdaccio/sec/login`\n * - Modern npm (>= 10.x) refuses to run `npm publish` at all without\n * an `_authToken` entry in `.npmrc`, even against a registry that\n * allows `$anonymous` publish — it errors out client-side\n *\n * The throwaway user stays in the test registry's htpasswd store after\n * the run, which is fine for ephemeral CI environments and local temp\n * setups (both wipe storage between runs).\n */\nasync function obtainLegacyToken(\n registryUrl: string\n): Promise<{ user: string; token: string }> {\n const user = `e2e-bot-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n const password = 'e2e-bot-password';\n const base = registryUrl.replace(/\\/$/, '');\n const url = `${base}/-/user/org.couchdb.user:${encodeURIComponent(user)}`;\n const res = await fetch(url, {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n name: user,\n password,\n _id: `org.couchdb.user:${user}`,\n type: 'user',\n roles: [],\n }),\n });\n if (!res.ok) {\n const body = await res.text();\n throw new Error(\n `[publishPackage] failed to create throwaway user \"${user}\" ` +\n `(HTTP ${res.status}): ${body}`\n );\n }\n const json = (await res.json()) as { token?: string };\n if (!json.token) {\n throw new Error(\n `[publishPackage] user creation response did not contain a token: ${JSON.stringify(\n json\n )}`\n );\n }\n return { user, token: json.token };\n}\n\nasync function createTempProject(\n pkgName: string,\n version: string,\n registryUrl: string,\n token: string,\n dependencies: Record<string, string>,\n devDependencies: Record<string, string>\n): Promise<string> {\n const tempFolder = await mkdtemp(\n join(tmpdir(), `verdaccio-e2e-ui-${sanitizeFolderName(pkgName)}-`)\n );\n const manifest = {\n name: pkgName,\n version,\n description: `e2e test fixture ${pkgName}`,\n main: 'index.js',\n dependencies,\n devDependencies,\n keywords: ['verdaccio', 'e2e', 'test'],\n author: 'Verdaccio E2E <verdaccio@example.org>',\n license: 'MIT',\n // Scoped packages default to `restricted` access which modern npm\n // refuses to publish anonymously. Pin it to `public` so the CLI\n // skips that check for fixtures like `@verdaccio/pkg-scoped`.\n publishConfig: {\n access: 'public',\n registry: registryUrl,\n },\n };\n await writeFile(join(tempFolder, 'package.json'), JSON.stringify(manifest, null, 2));\n await writeFile(\n join(tempFolder, 'README.md'),\n `# ${pkgName}\\n\\nPublished by @verdaccio/e2e-ui for e2e testing.\\n`\n );\n await writeFile(\n join(tempFolder, 'index.js'),\n `module.exports = ${JSON.stringify(pkgName)};\\n`\n );\n\n // `.npmrc` — include an `_authToken` scoped to the registry host so\n // modern npm (>= 10.x) is willing to run `npm publish`. Without this\n // the CLI errors client-side with \"This command requires you to be\n // logged in.\" even against a registry configured for `$anonymous`\n // publish. The token is a legacy Verdaccio auth token obtained by\n // creating a throwaway user via `PUT /-/user/...`.\n const registryHost = registryUrl.replace(/^https?:/, '');\n const npmrc = [\n `registry=${registryUrl}`,\n `${registryHost}/:_authToken=${token}`,\n // Force public access for scoped packages.\n 'access=public',\n '',\n ].join('\\n');\n await writeFile(join(tempFolder, '.npmrc'), npmrc);\n\n return tempFolder;\n}\n\nfunction spawnNpmPublish(\n cwd: string,\n registryUrl: string\n): Promise<{ stdout: string; stderr: string; exitCode: number }> {\n return new Promise((resolvePromise, rejectPromise) => {\n // `--tag latest` is required so npm accepts prerelease versions\n // (e.g. `1.0.0-t<ts>` when `unique: true`). Without it npm bails\n // with \"You must specify a tag using --tag when publishing a\n // prerelease version.\" For non-prerelease versions it's a no-op.\n const proc = spawn(\n 'npm',\n [\n 'publish',\n '--registry',\n registryUrl,\n '--tag',\n 'latest',\n '--loglevel=error',\n ],\n {\n cwd,\n env: { ...process.env },\n }\n );\n let stdout = '';\n let stderr = '';\n proc.stdout.on('data', (chunk) => {\n stdout += chunk.toString();\n });\n proc.stderr.on('data', (chunk) => {\n stderr += chunk.toString();\n });\n proc.on('error', rejectPromise);\n proc.on('close', (code) => {\n resolvePromise({ stdout, stderr, exitCode: code ?? -1 });\n });\n });\n}\n\n/**\n * Publish a throwaway npm package to the target Verdaccio registry.\n *\n * Flow:\n * 1. Create a throwaway user via `PUT /-/user/...` and capture its\n * legacy auth token. See `obtainLegacyToken` for why.\n * 2. Scaffold a temp project with `package.json`, `README.md`,\n * `index.js`, and an `.npmrc` that includes the token.\n * 3. Spawn `npm publish` from that temp dir.\n *\n * `input.credentials` is kept on the signature for forward-compat but\n * is currently unused — each call mints its own throwaway user.\n *\n * Throws on non-zero npm exit. Returns the temp folder path on success\n * so callers can inspect or clean up.\n */\nexport async function publishPackage(\n input: PublishPackageInput\n): Promise<PublishPackageResult> {\n const baseVersion = input.version ?? '1.0.0';\n const version = input.unique ? `${baseVersion}-t${Date.now()}` : baseVersion;\n\n const { token } = await obtainLegacyToken(input.registryUrl);\n\n const tempFolder = await createTempProject(\n input.pkgName,\n version,\n input.registryUrl,\n token,\n input.dependencies ?? {},\n input.devDependencies ?? {}\n );\n const { stdout, stderr, exitCode } = await spawnNpmPublish(\n tempFolder,\n input.registryUrl\n );\n if (exitCode !== 0) {\n throw new Error(\n `[publishPackage] npm publish failed for ${input.pkgName}@${version} ` +\n `(exit ${exitCode}):\\n${stderr || stdout}`\n );\n }\n return { pkgName: input.pkgName, version, tempFolder, stdout, stderr, exitCode };\n}\n\n/**\n * Remove a temp project folder previously created by publishPackage.\n * Safe to call with a missing path or a path outside the OS tmp dir —\n * in the latter case it refuses rather than rm-rf'ing arbitrary paths.\n */\nexport async function cleanupPublished(tempFolder: string): Promise<void> {\n if (!tempFolder) return;\n const tmpRoot = tmpdir();\n if (!tempFolder.startsWith(tmpRoot)) {\n throw new Error(\n `[cleanupPublished] refusing to remove \"${tempFolder}\" — not under ${tmpRoot}`\n );\n }\n await rm(tempFolder, { recursive: true, force: true });\n}\n\nexport interface UnpublishPackageInput {\n registryUrl: string;\n pkgName: string;\n /**\n * Temp folder returned by a previous `publishPackage` call. If\n * provided, its existing `.npmrc` (which already carries a legacy\n * auth token) is reused for the unpublish call — no extra throwaway\n * user is minted. Otherwise this task creates its own.\n */\n tempFolder?: string;\n}\n\nexport interface UnpublishPackageResult {\n pkgName: string;\n stdout: string;\n stderr: string;\n exitCode: number;\n /**\n * True if the package was already absent (HTTP 404 from the\n * registry). Tests typically want to treat this as success.\n */\n alreadyGone: boolean;\n}\n\nfunction spawnNpmUnpublish(\n cwd: string,\n registryUrl: string,\n pkgSpec: string\n): Promise<{ stdout: string; stderr: string; exitCode: number }> {\n return new Promise((resolvePromise, rejectPromise) => {\n const proc = spawn(\n 'npm',\n [\n 'unpublish',\n pkgSpec,\n '--force',\n '--registry',\n registryUrl,\n '--loglevel=error',\n ],\n {\n cwd,\n env: { ...process.env },\n }\n );\n let stdout = '';\n let stderr = '';\n proc.stdout.on('data', (chunk) => {\n stdout += chunk.toString();\n });\n proc.stderr.on('data', (chunk) => {\n stderr += chunk.toString();\n });\n proc.on('error', rejectPromise);\n proc.on('close', (code) => {\n resolvePromise({ stdout, stderr, exitCode: code ?? -1 });\n });\n });\n}\n\n/**\n * Unpublish a package from the target registry so subsequent tests\n * start from a clean slate.\n *\n * If `tempFolder` is provided (typically from a prior `publishPackage`\n * result), its `.npmrc` is reused so no extra throwaway user is\n * minted. Otherwise this function creates its own temp folder + token.\n * Either way the temp folder used by THIS call is removed on exit\n * (but callers still own the lifecycle of a tempFolder they passed in).\n *\n * Treats HTTP 404 / \"tarball does not exist\" as success so reruns and\n * parallel teardowns don't flap.\n */\nexport async function unpublishPackage(\n input: UnpublishPackageInput\n): Promise<UnpublishPackageResult> {\n let workingFolder = input.tempFolder;\n let ownsWorkingFolder = false;\n\n if (!workingFolder) {\n ownsWorkingFolder = true;\n const { token } = await obtainLegacyToken(input.registryUrl);\n workingFolder = await mkdtemp(\n join(tmpdir(), `verdaccio-e2e-ui-unpublish-`)\n );\n const registryHost = input.registryUrl.replace(/^https?:/, '');\n await writeFile(\n join(workingFolder, '.npmrc'),\n [\n `registry=${input.registryUrl}`,\n `${registryHost}/:_authToken=${token}`,\n '',\n ].join('\\n')\n );\n }\n\n try {\n const { stdout, stderr, exitCode } = await spawnNpmUnpublish(\n workingFolder,\n input.registryUrl,\n input.pkgName\n );\n\n // Treat \"already absent\" as success. npm prints slightly different\n // messages depending on version — match loosely.\n const alreadyGone =\n /404|not found|no such package|does not (exist|match)/i.test(\n `${stderr}\\n${stdout}`\n );\n\n if (exitCode !== 0 && !alreadyGone) {\n throw new Error(\n `[unpublishPackage] npm unpublish failed for ${input.pkgName} ` +\n `(exit ${exitCode}):\\n${stderr || stdout}`\n );\n }\n\n return {\n pkgName: input.pkgName,\n stdout,\n stderr,\n exitCode,\n alreadyGone,\n };\n } finally {\n if (ownsWorkingFolder && workingFolder) {\n await rm(workingFolder, { recursive: true, force: true }).catch(\n () => undefined\n );\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,116 @@
1
+ //#region src/testIds.ts
2
+ /**
3
+ * Defaults matching Verdaccio 6.x (bundled ui-theme@9.0.0-next-9.x).
4
+ * Overridable via `createRegistryConfig({ testIds: { ... } })`.
5
+ */
6
+ var DEFAULT_TEST_IDS = {
7
+ home: {
8
+ helpCard: "help-card",
9
+ notFound: "404"
10
+ },
11
+ header: {
12
+ container: "header",
13
+ innerNavBar: "inner-nav-bar",
14
+ right: "header-right",
15
+ searchContainer: "search-container",
16
+ defaultLogo: "default-logo",
17
+ customLogo: "custom-logo",
18
+ loginButton: "header--button-login",
19
+ settingsTooltip: "header--tooltip-settings",
20
+ infoTooltip: "header--tooltip-info",
21
+ themeSwitchLight: "header--button--light",
22
+ themeSwitchDark: "header--button--dark",
23
+ logInDialogIcon: "logInDialogIcon",
24
+ logOutDialogIcon: "logOutDialogIcon",
25
+ greetingsLabel: "greetings-label"
26
+ },
27
+ footer: {
28
+ container: "footer",
29
+ version: "version-footer"
30
+ },
31
+ login: {
32
+ dialog: "login--dialog",
33
+ dialogContent: "dialogContentLogin",
34
+ error: "error"
35
+ },
36
+ package: {
37
+ itemList: "package-item-list",
38
+ title: "package-title",
39
+ readme: "readme",
40
+ sidebar: "sidebar",
41
+ installList: "installList",
42
+ installNpm: "installListItem-npm",
43
+ installYarn: "installListItem-yarn",
44
+ installPnpm: "installListItem-pnpm",
45
+ keywordList: "keyword-list",
46
+ dependenciesTab: "dependencies-tab",
47
+ dependencies: "dependencies",
48
+ versionsTab: "versions-tab",
49
+ tagLatest: "tag-latest",
50
+ uplinksTab: "uplinks-tab",
51
+ noUplinks: "no-uplinks",
52
+ downloadTarballBtn: "download-tarball-btn",
53
+ rawBtn: "raw-btn",
54
+ rawViewerDialog: "rawViewer--dialog",
55
+ closeRawViewer: "close-raw-viewer"
56
+ }
57
+ };
58
+ /**
59
+ * Defaults for non-testid CSS selectors. Overridable via
60
+ * `createRegistryConfig({ selectors: { ... } })`.
61
+ */
62
+ var DEFAULT_SELECTORS = {
63
+ markdownBody: ".markdown-body",
64
+ loginDialog: {
65
+ usernameInput: "#login--dialog-username",
66
+ passwordInput: "#login--dialog-password",
67
+ submitButton: "#login--dialog-button-submit"
68
+ }
69
+ };
70
+ /**
71
+ * Merge user overrides into the default testIds map. Merging is
72
+ * per-section (one level deep): `overrides.header` replaces individual
73
+ * fields under `defaults.header` without touching `defaults.footer`.
74
+ * The shape is fixed and small, so we enumerate sections by hand
75
+ * rather than relying on a recursive generic merger.
76
+ */
77
+ function mergeTestIds(defaults, overrides) {
78
+ if (!overrides) return defaults;
79
+ return {
80
+ home: {
81
+ ...defaults.home,
82
+ ...overrides.home
83
+ },
84
+ header: {
85
+ ...defaults.header,
86
+ ...overrides.header
87
+ },
88
+ footer: {
89
+ ...defaults.footer,
90
+ ...overrides.footer
91
+ },
92
+ login: {
93
+ ...defaults.login,
94
+ ...overrides.login
95
+ },
96
+ package: {
97
+ ...defaults.package,
98
+ ...overrides.package
99
+ }
100
+ };
101
+ }
102
+ /** Same idea as `mergeTestIds`, for the CSS-selector block. */
103
+ function mergeSelectors(defaults, overrides) {
104
+ if (!overrides) return defaults;
105
+ return {
106
+ markdownBody: overrides.markdownBody ?? defaults.markdownBody,
107
+ loginDialog: {
108
+ ...defaults.loginDialog,
109
+ ...overrides.loginDialog
110
+ }
111
+ };
112
+ }
113
+ //#endregion
114
+ export { DEFAULT_SELECTORS, DEFAULT_TEST_IDS, mergeSelectors, mergeTestIds };
115
+
116
+ //# sourceMappingURL=testIds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testIds.js","names":[],"sources":["../../src/testIds.ts"],"sourcesContent":["/**\n * Configurable DOM selectors used by the e2e-ui test suites.\n *\n * The Verdaccio UI is a moving target — data-testids can and do change\n * between majors — so every selector referenced by a test lives here\n * and can be overridden by consumers of `@verdaccio/e2e-ui` via\n * `createRegistryConfig({ testIds, selectors })`.\n *\n * The defaults below match Verdaccio 6.x as of the last time we audited\n * the ui-components source. If a selector moves, override just the\n * affected field instead of forking the suite.\n */\n\n/**\n * Map of data-testid values used by the test suites, grouped by UI\n * section. Each field holds the bare testid string (no `data-testid=\"…\"`\n * wrapping) — the test helpers pass it through `cy.getByTestId(...)`.\n */\nexport interface TestIds {\n home: {\n /** Help card shown on the empty-registry landing page. */\n helpCard: string;\n /** 404 \"not found\" container. */\n notFound: string;\n };\n header: {\n /** Outermost `<NavBar>` element. */\n container: string;\n /** Inner wrapper inside the nav bar. */\n innerNavBar: string;\n /** Right-side action cluster wrapper. */\n right: string;\n /** Wrapper around the header search input. */\n searchContainer: string;\n /** Default SVG Verdaccio logo. */\n defaultLogo: string;\n /** Custom (user-provided) logo image. */\n customLogo: string;\n /** \"Login\" button shown when logged out. */\n loginButton: string;\n /** Gear icon that opens the settings dialog. */\n settingsTooltip: string;\n /** Info icon that opens the registry info dialog. */\n infoTooltip: string;\n /**\n * Theme switch button shown while in LIGHT mode (clicking it\n * flips to dark). The underlying component swaps between this\n * and `themeSwitchDark` based on `isDarkMode`.\n */\n themeSwitchLight: string;\n /** Theme switch button shown while in DARK mode. */\n themeSwitchDark: string;\n /** Menu icon shown after login (opens the logged-in menu). */\n logInDialogIcon: string;\n /** \"Log out\" entry inside the logged-in menu. */\n logOutDialogIcon: string;\n /** \"Hi <username>\" label inside the logged-in menu. */\n greetingsLabel: string;\n };\n footer: {\n /** Outer footer wrapper. */\n container: string;\n /** \"Powered by\" version text on the right side of the footer. */\n version: string;\n };\n login: {\n /** Login dialog container (the MUI Dialog root). */\n dialog: string;\n /** DialogContent wrapper inside the login dialog. */\n dialogContent: string;\n /**\n * Error banner shown inside the login dialog when the server\n * rejects credentials (or any other `errors.root` message the form\n * sets). Renders inside the `LoginDialogFormError` component.\n */\n error: string;\n };\n package: {\n /** Wrapper around the list of packages on the home page. */\n itemList: string;\n /** Package name link in the package list (home + search results). */\n title: string;\n /** Readme container on the package detail page. */\n readme: string;\n /** Sidebar container on the package detail page. */\n sidebar: string;\n /** Install commands section list. */\n installList: string;\n /** Individual install line for npm. */\n installNpm: string;\n /** Individual install line for yarn. */\n installYarn: string;\n /** Individual install line for pnpm. */\n installPnpm: string;\n /** Keyword list below the install section. */\n keywordList: string;\n /** Tab that reveals the dependencies view. */\n dependenciesTab: string;\n /** Dependencies list wrapper (one entry per dep). */\n dependencies: string;\n /** Tab that reveals the versions view. */\n versionsTab: string;\n /** \"latest\" tag row inside the versions view. */\n tagLatest: string;\n /** Tab that reveals the uplinks view. */\n uplinksTab: string;\n /** Empty-state message when the package has no uplinks. */\n noUplinks: string;\n /** Action-bar tarball download FAB. */\n downloadTarballBtn: string;\n /** Action-bar \"view raw manifest\" FAB. */\n rawBtn: string;\n /** Full-screen dialog that opens when `rawBtn` is clicked. */\n rawViewerDialog: string;\n /** Close button inside the raw viewer dialog. */\n closeRawViewer: string;\n };\n}\n\n/**\n * CSS selectors (not data-testids) used by the test suites. These are\n * things like form-field IDs and framework-specific class names that\n * Verdaccio's UI exposes as plain selectors rather than testids.\n */\nexport interface Selectors {\n /** Class applied to the parsed README markdown body. */\n markdownBody: string;\n loginDialog: {\n /** Username text input inside the login dialog. */\n usernameInput: string;\n /** Password text input inside the login dialog. */\n passwordInput: string;\n /** Submit button inside the login dialog. */\n submitButton: string;\n };\n}\n\n/**\n * Defaults matching Verdaccio 6.x (bundled ui-theme@9.0.0-next-9.x).\n * Overridable via `createRegistryConfig({ testIds: { ... } })`.\n */\nexport const DEFAULT_TEST_IDS: TestIds = {\n home: {\n helpCard: 'help-card',\n notFound: '404',\n },\n header: {\n container: 'header',\n innerNavBar: 'inner-nav-bar',\n right: 'header-right',\n searchContainer: 'search-container',\n defaultLogo: 'default-logo',\n customLogo: 'custom-logo',\n loginButton: 'header--button-login',\n settingsTooltip: 'header--tooltip-settings',\n infoTooltip: 'header--tooltip-info',\n themeSwitchLight: 'header--button--light',\n themeSwitchDark: 'header--button--dark',\n logInDialogIcon: 'logInDialogIcon',\n logOutDialogIcon: 'logOutDialogIcon',\n greetingsLabel: 'greetings-label',\n },\n footer: {\n container: 'footer',\n version: 'version-footer',\n },\n login: {\n dialog: 'login--dialog',\n dialogContent: 'dialogContentLogin',\n error: 'error',\n },\n package: {\n itemList: 'package-item-list',\n title: 'package-title',\n readme: 'readme',\n sidebar: 'sidebar',\n installList: 'installList',\n installNpm: 'installListItem-npm',\n installYarn: 'installListItem-yarn',\n installPnpm: 'installListItem-pnpm',\n keywordList: 'keyword-list',\n dependenciesTab: 'dependencies-tab',\n dependencies: 'dependencies',\n versionsTab: 'versions-tab',\n tagLatest: 'tag-latest',\n uplinksTab: 'uplinks-tab',\n noUplinks: 'no-uplinks',\n downloadTarballBtn: 'download-tarball-btn',\n rawBtn: 'raw-btn',\n rawViewerDialog: 'rawViewer--dialog',\n closeRawViewer: 'close-raw-viewer',\n },\n};\n\n/**\n * Defaults for non-testid CSS selectors. Overridable via\n * `createRegistryConfig({ selectors: { ... } })`.\n */\nexport const DEFAULT_SELECTORS: Selectors = {\n markdownBody: '.markdown-body',\n loginDialog: {\n usernameInput: '#login--dialog-username',\n passwordInput: '#login--dialog-password',\n submitButton: '#login--dialog-button-submit',\n },\n};\n\n/** Deep-partial helper — every field of a nested object becomes optional. */\nexport type DeepPartial<T> = {\n [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];\n};\n\n/**\n * Merge user overrides into the default testIds map. Merging is\n * per-section (one level deep): `overrides.header` replaces individual\n * fields under `defaults.header` without touching `defaults.footer`.\n * The shape is fixed and small, so we enumerate sections by hand\n * rather than relying on a recursive generic merger.\n */\nexport function mergeTestIds(\n defaults: TestIds,\n overrides?: DeepPartial<TestIds>\n): TestIds {\n if (!overrides) return defaults;\n return {\n home: { ...defaults.home, ...overrides.home },\n header: { ...defaults.header, ...overrides.header },\n footer: { ...defaults.footer, ...overrides.footer },\n login: { ...defaults.login, ...overrides.login },\n package: { ...defaults.package, ...overrides.package },\n };\n}\n\n/** Same idea as `mergeTestIds`, for the CSS-selector block. */\nexport function mergeSelectors(\n defaults: Selectors,\n overrides?: DeepPartial<Selectors>\n): Selectors {\n if (!overrides) return defaults;\n return {\n markdownBody: overrides.markdownBody ?? defaults.markdownBody,\n loginDialog: { ...defaults.loginDialog, ...overrides.loginDialog },\n };\n}\n"],"mappings":";;;;;AA6IA,IAAa,mBAA4B;CACvC,MAAM;EACJ,UAAU;EACV,UAAU;EACX;CACD,QAAQ;EACN,WAAW;EACX,aAAa;EACb,OAAO;EACP,iBAAiB;EACjB,aAAa;EACb,YAAY;EACZ,aAAa;EACb,iBAAiB;EACjB,aAAa;EACb,kBAAkB;EAClB,iBAAiB;EACjB,iBAAiB;EACjB,kBAAkB;EAClB,gBAAgB;EACjB;CACD,QAAQ;EACN,WAAW;EACX,SAAS;EACV;CACD,OAAO;EACL,QAAQ;EACR,eAAe;EACf,OAAO;EACR;CACD,SAAS;EACP,UAAU;EACV,OAAO;EACP,QAAQ;EACR,SAAS;EACT,aAAa;EACb,YAAY;EACZ,aAAa;EACb,aAAa;EACb,aAAa;EACb,iBAAiB;EACjB,cAAc;EACd,aAAa;EACb,WAAW;EACX,YAAY;EACZ,WAAW;EACX,oBAAoB;EACpB,QAAQ;EACR,iBAAiB;EACjB,gBAAgB;EACjB;CACF;;;;;AAMD,IAAa,oBAA+B;CAC1C,cAAc;CACd,aAAa;EACX,eAAe;EACf,eAAe;EACf,cAAc;EACf;CACF;;;;;;;;AAcD,SAAgB,aACd,UACA,WACS;AACT,KAAI,CAAC,UAAW,QAAO;AACvB,QAAO;EACL,MAAM;GAAE,GAAG,SAAS;GAAM,GAAG,UAAU;GAAM;EAC7C,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;GAAQ;EACnD,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;GAAQ;EACnD,OAAO;GAAE,GAAG,SAAS;GAAO,GAAG,UAAU;GAAO;EAChD,SAAS;GAAE,GAAG,SAAS;GAAS,GAAG,UAAU;GAAS;EACvD;;;AAIH,SAAgB,eACd,UACA,WACW;AACX,KAAI,CAAC,UAAW,QAAO;AACvB,QAAO;EACL,cAAc,UAAU,gBAAgB,SAAS;EACjD,aAAa;GAAE,GAAG,SAAS;GAAa,GAAG,UAAU;GAAa;EACnE"}
@@ -1,7 +1,11 @@
1
1
  //#region src/tests/home.ts
2
2
  function homeTests(config) {
3
+ const { home, header, package: pkg } = config.testIds;
4
+ const { features } = config;
5
+ const registerPublishedPackageBlock = features.home.publishedPackageRendering;
3
6
  describe("home", () => {
4
7
  beforeEach(() => {
8
+ cy.intercept("GET", "/-/verdaccio/data/packages").as("pkgs");
5
9
  cy.visit(config.registryUrl);
6
10
  cy.get("body").should("be.visible");
7
11
  });
@@ -12,19 +16,71 @@ function homeTests(config) {
12
16
  cy.location("pathname").should("include", "/");
13
17
  cy.title().should("eq", config.title);
14
18
  });
19
+ it("should fetch the package list from the API", () => {
20
+ cy.wait("@pkgs", { timeout: 1e4 }).then((interception) => {
21
+ const status = interception.response?.statusCode;
22
+ expect(status).to.be.oneOf([200, 304]);
23
+ if (status === 200) expect(interception.response?.body).to.be.an("array");
24
+ });
25
+ });
15
26
  it("should match title with no packages published", () => {
16
- cy.getByTestId("help-card", { timeout: 1e4 }).should("be.visible");
17
- cy.getByTestId("help-card").contains("No Package Published Yet.");
27
+ cy.wait("@pkgs");
28
+ cy.getByTestId(home.helpCard, { timeout: 1e4 }).should("be.visible");
29
+ cy.getByTestId(home.helpCard).contains("No Package Published Yet.");
18
30
  });
19
31
  it("should display instructions on help card", () => {
20
- cy.getByTestId("help-card", { timeout: 1e4 }).should("be.visible");
21
- cy.getByTestId("help-card").contains(`npm adduser --registry ${config.registryUrl}`);
22
- cy.getByTestId("help-card").contains(`npm publish --registry ${config.registryUrl}`);
32
+ cy.wait("@pkgs");
33
+ cy.getByTestId(home.helpCard, { timeout: 1e4 }).should("be.visible");
34
+ cy.getByTestId(home.helpCard).contains(`npm adduser --registry ${config.registryUrl}`);
35
+ cy.getByTestId(home.helpCard).contains(`npm publish --registry ${config.registryUrl}`);
36
+ });
37
+ it("should render the header logo and login button", () => {
38
+ cy.getByTestId(header.container).should("be.visible");
39
+ cy.get(`[data-testid="${header.defaultLogo}"], [data-testid="${header.customLogo}"]`).should("be.visible");
40
+ cy.getByTestId(header.loginButton).should("be.visible");
41
+ });
42
+ it("should navigate back to home when clicking the header logo", () => {
43
+ cy.visit(`${config.registryUrl}/-/web/detail/@verdaccio/not-found`);
44
+ cy.getByTestId(home.notFound, { timeout: 1e4 }).should("be.visible");
45
+ cy.get(`[data-testid="${header.defaultLogo}"], [data-testid="${header.customLogo}"]`).first().click();
46
+ cy.location("pathname").should("eq", "/");
47
+ cy.getByTestId(home.helpCard).should("be.visible");
23
48
  });
24
49
  it("should go to 404 page", () => {
25
50
  cy.visit(`${config.registryUrl}/-/web/detail/@verdaccio/not-found`);
26
- cy.getByTestId("404", { timeout: 1e4 }).should("be.visible");
27
- cy.getByTestId("404").contains("Sorry, we couldn't find it.");
51
+ cy.getByTestId(home.notFound, { timeout: 1e4 }).should("be.visible");
52
+ cy.getByTestId(home.notFound).contains("Sorry, we couldn't find it.");
53
+ });
54
+ (registerPublishedPackageBlock ? describe : describe.skip)("with a published package", () => {
55
+ const pkgName = "@verdaccio/home-fixture";
56
+ let tempFolder = null;
57
+ beforeEach(() => {
58
+ cy.task("publishPackage", {
59
+ pkgName,
60
+ version: "1.0.0",
61
+ unique: true
62
+ }).then((result) => {
63
+ tempFolder = result?.tempFolder ?? null;
64
+ });
65
+ cy.visit(config.registryUrl);
66
+ });
67
+ afterEach(() => {
68
+ cy.task("unpublishPackage", {
69
+ pkgName,
70
+ tempFolder: tempFolder ?? void 0
71
+ });
72
+ if (tempFolder) cy.task("cleanupPublished", tempFolder);
73
+ tempFolder = null;
74
+ });
75
+ it("should render the package list when a package exists", () => {
76
+ cy.wait("@pkgs");
77
+ cy.getByTestId(pkg.itemList).should("be.visible");
78
+ cy.getByTestId(pkg.title).should("have.length.at.least", 1);
79
+ });
80
+ it("should show the published package name in the list", () => {
81
+ cy.wait("@pkgs");
82
+ cy.contains(`[data-testid="${pkg.title}"]`, pkgName).should("be.visible");
83
+ });
28
84
  });
29
85
  });
30
86
  }
@@ -1 +1 @@
1
- {"version":3,"file":"home.js","names":[],"sources":["../../../src/tests/home.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\nimport { RegistryConfig } from '../types';\n\nexport function homeTests(config: RegistryConfig) {\n describe('home', () => {\n beforeEach(() => {\n cy.visit(config.registryUrl);\n // Wait for the app to render\n cy.get('body').should('be.visible');\n });\n\n afterEach(() => {\n cy.wait(2000);\n });\n\n it('title should be correct', () => {\n cy.location('pathname').should('include', '/');\n cy.title().should('eq', config.title);\n });\n\n it('should match title with no packages published', () => {\n cy.getByTestId('help-card', { timeout: 10000 }).should('be.visible');\n cy.getByTestId('help-card').contains('No Package Published Yet.');\n });\n\n it('should display instructions on help card', () => {\n cy.getByTestId('help-card', { timeout: 10000 }).should('be.visible');\n cy.getByTestId('help-card').contains(\n `npm adduser --registry ${config.registryUrl}`\n );\n cy.getByTestId('help-card').contains(\n `npm publish --registry ${config.registryUrl}`\n );\n });\n\n it('should go to 404 page', () => {\n cy.visit(`${config.registryUrl}/-/web/detail/@verdaccio/not-found`);\n cy.getByTestId('404', { timeout: 10000 }).should('be.visible');\n cy.getByTestId('404').contains(\"Sorry, we couldn't find it.\");\n });\n });\n}\n"],"mappings":";AAIA,SAAgB,UAAU,QAAwB;AAChD,UAAS,cAAc;AACrB,mBAAiB;AACf,MAAG,MAAM,OAAO,YAAY;AAE5B,MAAG,IAAI,OAAO,CAAC,OAAO,aAAa;IACnC;AAEF,kBAAgB;AACd,MAAG,KAAK,IAAK;IACb;AAEF,KAAG,iCAAiC;AAClC,MAAG,SAAS,WAAW,CAAC,OAAO,WAAW,IAAI;AAC9C,MAAG,OAAO,CAAC,OAAO,MAAM,OAAO,MAAM;IACrC;AAEF,KAAG,uDAAuD;AACxD,MAAG,YAAY,aAAa,EAAE,SAAS,KAAO,CAAC,CAAC,OAAO,aAAa;AACpE,MAAG,YAAY,YAAY,CAAC,SAAS,4BAA4B;IACjE;AAEF,KAAG,kDAAkD;AACnD,MAAG,YAAY,aAAa,EAAE,SAAS,KAAO,CAAC,CAAC,OAAO,aAAa;AACpE,MAAG,YAAY,YAAY,CAAC,SAC1B,0BAA0B,OAAO,cAClC;AACD,MAAG,YAAY,YAAY,CAAC,SAC1B,0BAA0B,OAAO,cAClC;IACD;AAEF,KAAG,+BAA+B;AAChC,MAAG,MAAM,GAAG,OAAO,YAAY,oCAAoC;AACnE,MAAG,YAAY,OAAO,EAAE,SAAS,KAAO,CAAC,CAAC,OAAO,aAAa;AAC9D,MAAG,YAAY,MAAM,CAAC,SAAS,8BAA8B;IAC7D;GACF"}
1
+ {"version":3,"file":"home.js","names":[],"sources":["../../../src/tests/home.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\nimport { RegistryConfig } from '../types';\n\nexport function homeTests(config: RegistryConfig) {\n const { home, header, package: pkg } = config.testIds;\n const { features } = config;\n\n // Only register the `with a published package` nested describe when\n // the corresponding feature flag is on. Using a plain `if` is simpler\n // than wrapping `describe` itself in `describe.skip`, and it avoids\n // emitting a pending describe block in reports.\n const registerPublishedPackageBlock = features.home.publishedPackageRendering;\n\n describe('home', () => {\n beforeEach(() => {\n cy.intercept('GET', '/-/verdaccio/data/packages').as('pkgs');\n cy.visit(config.registryUrl);\n // Wait for the app to render\n cy.get('body').should('be.visible');\n });\n\n afterEach(() => {\n cy.wait(2000);\n });\n\n it('title should be correct', () => {\n cy.location('pathname').should('include', '/');\n cy.title().should('eq', config.title);\n });\n\n it('should fetch the package list from the API', () => {\n // The home page always fires /-/verdaccio/data/packages on mount;\n // verifying the endpoint is healthy is the cheapest way to catch\n // registry-side regressions before any DOM assertions.\n //\n // Accept both 200 (fresh fetch) and 304 (cache hit from a prior\n // spec run in the same session) — both indicate a healthy endpoint.\n // 304 responses have no body, so only assert array-shape on 200.\n cy.wait('@pkgs', { timeout: 10000 }).then((interception) => {\n const status = interception.response?.statusCode;\n expect(status).to.be.oneOf([200, 304]);\n if (status === 200) {\n expect(interception.response?.body).to.be.an('array');\n }\n });\n });\n\n it('should match title with no packages published', () => {\n cy.wait('@pkgs');\n cy.getByTestId(home.helpCard, { timeout: 10000 }).should('be.visible');\n cy.getByTestId(home.helpCard).contains('No Package Published Yet.');\n });\n\n it('should display instructions on help card', () => {\n cy.wait('@pkgs');\n cy.getByTestId(home.helpCard, { timeout: 10000 }).should('be.visible');\n cy.getByTestId(home.helpCard).contains(\n `npm adduser --registry ${config.registryUrl}`\n );\n cy.getByTestId(home.helpCard).contains(\n `npm publish --registry ${config.registryUrl}`\n );\n });\n\n it('should render the header logo and login button', () => {\n cy.getByTestId(header.container).should('be.visible');\n cy.get(\n `[data-testid=\"${header.defaultLogo}\"], [data-testid=\"${header.customLogo}\"]`\n ).should('be.visible');\n cy.getByTestId(header.loginButton).should('be.visible');\n });\n\n it('should navigate back to home when clicking the header logo', () => {\n // Land on the 404 page, then click the logo — URL should reset.\n cy.visit(`${config.registryUrl}/-/web/detail/@verdaccio/not-found`);\n cy.getByTestId(home.notFound, { timeout: 10000 }).should('be.visible');\n cy.get(\n `[data-testid=\"${header.defaultLogo}\"], [data-testid=\"${header.customLogo}\"]`\n )\n .first()\n .click();\n cy.location('pathname').should('eq', '/');\n cy.getByTestId(home.helpCard).should('be.visible');\n });\n\n it('should go to 404 page', () => {\n cy.visit(`${config.registryUrl}/-/web/detail/@verdaccio/not-found`);\n cy.getByTestId(home.notFound, { timeout: 10000 }).should('be.visible');\n cy.getByTestId(home.notFound).contains(\"Sorry, we couldn't find it.\");\n });\n\n // ── Rendering assertions that require real package data ─────────\n // Publishes a throwaway package before each test in this block so\n // we can verify the home page actually renders the list (not just\n // the empty state) and cleans up after each test so the outer\n // \"empty registry\" assertions above keep working in isolation.\n //\n // Gated on `features.home.publishedPackageRendering` so builds\n // where this shape doesn't apply can skip it without forking.\n (registerPublishedPackageBlock ? describe : describe.skip)('with a published package', () => {\n const pkgName = '@verdaccio/home-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 // Re-visit so the page picks up the just-published package.\n cy.visit(config.registryUrl);\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 it('should render the package list when a package exists', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.itemList).should('be.visible');\n cy.getByTestId(pkg.title).should('have.length.at.least', 1);\n });\n\n it('should show the published package name in the list', () => {\n cy.wait('@pkgs');\n cy.contains(`[data-testid=\"${pkg.title}\"]`, pkgName).should(\n 'be.visible'\n );\n });\n });\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ import { maybeIt } from "../features.js";
2
+ //#region src/tests/layout.ts
3
+ /**
4
+ * Tests for the persistent page chrome: the header (nav bar, logo,
5
+ * search container, action buttons) and the footer (version marker).
6
+ * Also asserts that the runtime UI configuration endpoint
7
+ * `/-/static/ui-options.js` loads successfully — this endpoint emits
8
+ * `window.__VERDACCIO_BASENAME_UI_OPTIONS`, which the ui-theme relies
9
+ * on for feature flags like showFooter / showSearch / showSettings. If
10
+ * it fails the whole SPA degrades to whatever defaults the provider
11
+ * ships with, so it's worth a direct network-level check.
12
+ */
13
+ function layoutTests(config) {
14
+ const { header, footer } = config.testIds;
15
+ const { features } = config;
16
+ describe("layout: header, footer, ui-options", () => {
17
+ beforeEach(() => {
18
+ cy.intercept("GET", "**/-/static/ui-options.js").as("uiOptions");
19
+ cy.visit(config.registryUrl);
20
+ cy.get("body").should("be.visible");
21
+ });
22
+ it("should load /-/static/ui-options.js with HTTP 200", () => {
23
+ cy.wait("@uiOptions", { timeout: 1e4 }).its("response.statusCode").should("eq", 200);
24
+ });
25
+ it("should serve ui-options.js with a JavaScript content-type", () => {
26
+ cy.wait("@uiOptions", { timeout: 1e4 }).then((interception) => {
27
+ const contentType = interception.response?.headers?.["content-type"] || "";
28
+ expect(contentType).to.match(/javascript/i);
29
+ });
30
+ });
31
+ it("should expose window.__VERDACCIO_BASENAME_UI_OPTIONS at runtime", () => {
32
+ cy.wait("@uiOptions", { timeout: 1e4 });
33
+ cy.window().its("__VERDACCIO_BASENAME_UI_OPTIONS").should("be.an", "object");
34
+ });
35
+ describe("header", () => {
36
+ it("should render the header container", () => {
37
+ cy.getByTestId(header.container).should("be.visible");
38
+ cy.getByTestId(header.innerNavBar).should("be.visible");
39
+ });
40
+ it("should render the logo", () => {
41
+ cy.get(`[data-testid="${header.defaultLogo}"], [data-testid="${header.customLogo}"]`).should("be.visible");
42
+ });
43
+ it("should render the search container", () => {
44
+ cy.getByTestId(header.searchContainer).should("be.visible");
45
+ });
46
+ it("should render the header-right action cluster", () => {
47
+ cy.getByTestId(header.right).should("be.visible");
48
+ });
49
+ it("should render the login button when logged out", () => {
50
+ cy.getByTestId(header.loginButton).should("be.visible");
51
+ });
52
+ it("should render the settings and info buttons", () => {
53
+ cy.getByTestId(header.settingsTooltip).should("be.visible");
54
+ cy.getByTestId(header.infoTooltip).should("be.visible");
55
+ });
56
+ maybeIt(features.layout.themeSwitch)("should toggle between light and dark mode", () => {
57
+ cy.getByTestId(header.themeSwitchLight).should("be.visible").click();
58
+ cy.getByTestId(header.themeSwitchDark, { timeout: 5e3 }).should("be.visible");
59
+ cy.getByTestId(header.themeSwitchLight).should("not.exist");
60
+ cy.window().its("localStorage").invoke("getItem", "darkMode").should("eq", "true");
61
+ cy.getByTestId(header.themeSwitchDark).click();
62
+ cy.getByTestId(header.themeSwitchLight, { timeout: 5e3 }).should("be.visible");
63
+ });
64
+ });
65
+ describe("footer", () => {
66
+ it("should render the footer container", () => {
67
+ cy.getByTestId(footer.container).scrollIntoView().should("be.visible");
68
+ });
69
+ it("should render the version marker with the powered-by label", () => {
70
+ cy.getByTestId(footer.version).scrollIntoView().should("be.visible").invoke("text").should("have.length.greaterThan", 0);
71
+ });
72
+ it("should render a logo next to the version marker", () => {
73
+ cy.getByTestId(footer.container).find(`[data-testid="${header.defaultLogo}"], [data-testid="${header.customLogo}"]`).should("exist");
74
+ });
75
+ });
76
+ });
77
+ }
78
+ //#endregion
79
+ export { layoutTests };
80
+
81
+ //# sourceMappingURL=layout.js.map