@verdaccio/e2e-ui 2.4.1 → 2.4.3

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 (41) hide show
  1. package/README.md +18 -12
  2. package/build/cjs/commands/index.cjs.map +1 -1
  3. package/build/cjs/features.cjs +1 -0
  4. package/build/cjs/features.cjs.map +1 -1
  5. package/build/cjs/index.cjs +28 -3
  6. package/build/cjs/index.cjs.map +1 -1
  7. package/build/cjs/tasks/publish.cjs.map +1 -1
  8. package/build/cjs/testIds.cjs +1 -0
  9. package/build/cjs/testIds.cjs.map +1 -1
  10. package/build/cjs/tests/change-password.cjs.map +1 -1
  11. package/build/cjs/tests/home.cjs.map +1 -1
  12. package/build/cjs/tests/layout.cjs.map +1 -1
  13. package/build/cjs/tests/publish.cjs +64 -24
  14. package/build/cjs/tests/publish.cjs.map +1 -1
  15. package/build/cjs/tests/search.cjs +0 -7
  16. package/build/cjs/tests/search.cjs.map +1 -1
  17. package/build/cjs/tests/settings.cjs.map +1 -1
  18. package/build/cjs/tests/signin.cjs.map +1 -1
  19. package/build/commands/index.d.ts +0 -2
  20. package/build/esm/commands/index.js.map +1 -1
  21. package/build/esm/features.js +1 -0
  22. package/build/esm/features.js.map +1 -1
  23. package/build/esm/index.js +28 -3
  24. package/build/esm/index.js.map +1 -1
  25. package/build/esm/tasks/publish.js.map +1 -1
  26. package/build/esm/testIds.js +1 -0
  27. package/build/esm/testIds.js.map +1 -1
  28. package/build/esm/tests/change-password.js.map +1 -1
  29. package/build/esm/tests/home.js.map +1 -1
  30. package/build/esm/tests/layout.js.map +1 -1
  31. package/build/esm/tests/publish.js +64 -24
  32. package/build/esm/tests/publish.js.map +1 -1
  33. package/build/esm/tests/search.js +0 -7
  34. package/build/esm/tests/search.js.map +1 -1
  35. package/build/esm/tests/settings.js.map +1 -1
  36. package/build/esm/tests/signin.js.map +1 -1
  37. package/build/features.d.ts +16 -6
  38. package/build/index.d.ts +1 -2
  39. package/build/testIds.d.ts +2 -0
  40. package/build/types.d.ts +0 -3
  41. package/package.json +4 -4
@@ -1 +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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"}
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<Omit<PublishPackageInput, 'pkgName'>> & {\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(registryUrl: string): 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(json)}`\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(join(tempFolder, 'index.js'), `module.exports = ${JSON.stringify(pkgName)};\\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 ['publish', '--registry', registryUrl, '--tag', 'latest', '--loglevel=error'],\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(input: PublishPackageInput): 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(tempFolder, input.registryUrl);\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(`[cleanupPublished] refusing to remove \"${tempFolder}\" — not under ${tmpRoot}`);\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 ['unpublish', pkgSpec, '--force', '--registry', registryUrl, '--loglevel=error'],\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(join(tmpdir(), `verdaccio-e2e-ui-unpublish-`));\n const registryHost = input.registryUrl.replace(/^https?:/, '');\n await writeFile(\n join(workingFolder, '.npmrc'),\n [`registry=${input.registryUrl}`, `${registryHost}/:_authToken=${token}`, ''].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 = /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(() => undefined);\n }\n }\n}\n"],"mappings":";;;;;AAwCA,SAAS,mBAAmB,MAAsB;CAChD,OAAO,KAAK,QAAQ,mBAAmB,GAAG;AAC5C;;;;;;;;;;;;;;;;;;;AAoBA,eAAe,kBAAkB,aAA+D;CAC9F,MAAM,OAAO,WAAW,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;CAC3E,MAAM,WAAW;CAEjB,MAAM,MAAM,GADC,YAAY,QAAQ,OAAO,EACzB,EAAK,2BAA2B,mBAAmB,IAAI;CACtE,MAAM,MAAM,MAAM,MAAM,KAAK;EAC3B,QAAQ;EACR,SAAS,EAAE,gBAAgB,mBAAmB;EAC9C,MAAM,KAAK,UAAU;GACnB,MAAM;GACN;GACA,KAAK,oBAAoB;GACzB,MAAM;GACN,OAAO,CAAC;EACV,CAAC;CACH,CAAC;CACD,IAAI,CAAC,IAAI,IAAI;EACX,MAAM,OAAO,MAAM,IAAI,KAAK;EAC5B,MAAM,IAAI,MACR,qDAAqD,KAAK,UAC/C,IAAI,OAAO,KAAK,MAC7B;CACF;CACA,MAAM,OAAQ,MAAM,IAAI,KAAK;CAC7B,IAAI,CAAC,KAAK,OACR,MAAM,IAAI,MACR,oEAAoE,KAAK,UAAU,IAAI,GACzF;CAEF,OAAO;EAAE;EAAM,OAAO,KAAK;CAAM;AACnC;AAEA,eAAe,kBACb,SACA,SACA,aACA,OACA,cACA,iBACiB;CACjB,MAAM,aAAa,MAAM,QACvB,KAAK,OAAO,GAAG,oBAAoB,mBAAmB,OAAO,EAAE,EAAE,CACnE;CACA,MAAM,WAAW;EACf,MAAM;EACN;EACA,aAAa,oBAAoB;EACjC,MAAM;EACN;EACA;EACA,UAAU;GAAC;GAAa;GAAO;EAAM;EACrC,QAAQ;EACR,SAAS;EAIT,eAAe;GACb,QAAQ;GACR,UAAU;EACZ;CACF;CACA,MAAM,UAAU,KAAK,YAAY,cAAc,GAAG,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC;CACnF,MAAM,UACJ,KAAK,YAAY,WAAW,GAC5B,KAAK,QAAQ,sDACf;CACA,MAAM,UAAU,KAAK,YAAY,UAAU,GAAG,oBAAoB,KAAK,UAAU,OAAO,EAAE,IAAI;CAQ9F,MAAM,eAAe,YAAY,QAAQ,YAAY,EAAE;CACvD,MAAM,QAAQ;EACZ,YAAY;EACZ,GAAG,aAAa,eAAe;EAE/B;EACA;CACF,CAAC,CAAC,KAAK,IAAI;CACX,MAAM,UAAU,KAAK,YAAY,QAAQ,GAAG,KAAK;CAEjD,OAAO;AACT;AAEA,SAAS,gBACP,KACA,aAC+D;CAC/D,OAAO,IAAI,SAAS,gBAAgB,kBAAkB;EAKpD,MAAM,OAAO,MACX,OACA;GAAC;GAAW;GAAc;GAAa;GAAS;GAAU;EAAkB,GAC5E;GACE;GACA,KAAK,EAAE,GAAG,QAAQ,IAAI;EACxB,CACF;EACA,IAAI,SAAS;EACb,IAAI,SAAS;EACb,KAAK,OAAO,GAAG,SAAS,UAAU;GAChC,UAAU,MAAM,SAAS;EAC3B,CAAC;EACD,KAAK,OAAO,GAAG,SAAS,UAAU;GAChC,UAAU,MAAM,SAAS;EAC3B,CAAC;EACD,KAAK,GAAG,SAAS,aAAa;EAC9B,KAAK,GAAG,UAAU,SAAS;GACzB,eAAe;IAAE;IAAQ;IAAQ,UAAU,QAAQ;GAAG,CAAC;EACzD,CAAC;CACH,CAAC;AACH;;;;;;;;;;;;;;;;;AAkBA,eAAsB,eAAe,OAA2D;CAC9F,MAAM,cAAc,MAAM,WAAW;CACrC,MAAM,UAAU,MAAM,SAAS,GAAG,YAAY,IAAI,KAAK,IAAI,MAAM;CAEjE,MAAM,EAAE,UAAU,MAAM,kBAAkB,MAAM,WAAW;CAE3D,MAAM,aAAa,MAAM,kBACvB,MAAM,SACN,SACA,MAAM,aACN,OACA,MAAM,gBAAgB,CAAC,GACvB,MAAM,mBAAmB,CAAC,CAC5B;CACA,MAAM,EAAE,QAAQ,QAAQ,aAAa,MAAM,gBAAgB,YAAY,MAAM,WAAW;CACxF,IAAI,aAAa,GACf,MAAM,IAAI,MACR,2CAA2C,MAAM,QAAQ,GAAG,QAAQ,SACzD,SAAS,MAAM,UAAU,QACtC;CAEF,OAAO;EAAE,SAAS,MAAM;EAAS;EAAS;EAAY;EAAQ;EAAQ;CAAS;AACjF;;;;;;AAOA,eAAsB,iBAAiB,YAAmC;CACxE,IAAI,CAAC,YAAY;CACjB,MAAM,UAAU,OAAO;CACvB,IAAI,CAAC,WAAW,WAAW,OAAO,GAChC,MAAM,IAAI,MAAM,0CAA0C,WAAW,gBAAgB,SAAS;CAEhG,MAAM,GAAG,YAAY;EAAE,WAAW;EAAM,OAAO;CAAK,CAAC;AACvD;AA0BA,SAAS,kBACP,KACA,aACA,SAC+D;CAC/D,OAAO,IAAI,SAAS,gBAAgB,kBAAkB;EACpD,MAAM,OAAO,MACX,OACA;GAAC;GAAa;GAAS;GAAW;GAAc;GAAa;EAAkB,GAC/E;GACE;GACA,KAAK,EAAE,GAAG,QAAQ,IAAI;EACxB,CACF;EACA,IAAI,SAAS;EACb,IAAI,SAAS;EACb,KAAK,OAAO,GAAG,SAAS,UAAU;GAChC,UAAU,MAAM,SAAS;EAC3B,CAAC;EACD,KAAK,OAAO,GAAG,SAAS,UAAU;GAChC,UAAU,MAAM,SAAS;EAC3B,CAAC;EACD,KAAK,GAAG,SAAS,aAAa;EAC9B,KAAK,GAAG,UAAU,SAAS;GACzB,eAAe;IAAE;IAAQ;IAAQ,UAAU,QAAQ;GAAG,CAAC;EACzD,CAAC;CACH,CAAC;AACH;;;;;;;;;;;;;;AAeA,eAAsB,iBACpB,OACiC;CACjC,IAAI,gBAAgB,MAAM;CAC1B,IAAI,oBAAoB;CAExB,IAAI,CAAC,eAAe;EAClB,oBAAoB;EACpB,MAAM,EAAE,UAAU,MAAM,kBAAkB,MAAM,WAAW;EAC3D,gBAAgB,MAAM,QAAQ,KAAK,OAAO,GAAG,6BAA6B,CAAC;EAC3E,MAAM,eAAe,MAAM,YAAY,QAAQ,YAAY,EAAE;EAC7D,MAAM,UACJ,KAAK,eAAe,QAAQ,GAC5B;GAAC,YAAY,MAAM;GAAe,GAAG,aAAa,eAAe;GAAS;EAAE,CAAC,CAAC,KAAK,IAAI,CACzF;CACF;CAEA,IAAI;EACF,MAAM,EAAE,QAAQ,QAAQ,aAAa,MAAM,kBACzC,eACA,MAAM,aACN,MAAM,OACR;EAIA,MAAM,cAAc,wDAAwD,KAC1E,GAAG,OAAO,IAAI,QAChB;EAEA,IAAI,aAAa,KAAK,CAAC,aACrB,MAAM,IAAI,MACR,+CAA+C,MAAM,QAAQ,SAClD,SAAS,MAAM,UAAU,QACtC;EAGF,OAAO;GACL,SAAS,MAAM;GACf;GACA;GACA;GACA;EACF;CACF,UAAU;EACR,IAAI,qBAAqB,eACvB,MAAM,GAAG,eAAe;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC,CAAC,CAAC,YAAY,KAAA,CAAS;CAEnF;AACF"}
@@ -50,6 +50,7 @@ var DEFAULT_TEST_IDS = {
50
50
  uplinksTab: "uplinks-tab",
51
51
  noUplinks: "no-uplinks",
52
52
  downloadTarballBtn: "download-tarball-btn",
53
+ downloadTarball: "download-tarball",
53
54
  rawBtn: "raw-btn",
54
55
  rawViewerDialog: "rawViewer--dialog",
55
56
  closeRawViewer: "close-raw-viewer"
@@ -1 +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
+ {"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 /** Package list tarball download button. */\n downloadTarball: 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 downloadTarball: 'download-tarball',\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(defaults: TestIds, overrides?: DeepPartial<TestIds>): 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(defaults: Selectors, overrides?: DeepPartial<Selectors>): Selectors {\n if (!overrides) return defaults;\n return {\n markdownBody: overrides.markdownBody ?? defaults.markdownBody,\n loginDialog: { ...defaults.loginDialog, ...overrides.loginDialog },\n };\n}\n"],"mappings":";;;;;AA+IA,IAAa,mBAA4B;CACvC,MAAM;EACJ,UAAU;EACV,UAAU;CACZ;CACA,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;CAClB;CACA,QAAQ;EACN,WAAW;EACX,SAAS;CACX;CACA,OAAO;EACL,QAAQ;EACR,eAAe;EACf,OAAO;CACT;CACA,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,iBAAiB;EACjB,QAAQ;EACR,iBAAiB;EACjB,gBAAgB;CAClB;AACF;;;;;AAMA,IAAa,oBAA+B;CAC1C,cAAc;CACd,aAAa;EACX,eAAe;EACf,eAAe;EACf,cAAc;CAChB;AACF;;;;;;;;AAcA,SAAgB,aAAa,UAAmB,WAA2C;CACzF,IAAI,CAAC,WAAW,OAAO;CACvB,OAAO;EACL,MAAM;GAAE,GAAG,SAAS;GAAM,GAAG,UAAU;EAAK;EAC5C,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;EAAO;EAClD,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;EAAO;EAClD,OAAO;GAAE,GAAG,SAAS;GAAO,GAAG,UAAU;EAAM;EAC/C,SAAS;GAAE,GAAG,SAAS;GAAS,GAAG,UAAU;EAAQ;CACvD;AACF;;AAGA,SAAgB,eAAe,UAAqB,WAA+C;CACjG,IAAI,CAAC,WAAW,OAAO;CACvB,OAAO;EACL,cAAc,UAAU,gBAAgB,SAAS;EACjD,aAAa;GAAE,GAAG,SAAS;GAAa,GAAG,UAAU;EAAY;CACnE;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"change-password.js","names":[],"sources":["../../../src/tests/change-password.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Tests for the Change Password page at /-/web/change-password.\n *\n * The page renders only when the server is started with\n * `flags.changePassword: true` (otherwise the React component\n * redirects to `/` on mount). Each test logs in first, navigates\n * directly to the page, and drives the form.\n *\n * Selector strategy: the ChangePassword form does not ship stable\n * `id`/testid attributes on its inputs, but every field is registered\n * via react-hook-form's `register('<name>')`, which sets a stable\n * `name` attribute on the underlying `<input>`. The labels themselves\n * are `t('security.changePassword.*')` calls — when the i18n bundle\n * hasn't finished loading (or isn't loaded at all on this route), MUI\n * renders the literal i18n key as the label, so any selector that\n * matches on visible label text silently misses every field and the\n * form stays empty (which would also make the mismatch test \"pass\"\n * for the wrong reason: the submit button is disabled because the\n * form is empty, not because yup rejected the mismatch).\n *\n * The stable, i18n-independent contract from ChangePassword.tsx is:\n * register('username') → input[name=\"username\"]\n * register('oldPassword') → input[name=\"oldPassword\"]\n * register('newPassword') → input[name=\"newPassword\"]\n * register('confirmPassword') → input[name=\"confirmPassword\"]\n * submit button → form button[type=\"submit\"]\n */\nexport function changePasswordTests(config: RegistryConfig) {\n const { header, login } = config.testIds;\n const { loginDialog } = config.selectors;\n const { features } = config;\n\n // The onSubmit catch block in ChangePassword.tsx sets a hardcoded\n // English string — if the upstream component ever localizes this,\n // this constant and the wrongOldPassword test will need updating.\n const GENERIC_FAILURE_TEXT = 'Failed to change password';\n\n describe('change password', () => {\n const CHANGE_PASSWORD_PATH = '/-/web/change-password';\n const { user, password } = config.credentials;\n\n /**\n * Tests mutate the user's password. We track the \"current\" value\n * across tests so the `after()` hook can restore the original,\n * leaving the registry in the same state other suites assume.\n */\n let currentPassword = password;\n\n /**\n * Capability check. The ChangePassword page's `useEffect` redirects\n * to `/` whenever `configuration.flags.changePassword` is not truthy\n * — which is the case on any registry that either (a) didn't set\n * `flags.changePassword: true` in its config, or (b) runs a\n * verdaccio build whose middleware doesn't yet propagate the flag\n * into `__VERDACCIO_BASENAME_UI_OPTIONS` (older `verdaccio:6`\n * tagged images fall in this bucket).\n *\n * In either case the suite has nothing to exercise, so skip the\n * whole describe with a clear reason rather than burning five\n * seconds per test on cy.contains timeouts.\n */\n before(function () {\n cy.visit(config.registryUrl);\n cy.window().then((win) => {\n const opts = (win as any).__VERDACCIO_BASENAME_UI_OPTIONS;\n const enabled = !!opts?.flags?.changePassword;\n if (!enabled) {\n // eslint-disable-next-line no-console\n console.warn(\n '[change-password] server did not advertise flags.changePassword=true ' +\n '— skipping suite. ui-options.flags: ' +\n JSON.stringify(opts?.flags ?? {})\n );\n this.skip();\n }\n });\n });\n\n beforeEach(() => {\n // Intercept the login POST and wait on it explicitly instead of\n // leaning on a visual sentinel — mirrors the pattern used by\n // signinTests and avoids a race between cy.login's fire-and-forget\n // submit and the next cy.visit.\n cy.intercept('POST', '/-/verdaccio/sec/login').as('signChangePwd');\n cy.visit(config.registryUrl);\n cy.login(user, currentPassword, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@signChangePwd').its('response.statusCode').should('eq', 200);\n\n cy.visit(CHANGE_PASSWORD_PATH);\n // If flags.changePassword is off server-side, the component's\n // useEffect redirects to `/` and this assertion times out — which\n // is the correct signal that the registry is misconfigured.\n // Use the stable `type=\"submit\"` selector so the assertion is\n // independent of whether the i18n bundle has resolved by now.\n cy.get('form button[type=\"submit\"]', { timeout: 5000 }).should('be.visible');\n });\n\n after(() => {\n // Restore the original password so subsequent spec files\n // (and retries) can still log in with `config.credentials`.\n if (currentPassword === password) return;\n cy.intercept('POST', '/-/verdaccio/sec/login').as('signChangePwdRestore');\n cy.visit(config.registryUrl);\n cy.login(user, currentPassword, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@signChangePwdRestore').its('response.statusCode').should('eq', 200);\n cy.visit(CHANGE_PASSWORD_PATH);\n cy.get('input[name=\"username\"]').type(user);\n cy.get('input[name=\"oldPassword\"]').type(currentPassword);\n cy.get('input[name=\"newPassword\"]').type(password);\n cy.get('input[name=\"confirmPassword\"]').type(password);\n cy.get('form button[type=\"submit\"]').click();\n currentPassword = password;\n });\n\n // ── Validation (client-side yup) ─────────────────────────────\n\n maybeIt(features.changePassword.validation)(\n 'should disable the submit button while the form is empty',\n () => {\n cy.get('form button[type=\"submit\"]').should('be.disabled');\n }\n );\n\n maybeIt(features.changePassword.validation)(\n 'should keep submit disabled when new and confirm passwords mismatch',\n () => {\n cy.get('input[name=\"username\"]').type(user);\n cy.get('input[name=\"oldPassword\"]').type(currentPassword);\n cy.get('input[name=\"newPassword\"]').type('newSecretPass123');\n cy.get('input[name=\"confirmPassword\"]').type('different-value');\n // yup schema rejects mismatch → isValid stays false → button disabled.\n cy.get('form button[type=\"submit\"]').should('be.disabled');\n }\n );\n\n // ── Server error path ────────────────────────────────────────\n\n maybeIt(features.changePassword.wrongOldPassword)(\n 'should show an error banner when the old password is wrong',\n () => {\n cy.intercept('PUT', '/-/verdaccio/sec/reset_password').as('reset');\n cy.get('input[name=\"username\"]').type(user);\n cy.get('input[name=\"oldPassword\"]').type('definitely-wrong-xyz');\n cy.get('input[name=\"newPassword\"]').type('newSecretPass123');\n cy.get('input[name=\"confirmPassword\"]').type('newSecretPass123');\n cy.get('form button[type=\"submit\"]')\n .should('not.be.disabled')\n .click();\n // Server rejects (htpasswd → plain Error → handler returns 4xx).\n cy.wait('@reset').its('response.statusCode').should('not.eq', 200);\n // onSubmit's catch sets errors.root → rendered via LoginDialogFormError.\n cy.getByTestId(login.error, { timeout: 5000 })\n .should('be.visible')\n .and('contain.text', GENERIC_FAILURE_TEXT);\n // Still on the change-password page so the user can retry.\n cy.location('pathname').should('include', CHANGE_PASSWORD_PATH);\n }\n );\n\n // ── Happy path (mutates state; keeps `currentPassword` in sync) ─\n\n maybeIt(features.changePassword.happyPath)(\n 'should change the password and navigate to the success page',\n () => {\n const newPassword = `${currentPassword}-rotated`;\n cy.intercept('PUT', '/-/verdaccio/sec/reset_password').as('reset');\n\n cy.get('input[name=\"username\"]').type(user);\n cy.get('input[name=\"oldPassword\"]').type(currentPassword);\n cy.get('input[name=\"newPassword\"]').type(newPassword);\n cy.get('input[name=\"confirmPassword\"]').type(newPassword);\n cy.get('form button[type=\"submit\"]')\n .should('not.be.disabled')\n .click();\n\n cy.wait('@reset').its('response.statusCode').should('eq', 200);\n // Post-submit the component navigates to Route.SUCCESS with a\n // messageType query param. Assert the pathname; the message text\n // is i18n-driven and out of scope for this selector layer.\n cy.location('pathname', { timeout: 5000 }).should('include', '/-/web/success');\n\n // Track the rotation so `after()` can restore it.\n currentPassword = newPassword;\n }\n );\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"change-password.js","names":[],"sources":["../../../src/tests/change-password.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Tests for the Change Password page at /-/web/change-password.\n *\n * The page renders only when the server is started with\n * `flags.changePassword: true` (otherwise the React component\n * redirects to `/` on mount). Each test logs in first, navigates\n * directly to the page, and drives the form.\n *\n * Selector strategy: the ChangePassword form does not ship stable\n * `id`/testid attributes on its inputs, but every field is registered\n * via react-hook-form's `register('<name>')`, which sets a stable\n * `name` attribute on the underlying `<input>`. The labels themselves\n * are `t('security.changePassword.*')` calls — when the i18n bundle\n * hasn't finished loading (or isn't loaded at all on this route), MUI\n * renders the literal i18n key as the label, so any selector that\n * matches on visible label text silently misses every field and the\n * form stays empty (which would also make the mismatch test \"pass\"\n * for the wrong reason: the submit button is disabled because the\n * form is empty, not because yup rejected the mismatch).\n *\n * The stable, i18n-independent contract from ChangePassword.tsx is:\n * register('username') → input[name=\"username\"]\n * register('oldPassword') → input[name=\"oldPassword\"]\n * register('newPassword') → input[name=\"newPassword\"]\n * register('confirmPassword') → input[name=\"confirmPassword\"]\n * submit button → form button[type=\"submit\"]\n */\nexport function changePasswordTests(config: RegistryConfig) {\n const { header, login } = config.testIds;\n const { loginDialog } = config.selectors;\n const { features } = config;\n\n // The onSubmit catch block in ChangePassword.tsx sets a hardcoded\n // English string — if the upstream component ever localizes this,\n // this constant and the wrongOldPassword test will need updating.\n const GENERIC_FAILURE_TEXT = 'Failed to change password';\n\n describe('change password', () => {\n const CHANGE_PASSWORD_PATH = '/-/web/change-password';\n const { user, password } = config.credentials;\n\n /**\n * Tests mutate the user's password. We track the \"current\" value\n * across tests so the `after()` hook can restore the original,\n * leaving the registry in the same state other suites assume.\n */\n let currentPassword = password;\n\n /**\n * Capability check. The ChangePassword page's `useEffect` redirects\n * to `/` whenever `configuration.flags.changePassword` is not truthy\n * — which is the case on any registry that either (a) didn't set\n * `flags.changePassword: true` in its config, or (b) runs a\n * verdaccio build whose middleware doesn't yet propagate the flag\n * into `__VERDACCIO_BASENAME_UI_OPTIONS` (older `verdaccio:6`\n * tagged images fall in this bucket).\n *\n * In either case the suite has nothing to exercise, so skip the\n * whole describe with a clear reason rather than burning five\n * seconds per test on cy.contains timeouts.\n */\n before(function () {\n cy.visit(config.registryUrl);\n cy.window().then((win) => {\n const opts = (win as any).__VERDACCIO_BASENAME_UI_OPTIONS;\n const enabled = !!opts?.flags?.changePassword;\n if (!enabled) {\n // eslint-disable-next-line no-console\n console.warn(\n '[change-password] server did not advertise flags.changePassword=true ' +\n '— skipping suite. ui-options.flags: ' +\n JSON.stringify(opts?.flags ?? {})\n );\n this.skip();\n }\n });\n });\n\n beforeEach(() => {\n // Intercept the login POST and wait on it explicitly instead of\n // leaning on a visual sentinel — mirrors the pattern used by\n // signinTests and avoids a race between cy.login's fire-and-forget\n // submit and the next cy.visit.\n cy.intercept('POST', '/-/verdaccio/sec/login').as('signChangePwd');\n cy.visit(config.registryUrl);\n cy.login(user, currentPassword, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@signChangePwd').its('response.statusCode').should('eq', 200);\n\n cy.visit(CHANGE_PASSWORD_PATH);\n // If flags.changePassword is off server-side, the component's\n // useEffect redirects to `/` and this assertion times out — which\n // is the correct signal that the registry is misconfigured.\n // Use the stable `type=\"submit\"` selector so the assertion is\n // independent of whether the i18n bundle has resolved by now.\n cy.get('form button[type=\"submit\"]', { timeout: 5000 }).should('be.visible');\n });\n\n after(() => {\n // Restore the original password so subsequent spec files\n // (and retries) can still log in with `config.credentials`.\n if (currentPassword === password) return;\n cy.intercept('POST', '/-/verdaccio/sec/login').as('signChangePwdRestore');\n cy.visit(config.registryUrl);\n cy.login(user, currentPassword, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@signChangePwdRestore').its('response.statusCode').should('eq', 200);\n cy.visit(CHANGE_PASSWORD_PATH);\n cy.get('input[name=\"username\"]').type(user);\n cy.get('input[name=\"oldPassword\"]').type(currentPassword);\n cy.get('input[name=\"newPassword\"]').type(password);\n cy.get('input[name=\"confirmPassword\"]').type(password);\n cy.get('form button[type=\"submit\"]').click();\n currentPassword = password;\n });\n\n // ── Validation (client-side yup) ─────────────────────────────\n\n maybeIt(features.changePassword.validation)(\n 'should disable the submit button while the form is empty',\n () => {\n cy.get('form button[type=\"submit\"]').should('be.disabled');\n }\n );\n\n maybeIt(features.changePassword.validation)(\n 'should keep submit disabled when new and confirm passwords mismatch',\n () => {\n cy.get('input[name=\"username\"]').type(user);\n cy.get('input[name=\"oldPassword\"]').type(currentPassword);\n cy.get('input[name=\"newPassword\"]').type('newSecretPass123');\n cy.get('input[name=\"confirmPassword\"]').type('different-value');\n // yup schema rejects mismatch → isValid stays false → button disabled.\n cy.get('form button[type=\"submit\"]').should('be.disabled');\n }\n );\n\n // ── Server error path ────────────────────────────────────────\n\n maybeIt(features.changePassword.wrongOldPassword)(\n 'should show an error banner when the old password is wrong',\n () => {\n cy.intercept('PUT', '/-/verdaccio/sec/reset_password').as('reset');\n cy.get('input[name=\"username\"]').type(user);\n cy.get('input[name=\"oldPassword\"]').type('definitely-wrong-xyz');\n cy.get('input[name=\"newPassword\"]').type('newSecretPass123');\n cy.get('input[name=\"confirmPassword\"]').type('newSecretPass123');\n cy.get('form button[type=\"submit\"]').should('not.be.disabled').click();\n // Server rejects (htpasswd → plain Error → handler returns 4xx).\n cy.wait('@reset').its('response.statusCode').should('not.eq', 200);\n // onSubmit's catch sets errors.root → rendered via LoginDialogFormError.\n cy.getByTestId(login.error, { timeout: 5000 })\n .should('be.visible')\n .and('contain.text', GENERIC_FAILURE_TEXT);\n // Still on the change-password page so the user can retry.\n cy.location('pathname').should('include', CHANGE_PASSWORD_PATH);\n }\n );\n\n // ── Happy path (mutates state; keeps `currentPassword` in sync) ─\n\n maybeIt(features.changePassword.happyPath)(\n 'should change the password and navigate to the success page',\n () => {\n const newPassword = `${currentPassword}-rotated`;\n cy.intercept('PUT', '/-/verdaccio/sec/reset_password').as('reset');\n\n cy.get('input[name=\"username\"]').type(user);\n cy.get('input[name=\"oldPassword\"]').type(currentPassword);\n cy.get('input[name=\"newPassword\"]').type(newPassword);\n cy.get('input[name=\"confirmPassword\"]').type(newPassword);\n cy.get('form button[type=\"submit\"]').should('not.be.disabled').click();\n\n cy.wait('@reset').its('response.statusCode').should('eq', 200);\n // Post-submit the component navigates to Route.SUCCESS with a\n // messageType query param. Assert the pathname; the message text\n // is i18n-driven and out of scope for this selector layer.\n cy.location('pathname', { timeout: 5000 }).should('include', '/-/web/success');\n\n // Track the rotation so `after()` can restore it.\n currentPassword = newPassword;\n }\n );\n });\n}\n"],"mappings":"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"}
@@ -1 +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 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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"}
1
+ {"version":3,"file":"home.js","names":[],"sources":["../../../src/tests/home.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\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(`npm adduser --registry ${config.registryUrl}`);\n cy.getByTestId(home.helpCard).contains(`npm publish --registry ${config.registryUrl}`);\n });\n\n it('should render the header logo and login button', () => {\n cy.getByTestId(header.container).should('be.visible');\n cy.get(`[data-testid=\"${header.defaultLogo}\"], [data-testid=\"${header.customLogo}\"]`).should(\n 'be.visible'\n );\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(`[data-testid=\"${header.defaultLogo}\"], [data-testid=\"${header.customLogo}\"]`)\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('be.visible');\n });\n });\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"layout.js","names":[],"sources":["../../../src/tests/layout.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Tests for the persistent page chrome: the header (nav bar, logo,\n * search container, action buttons) and the footer (version marker).\n * Also asserts that the runtime UI configuration endpoint\n * `/-/static/ui-options.js` loads successfully — this endpoint emits\n * `window.__VERDACCIO_BASENAME_UI_OPTIONS`, which the ui-theme relies\n * on for feature flags like showFooter / showSearch / showSettings. If\n * it fails the whole SPA degrades to whatever defaults the provider\n * ships with, so it's worth a direct network-level check.\n */\nexport function layoutTests(config: RegistryConfig) {\n const { header, footer } = config.testIds;\n const { features } = config;\n\n describe('layout: header, footer, ui-options', () => {\n beforeEach(() => {\n cy.intercept('GET', '**/-/static/ui-options.js').as('uiOptions');\n cy.visit(config.registryUrl);\n cy.get('body').should('be.visible');\n });\n\n it('should load /-/static/ui-options.js with HTTP 200', () => {\n cy.wait('@uiOptions', { timeout: 10000 })\n .its('response.statusCode')\n .should('eq', 200);\n });\n\n it('should serve ui-options.js with a JavaScript content-type', () => {\n cy.wait('@uiOptions', { timeout: 10000 }).then((interception: any) => {\n const contentType =\n interception.response?.headers?.['content-type'] || '';\n // Verdaccio serves this as `application/javascript` (possibly\n // with a charset suffix). Match loosely so small header tweaks\n // don't break the test.\n expect(contentType).to.match(/javascript/i);\n });\n });\n\n it('should expose window.__VERDACCIO_BASENAME_UI_OPTIONS at runtime', () => {\n cy.wait('@uiOptions', { timeout: 10000 });\n // ui-options.js sets this global before the React app boots, so\n // by the time the body is visible it should already be populated.\n cy.window()\n .its('__VERDACCIO_BASENAME_UI_OPTIONS')\n .should('be.an', 'object');\n });\n\n describe('header', () => {\n it('should render the header container', () => {\n cy.getByTestId(header.container).should('be.visible');\n cy.getByTestId(header.innerNavBar).should('be.visible');\n });\n\n it('should render the logo', () => {\n // Either the default SVG logo or a user-provided custom one.\n cy.get(\n `[data-testid=\"${header.defaultLogo}\"], [data-testid=\"${header.customLogo}\"]`\n ).should('be.visible');\n });\n\n it('should render the search container', () => {\n cy.getByTestId(header.searchContainer).should('be.visible');\n });\n\n it('should render the header-right action cluster', () => {\n cy.getByTestId(header.right).should('be.visible');\n });\n\n it('should render the login button when logged out', () => {\n cy.getByTestId(header.loginButton).should('be.visible');\n });\n\n it('should render the settings and info buttons', () => {\n // Both depend on `showSettings` / `showInfo` being truthy in\n // the ui-options response. On the default config provider\n // they default to true so no explicit registry config is\n // required.\n cy.getByTestId(header.settingsTooltip).should('be.visible');\n cy.getByTestId(header.infoTooltip).should('be.visible');\n });\n\n maybeIt(features.layout.themeSwitch)(\n 'should toggle between light and dark mode',\n () => {\n // Cypress clears localStorage between tests (testIsolation),\n // so each test starts from whatever the client default is.\n // On CI the default is light (the Electron headless browser\n // reports `prefers-color-scheme: light`).\n //\n // `handleToggleDarkLightMode` in HeaderRight wraps\n // `setIsDarkMode` in a 300ms setTimeout, so we assert with\n // Cypress's built-in retryability (no `cy.wait(ms)` needed —\n // the `.should('be.visible')` retry window covers it).\n\n // Start: light mode → the \"light\" icon button is rendered.\n cy.getByTestId(header.themeSwitchLight).should('be.visible').click();\n\n // After the debounced flip, the \"dark\" variant replaces it.\n cy.getByTestId(header.themeSwitchDark, { timeout: 5000 }).should(\n 'be.visible'\n );\n cy.getByTestId(header.themeSwitchLight).should('not.exist');\n\n // localStorage.darkMode is the source of truth (see\n // useLocalStorage('darkMode', …) in ThemeProvider).\n cy.window().its('localStorage').invoke('getItem', 'darkMode')\n .should('eq', 'true');\n\n // Toggle back so subsequent tests don't inherit dark state\n // via a stale cache (testIsolation clears localStorage, but\n // being explicit keeps the assertion symmetric).\n cy.getByTestId(header.themeSwitchDark).click();\n cy.getByTestId(header.themeSwitchLight, { timeout: 5000 }).should(\n 'be.visible'\n );\n }\n );\n });\n\n describe('footer', () => {\n it('should render the footer container', () => {\n cy.getByTestId(footer.container).scrollIntoView().should('be.visible');\n });\n\n it('should render the version marker with the powered-by label', () => {\n // <PoweredBy> only renders when `configOptions.version` is\n // truthy, which Verdaccio sets automatically from its\n // package.json at startup.\n cy.getByTestId(footer.version)\n .scrollIntoView()\n .should('be.visible')\n .invoke('text')\n .should('have.length.greaterThan', 0);\n });\n\n it('should render a logo next to the version marker', () => {\n // The footer uses the default SVG logo as the link to\n // verdaccio.org.\n cy.getByTestId(footer.container)\n .find(\n `[data-testid=\"${header.defaultLogo}\"], [data-testid=\"${header.customLogo}\"]`\n )\n .should('exist');\n });\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;AAeA,SAAgB,YAAY,QAAwB;CAClD,MAAM,EAAE,QAAQ,WAAW,OAAO;CAClC,MAAM,EAAE,aAAa;AAErB,UAAS,4CAA4C;AACnD,mBAAiB;AACf,MAAG,UAAU,OAAO,4BAA4B,CAAC,GAAG,YAAY;AAChE,MAAG,MAAM,OAAO,YAAY;AAC5B,MAAG,IAAI,OAAO,CAAC,OAAO,aAAa;IACnC;AAEF,KAAG,2DAA2D;AAC5D,MAAG,KAAK,cAAc,EAAE,SAAS,KAAO,CAAC,CACtC,IAAI,sBAAsB,CAC1B,OAAO,MAAM,IAAI;IACpB;AAEF,KAAG,mEAAmE;AACpE,MAAG,KAAK,cAAc,EAAE,SAAS,KAAO,CAAC,CAAC,MAAM,iBAAsB;IACpE,MAAM,cACJ,aAAa,UAAU,UAAU,mBAAmB;AAItD,WAAO,YAAY,CAAC,GAAG,MAAM,cAAc;KAC3C;IACF;AAEF,KAAG,yEAAyE;AAC1E,MAAG,KAAK,cAAc,EAAE,SAAS,KAAO,CAAC;AAGzC,MAAG,QAAQ,CACR,IAAI,kCAAkC,CACtC,OAAO,SAAS,SAAS;IAC5B;AAEF,WAAS,gBAAgB;AACvB,MAAG,4CAA4C;AAC7C,OAAG,YAAY,OAAO,UAAU,CAAC,OAAO,aAAa;AACrD,OAAG,YAAY,OAAO,YAAY,CAAC,OAAO,aAAa;KACvD;AAEF,MAAG,gCAAgC;AAEjC,OAAG,IACD,iBAAiB,OAAO,YAAY,oBAAoB,OAAO,WAAW,IAC3E,CAAC,OAAO,aAAa;KACtB;AAEF,MAAG,4CAA4C;AAC7C,OAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO,aAAa;KAC3D;AAEF,MAAG,uDAAuD;AACxD,OAAG,YAAY,OAAO,MAAM,CAAC,OAAO,aAAa;KACjD;AAEF,MAAG,wDAAwD;AACzD,OAAG,YAAY,OAAO,YAAY,CAAC,OAAO,aAAa;KACvD;AAEF,MAAG,qDAAqD;AAKtD,OAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO,aAAa;AAC3D,OAAG,YAAY,OAAO,YAAY,CAAC,OAAO,aAAa;KACvD;AAEF,WAAQ,SAAS,OAAO,YAAY,CAClC,mDACM;AAYJ,OAAG,YAAY,OAAO,iBAAiB,CAAC,OAAO,aAAa,CAAC,OAAO;AAGpE,OAAG,YAAY,OAAO,iBAAiB,EAAE,SAAS,KAAM,CAAC,CAAC,OACxD,aACD;AACD,OAAG,YAAY,OAAO,iBAAiB,CAAC,OAAO,YAAY;AAI3D,OAAG,QAAQ,CAAC,IAAI,eAAe,CAAC,OAAO,WAAW,WAAW,CAC1D,OAAO,MAAM,OAAO;AAKvB,OAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO;AAC9C,OAAG,YAAY,OAAO,kBAAkB,EAAE,SAAS,KAAM,CAAC,CAAC,OACzD,aACD;KAEJ;IACD;AAEF,WAAS,gBAAgB;AACvB,MAAG,4CAA4C;AAC7C,OAAG,YAAY,OAAO,UAAU,CAAC,gBAAgB,CAAC,OAAO,aAAa;KACtE;AAEF,MAAG,oEAAoE;AAIrE,OAAG,YAAY,OAAO,QAAQ,CAC3B,gBAAgB,CAChB,OAAO,aAAa,CACpB,OAAO,OAAO,CACd,OAAO,2BAA2B,EAAE;KACvC;AAEF,MAAG,yDAAyD;AAG1D,OAAG,YAAY,OAAO,UAAU,CAC7B,KACC,iBAAiB,OAAO,YAAY,oBAAoB,OAAO,WAAW,IAC3E,CACA,OAAO,QAAQ;KAClB;IACF;GACF"}
1
+ {"version":3,"file":"layout.js","names":[],"sources":["../../../src/tests/layout.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Tests for the persistent page chrome: the header (nav bar, logo,\n * search container, action buttons) and the footer (version marker).\n * Also asserts that the runtime UI configuration endpoint\n * `/-/static/ui-options.js` loads successfully — this endpoint emits\n * `window.__VERDACCIO_BASENAME_UI_OPTIONS`, which the ui-theme relies\n * on for feature flags like showFooter / showSearch / showSettings. If\n * it fails the whole SPA degrades to whatever defaults the provider\n * ships with, so it's worth a direct network-level check.\n */\nexport function layoutTests(config: RegistryConfig) {\n const { header, footer } = config.testIds;\n const { features } = config;\n\n describe('layout: header, footer, ui-options', () => {\n beforeEach(() => {\n cy.intercept('GET', '**/-/static/ui-options.js').as('uiOptions');\n cy.visit(config.registryUrl);\n cy.get('body').should('be.visible');\n });\n\n it('should load /-/static/ui-options.js with HTTP 200', () => {\n cy.wait('@uiOptions', { timeout: 10000 }).its('response.statusCode').should('eq', 200);\n });\n\n it('should serve ui-options.js with a JavaScript content-type', () => {\n cy.wait('@uiOptions', { timeout: 10000 }).then((interception: any) => {\n const contentType = interception.response?.headers?.['content-type'] || '';\n // Verdaccio serves this as `application/javascript` (possibly\n // with a charset suffix). Match loosely so small header tweaks\n // don't break the test.\n expect(contentType).to.match(/javascript/i);\n });\n });\n\n it('should expose window.__VERDACCIO_BASENAME_UI_OPTIONS at runtime', () => {\n cy.wait('@uiOptions', { timeout: 10000 });\n // ui-options.js sets this global before the React app boots, so\n // by the time the body is visible it should already be populated.\n cy.window().its('__VERDACCIO_BASENAME_UI_OPTIONS').should('be.an', 'object');\n });\n\n describe('header', () => {\n it('should render the header container', () => {\n cy.getByTestId(header.container).should('be.visible');\n cy.getByTestId(header.innerNavBar).should('be.visible');\n });\n\n it('should render the logo', () => {\n // Either the default SVG logo or a user-provided custom one.\n cy.get(\n `[data-testid=\"${header.defaultLogo}\"], [data-testid=\"${header.customLogo}\"]`\n ).should('be.visible');\n });\n\n it('should render the search container', () => {\n cy.getByTestId(header.searchContainer).should('be.visible');\n });\n\n it('should render the header-right action cluster', () => {\n cy.getByTestId(header.right).should('be.visible');\n });\n\n it('should render the login button when logged out', () => {\n cy.getByTestId(header.loginButton).should('be.visible');\n });\n\n it('should render the settings and info buttons', () => {\n // Both depend on `showSettings` / `showInfo` being truthy in\n // the ui-options response. On the default config provider\n // they default to true so no explicit registry config is\n // required.\n cy.getByTestId(header.settingsTooltip).should('be.visible');\n cy.getByTestId(header.infoTooltip).should('be.visible');\n });\n\n maybeIt(features.layout.themeSwitch)('should toggle between light and dark mode', () => {\n // Cypress clears localStorage between tests (testIsolation),\n // so each test starts from whatever the client default is.\n // On CI the default is light (the Electron headless browser\n // reports `prefers-color-scheme: light`).\n //\n // `handleToggleDarkLightMode` in HeaderRight wraps\n // `setIsDarkMode` in a 300ms setTimeout, so we assert with\n // Cypress's built-in retryability (no `cy.wait(ms)` needed —\n // the `.should('be.visible')` retry window covers it).\n\n // Start: light mode → the \"light\" icon button is rendered.\n cy.getByTestId(header.themeSwitchLight).should('be.visible').click();\n\n // After the debounced flip, the \"dark\" variant replaces it.\n cy.getByTestId(header.themeSwitchDark, { timeout: 5000 }).should('be.visible');\n cy.getByTestId(header.themeSwitchLight).should('not.exist');\n\n // localStorage.darkMode is the source of truth (see\n // useLocalStorage('darkMode', …) in ThemeProvider).\n cy.window().its('localStorage').invoke('getItem', 'darkMode').should('eq', 'true');\n\n // Toggle back so subsequent tests don't inherit dark state\n // via a stale cache (testIsolation clears localStorage, but\n // being explicit keeps the assertion symmetric).\n cy.getByTestId(header.themeSwitchDark).click();\n cy.getByTestId(header.themeSwitchLight, { timeout: 5000 }).should('be.visible');\n });\n });\n\n describe('footer', () => {\n it('should render the footer container', () => {\n cy.getByTestId(footer.container).scrollIntoView().should('be.visible');\n });\n\n it('should render the version marker with the powered-by label', () => {\n // <PoweredBy> only renders when `configOptions.version` is\n // truthy, which Verdaccio sets automatically from its\n // package.json at startup.\n cy.getByTestId(footer.version)\n .scrollIntoView()\n .should('be.visible')\n .invoke('text')\n .should('have.length.greaterThan', 0);\n });\n\n it('should render a logo next to the version marker', () => {\n // The footer uses the default SVG logo as the link to\n // verdaccio.org.\n cy.getByTestId(footer.container)\n .find(`[data-testid=\"${header.defaultLogo}\"], [data-testid=\"${header.customLogo}\"]`)\n .should('exist');\n });\n });\n });\n}\n"],"mappings":"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"}
@@ -4,34 +4,34 @@ function publishTests(config) {
4
4
  const { header, package: pkg } = config.testIds;
5
5
  const { markdownBody, loginDialog } = config.selectors;
6
6
  const { features } = config;
7
+ /**
8
+ * Log in once, reuse the session across tests in the requested suite.
9
+ * `cy.session` caches cookies + localStorage keyed on the first
10
+ * argument, so subsequent calls restore without hitting the network.
11
+ */
12
+ const loginOnce = (sessionName) => {
13
+ cy.session([sessionName, config.credentials.user], () => {
14
+ cy.visit(config.registryUrl);
15
+ cy.login(config.credentials.user, config.credentials.password, {
16
+ loginButton: header.loginButton,
17
+ ...loginDialog
18
+ });
19
+ cy.wait("@sign");
20
+ }, {
21
+ validate() {
22
+ cy.request({
23
+ url: `${config.registryUrl}/-/verdaccio/data/packages`,
24
+ failOnStatusCode: false
25
+ }).its("status").should("be.oneOf", [200, 304]);
26
+ },
27
+ cacheAcrossSpecs: true
28
+ });
29
+ };
7
30
  describe("publish", () => {
8
31
  const pkgName = "@verdaccio/pkg-scoped";
9
32
  const depName = "debug";
10
33
  const depVersion = "4.0.0";
11
34
  let tempFolder = null;
12
- /**
13
- * Log in once, reuse the session across every test in this suite.
14
- * `cy.session` caches cookies + localStorage keyed on the first
15
- * argument, so subsequent calls restore without hitting the network.
16
- */
17
- const loginOnce = () => {
18
- cy.session(["publish-suite", config.credentials.user], () => {
19
- cy.visit(config.registryUrl);
20
- cy.login(config.credentials.user, config.credentials.password, {
21
- loginButton: header.loginButton,
22
- ...loginDialog
23
- });
24
- cy.wait("@sign");
25
- }, {
26
- validate() {
27
- cy.request({
28
- url: `${config.registryUrl}/-/verdaccio/data/packages`,
29
- failOnStatusCode: false
30
- }).its("status").should("be.oneOf", [200, 304]);
31
- },
32
- cacheAcrossSpecs: true
33
- });
34
- };
35
35
  beforeEach(() => {
36
36
  cy.intercept("POST", "/-/verdaccio/sec/login").as("sign");
37
37
  cy.intercept("GET", "/-/verdaccio/data/packages").as("pkgs");
@@ -45,7 +45,7 @@ function publishTests(config) {
45
45
  }).then((result) => {
46
46
  tempFolder = result?.tempFolder ?? null;
47
47
  });
48
- loginOnce();
48
+ loginOnce("publish-suite");
49
49
  cy.visit(config.registryUrl);
50
50
  });
51
51
  afterEach(() => {
@@ -145,6 +145,46 @@ function publishTests(config) {
145
145
  cy.getByTestId(pkg.rawViewerDialog).should("not.exist");
146
146
  });
147
147
  });
148
+ describe("private package tarball downloads", () => {
149
+ const privatePkgName = "@private/tarball-fixture";
150
+ let tempFolder = null;
151
+ beforeEach(() => {
152
+ cy.intercept("POST", "/-/verdaccio/sec/login").as("sign");
153
+ cy.intercept("GET", "/-/verdaccio/data/packages").as("pkgs");
154
+ cy.intercept("GET", "**/-/verdaccio/data/sidebar/@private/tarball-fixture*").as("sidebar");
155
+ cy.task("publishPackage", {
156
+ pkgName: privatePkgName,
157
+ version: "1.0.0",
158
+ unique: true
159
+ }).then((result) => {
160
+ tempFolder = result?.tempFolder ?? null;
161
+ });
162
+ loginOnce("private-tarball-suite");
163
+ cy.visit(config.registryUrl);
164
+ });
165
+ afterEach(() => {
166
+ cy.task("unpublishPackage", {
167
+ pkgName: privatePkgName,
168
+ tempFolder: tempFolder ?? void 0
169
+ });
170
+ if (tempFolder) cy.task("cleanupPublished", tempFolder);
171
+ tempFolder = null;
172
+ });
173
+ maybeIt(features.publish.privateDownloadTarball)("should fetch a private tarball from the home package list", () => {
174
+ cy.intercept("GET", "**/tarball-fixture-*.tgz").as("privateTarballFetch");
175
+ cy.wait("@pkgs");
176
+ cy.contains(`[data-testid="${pkg.title}"]`, privatePkgName).should("be.visible").closest(`[data-testid="${pkg.itemList}"]`).find(`[data-testid="${pkg.downloadTarball}"]`).should("be.visible").click();
177
+ cy.wait("@privateTarballFetch", { timeout: 1e4 }).its("response.statusCode").should("eq", 200);
178
+ });
179
+ maybeIt(features.publish.privateDownloadTarball)("should fetch a private tarball from the package sidebar", () => {
180
+ cy.intercept("GET", "**/tarball-fixture-*.tgz").as("privateTarballFetch");
181
+ cy.wait("@pkgs");
182
+ cy.contains(`[data-testid="${pkg.title}"]`, privatePkgName).click();
183
+ cy.wait("@sidebar");
184
+ cy.getByTestId(pkg.downloadTarballBtn).should("be.visible").click();
185
+ cy.wait("@privateTarballFetch", { timeout: 1e4 }).its("response.statusCode").should("eq", 200);
186
+ });
187
+ });
148
188
  }
149
189
  //#endregion
150
190
  export { publishTests };
@@ -1 +1 @@
1
- {"version":3,"file":"publish.js","names":[],"sources":["../../../src/tests/publish.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\nexport function publishTests(config: RegistryConfig) {\n const { header, package: pkg } = config.testIds;\n const { markdownBody, loginDialog } = config.selectors;\n const { features } = config;\n\n describe('publish', () => {\n const pkgName = '@verdaccio/pkg-scoped';\n // Single source of truth for the dependency the publish fixture\n // writes into its package.json. The dependencies-tab assertion\n // reads these back to verify the UI rendered both fields.\n const depName = 'debug';\n const depVersion = '4.0.0';\n // Per-test state so afterEach can clean up the specific publish\n // that this test created (temp folder + registry entry).\n let tempFolder: string | null = null;\n\n /**\n * Log in once, reuse the session across every test in this suite.\n * `cy.session` caches cookies + localStorage keyed on the first\n * argument, so subsequent calls restore without hitting the network.\n */\n const loginOnce = () => {\n cy.session(\n ['publish-suite', config.credentials.user],\n () => {\n cy.visit(config.registryUrl);\n cy.login(config.credentials.user, config.credentials.password, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@sign');\n },\n {\n validate() {\n cy.request({\n url: `${config.registryUrl}/-/verdaccio/data/packages`,\n failOnStatusCode: false,\n })\n .its('status')\n .should('be.oneOf', [200, 304]);\n },\n cacheAcrossSpecs: true,\n }\n );\n };\n\n beforeEach(() => {\n cy.intercept('POST', '/-/verdaccio/sec/login').as('sign');\n cy.intercept('GET', '/-/verdaccio/data/packages').as('pkgs');\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}`).as('sidebar');\n cy.intercept('GET', `/-/verdaccio/data/package/readme/${pkgName}`).as('readme');\n\n // Publish a fresh copy for this test. `unique: true` appends a\n // timestamp suffix so the version is distinct per test even when\n // the registry briefly has a stale copy from a prior afterEach.\n cy.task('publishPackage', {\n pkgName,\n version: '1.0.0',\n dependencies: { [depName]: depVersion },\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n });\n\n loginOnce();\n cy.visit(config.registryUrl);\n });\n\n afterEach(() => {\n // Remove the package from the registry AND the local temp folder.\n // Run both regardless of test outcome so the next test starts\n // clean. `unpublishPackage` treats 404 as success, so a re-run\n // after a half-published state is still safe.\n cy.task('unpublishPackage', { pkgName, tempFolder: tempFolder ?? undefined });\n if (tempFolder) {\n cy.task('cleanupPublished', tempFolder);\n }\n tempFolder = null;\n });\n\n it('should have one published package', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).should('have.length.at.least', 1);\n });\n\n it('should navigate to page detail', () => {\n cy.wait('@pkgs');\n cy.wait(300);\n cy.getByTestId(pkg.title).first().click();\n });\n\n it('should have readme content', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@readme');\n cy.wait('@sidebar');\n cy.getByTestId(pkg.readme).should('be.visible');\n cy.get(markdownBody).should('have.length', 1);\n // publishPackage writes a README whose body contains \"e2e testing\".\n cy.contains(markdownBody, /test/);\n cy.contains(`${markdownBody} h1`, pkgName).should('be.visible');\n });\n\n it('should render the sidebar with install commands for npm, yarn, pnpm', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@sidebar');\n cy.getByTestId(pkg.sidebar).should('be.visible');\n cy.getByTestId(pkg.installList).within(() => {\n cy.getByTestId(pkg.installNpm).should('be.visible');\n cy.getByTestId(pkg.installYarn).should('be.visible');\n cy.getByTestId(pkg.installPnpm).should('be.visible');\n });\n cy.getByTestId(pkg.installNpm).should('contain.text', pkgName);\n });\n\n it('should render the sidebar keywords from the published manifest', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@sidebar');\n // publishPackage writes `keywords: ['verdaccio', 'e2e', 'test']`\n // into the generated package.json.\n cy.getByTestId(pkg.keywordList).should('be.visible');\n cy.getByTestId(pkg.keywordList).should('contain.text', 'verdaccio');\n cy.getByTestId(pkg.keywordList).should('contain.text', 'e2e');\n cy.getByTestId(pkg.keywordList).should('contain.text', 'test');\n });\n\n it('should click on dependencies tab', () => {\n cy.wait('@pkgs');\n cy.wait(300);\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@readme');\n cy.wait('@sidebar');\n cy.getByTestId(pkg.dependenciesTab).click();\n cy.wait(100);\n cy.getByTestId(pkg.dependencies).should('have.length', 1);\n\n // The dep Chip uses the dep name as its data-testid (dynamic\n // Verdaccio convention, see DependencyBlock.tsx:68), and its\n // label is `\"${name}: ${version}\"` via the `dependencies.\n // dependency-block` i18n key. Assert BOTH fields are rendered.\n cy.getByTestId(depName)\n .should('be.visible')\n .and('contain.text', depName)\n .and('contain.text', depVersion);\n\n // Also verify the Chip text matches the exact \"name: version\"\n // format so a regression in the label template would fail here\n // rather than pass via a loose substring match.\n cy.getByTestId(depName)\n .invoke('text')\n .should('match', new RegExp(`${depName}\\\\s*:\\\\s*${depVersion}`));\n });\n\n it('should click on versions tab', () => {\n cy.wait('@pkgs');\n cy.wait(300);\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@readme');\n cy.wait('@sidebar');\n cy.getByTestId(pkg.versionsTab).click();\n // With `unique: true` the version becomes `1.0.0-t<timestamp>`,\n // but \"1.0.0\" still appears as a substring — match loosely.\n cy.getByTestId(pkg.tagLatest)\n .children()\n .invoke('text')\n .should('match', /1\\.0\\.0/);\n });\n\n it('should click on uplinks tab', () => {\n cy.wait('@pkgs');\n cy.wait(300);\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@readme');\n cy.wait('@sidebar');\n cy.getByTestId(pkg.uplinksTab).click();\n cy.getByTestId(pkg.noUplinks).should('be.visible');\n });\n\n // ── Action-bar FABs: tarball download + raw viewer ─────────────\n // Both buttons live in the sidebar ActionBar. They're gated on\n // `web.showDownloadTarball` / `web.showRaw` (both default to true\n // in the ui-theme's AppConfigurationProvider) and each test is\n // also guarded by a feature flag so branches that ship a\n // different action bar can skip cleanly.\n\n maybeIt(features.publish.downloadTarball)(\n 'should fetch the tarball when the download button is clicked',\n () => {\n // The download provider hits the package manifest's dist.tarball\n // URL directly. For our published fixture the filename looks\n // like `pkg-scoped-1.0.0-t<ts>.tgz`, served from\n // `/<pkg>/-/<filename>.tgz`. Intercept before clicking.\n cy.intercept('GET', '**/pkg-scoped-*.tgz').as('tarballFetch');\n\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@sidebar');\n\n cy.getByTestId(pkg.downloadTarballBtn)\n .should('be.visible')\n .click();\n\n // The fetch should fire and return 200. We can't assert on the\n // actual file landing on disk — Cypress doesn't track OS-level\n // downloads — but a successful GET proves the end-to-end path\n // from click → download provider → registry.\n cy.wait('@tarballFetch', { timeout: 10000 })\n .its('response.statusCode')\n .should('eq', 200);\n }\n );\n\n maybeIt(features.publish.rawViewer)(\n 'should open the raw manifest viewer when the raw button is clicked',\n () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@sidebar');\n\n // RawViewer is a full-screen MUI Dialog — initially unmounted\n // because `isOpen=false` keeps Dialog closed and Cypress won't\n // find it. Clicking the FAB flips `isOpen` to true.\n cy.getByTestId(pkg.rawBtn).should('be.visible').click();\n\n cy.getByTestId(pkg.rawViewerDialog, { timeout: 5000 }).should(\n 'be.visible'\n );\n // The ReactJson viewer renders the package manifest — the\n // package name should appear somewhere in the serialized JSON.\n cy.getByTestId(pkg.rawViewerDialog).should('contain.text', pkgName);\n\n // Close via the X button and confirm the dialog goes away so\n // subsequent tests don't inherit an open overlay.\n cy.getByTestId(pkg.closeRawViewer).click();\n cy.getByTestId(pkg.rawViewerDialog).should('not.exist');\n }\n );\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"publish.js","names":[],"sources":["../../../src/tests/publish.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\nexport function publishTests(config: RegistryConfig) {\n const { header, package: pkg } = config.testIds;\n const { markdownBody, loginDialog } = config.selectors;\n const { features } = config;\n /**\n * Log in once, reuse the session across tests in the requested suite.\n * `cy.session` caches cookies + localStorage keyed on the first\n * argument, so subsequent calls restore without hitting the network.\n */\n const loginOnce = (sessionName: string) => {\n cy.session(\n [sessionName, config.credentials.user],\n () => {\n cy.visit(config.registryUrl);\n cy.login(config.credentials.user, config.credentials.password, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@sign');\n },\n {\n validate() {\n cy.request({\n url: `${config.registryUrl}/-/verdaccio/data/packages`,\n failOnStatusCode: false,\n })\n .its('status')\n .should('be.oneOf', [200, 304]);\n },\n cacheAcrossSpecs: true,\n }\n );\n };\n\n describe('publish', () => {\n const pkgName = '@verdaccio/pkg-scoped';\n // Single source of truth for the dependency the publish fixture\n // writes into its package.json. The dependencies-tab assertion\n // reads these back to verify the UI rendered both fields.\n const depName = 'debug';\n const depVersion = '4.0.0';\n // Per-test state so afterEach can clean up the specific publish\n // that this test created (temp folder + registry entry).\n let tempFolder: string | null = null;\n\n beforeEach(() => {\n cy.intercept('POST', '/-/verdaccio/sec/login').as('sign');\n cy.intercept('GET', '/-/verdaccio/data/packages').as('pkgs');\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}`).as('sidebar');\n cy.intercept('GET', `/-/verdaccio/data/package/readme/${pkgName}`).as('readme');\n\n // Publish a fresh copy for this test. `unique: true` appends a\n // timestamp suffix so the version is distinct per test even when\n // the registry briefly has a stale copy from a prior afterEach.\n cy.task('publishPackage', {\n pkgName,\n version: '1.0.0',\n dependencies: { [depName]: depVersion },\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n });\n\n loginOnce('publish-suite');\n cy.visit(config.registryUrl);\n });\n\n afterEach(() => {\n // Remove the package from the registry AND the local temp folder.\n // Run both regardless of test outcome so the next test starts\n // clean. `unpublishPackage` treats 404 as success, so a re-run\n // after a half-published state is still safe.\n cy.task('unpublishPackage', { pkgName, tempFolder: tempFolder ?? undefined });\n if (tempFolder) {\n cy.task('cleanupPublished', tempFolder);\n }\n tempFolder = null;\n });\n\n it('should have one published package', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).should('have.length.at.least', 1);\n });\n\n it('should navigate to page detail', () => {\n cy.wait('@pkgs');\n cy.wait(300);\n cy.getByTestId(pkg.title).first().click();\n });\n\n it('should have readme content', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@readme');\n cy.wait('@sidebar');\n cy.getByTestId(pkg.readme).should('be.visible');\n cy.get(markdownBody).should('have.length', 1);\n // publishPackage writes a README whose body contains \"e2e testing\".\n cy.contains(markdownBody, /test/);\n cy.contains(`${markdownBody} h1`, pkgName).should('be.visible');\n });\n\n it('should render the sidebar with install commands for npm, yarn, pnpm', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@sidebar');\n cy.getByTestId(pkg.sidebar).should('be.visible');\n cy.getByTestId(pkg.installList).within(() => {\n cy.getByTestId(pkg.installNpm).should('be.visible');\n cy.getByTestId(pkg.installYarn).should('be.visible');\n cy.getByTestId(pkg.installPnpm).should('be.visible');\n });\n cy.getByTestId(pkg.installNpm).should('contain.text', pkgName);\n });\n\n it('should render the sidebar keywords from the published manifest', () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@sidebar');\n // publishPackage writes `keywords: ['verdaccio', 'e2e', 'test']`\n // into the generated package.json.\n cy.getByTestId(pkg.keywordList).should('be.visible');\n cy.getByTestId(pkg.keywordList).should('contain.text', 'verdaccio');\n cy.getByTestId(pkg.keywordList).should('contain.text', 'e2e');\n cy.getByTestId(pkg.keywordList).should('contain.text', 'test');\n });\n\n it('should click on dependencies tab', () => {\n cy.wait('@pkgs');\n cy.wait(300);\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@readme');\n cy.wait('@sidebar');\n cy.getByTestId(pkg.dependenciesTab).click();\n cy.wait(100);\n cy.getByTestId(pkg.dependencies).should('have.length', 1);\n\n // The dep Chip uses the dep name as its data-testid (dynamic\n // Verdaccio convention, see DependencyBlock.tsx:68), and its\n // label is `\"${name}: ${version}\"` via the `dependencies.\n // dependency-block` i18n key. Assert BOTH fields are rendered.\n cy.getByTestId(depName)\n .should('be.visible')\n .and('contain.text', depName)\n .and('contain.text', depVersion);\n\n // Also verify the Chip text matches the exact \"name: version\"\n // format so a regression in the label template would fail here\n // rather than pass via a loose substring match.\n cy.getByTestId(depName)\n .invoke('text')\n .should('match', new RegExp(`${depName}\\\\s*:\\\\s*${depVersion}`));\n });\n\n it('should click on versions tab', () => {\n cy.wait('@pkgs');\n cy.wait(300);\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@readme');\n cy.wait('@sidebar');\n cy.getByTestId(pkg.versionsTab).click();\n // With `unique: true` the version becomes `1.0.0-t<timestamp>`,\n // but \"1.0.0\" still appears as a substring — match loosely.\n cy.getByTestId(pkg.tagLatest)\n .children()\n .invoke('text')\n .should('match', /1\\.0\\.0/);\n });\n\n it('should click on uplinks tab', () => {\n cy.wait('@pkgs');\n cy.wait(300);\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@readme');\n cy.wait('@sidebar');\n cy.getByTestId(pkg.uplinksTab).click();\n cy.getByTestId(pkg.noUplinks).should('be.visible');\n });\n\n // ── Action-bar FABs: tarball download + raw viewer ─────────────\n // Both buttons live in the sidebar ActionBar. They're gated on\n // `web.showDownloadTarball` / `web.showRaw` (both default to true\n // in the ui-theme's AppConfigurationProvider) and each test is\n // also guarded by a feature flag so branches that ship a\n // different action bar can skip cleanly.\n\n maybeIt(features.publish.downloadTarball)(\n 'should fetch the tarball when the download button is clicked',\n () => {\n // The download provider hits the package manifest's dist.tarball\n // URL directly. For our published fixture the filename looks\n // like `pkg-scoped-1.0.0-t<ts>.tgz`, served from\n // `/<pkg>/-/<filename>.tgz`. Intercept before clicking.\n cy.intercept('GET', '**/pkg-scoped-*.tgz').as('tarballFetch');\n\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@sidebar');\n\n cy.getByTestId(pkg.downloadTarballBtn).should('be.visible').click();\n\n // The fetch should fire and return 200. We can't assert on the\n // actual file landing on disk — Cypress doesn't track OS-level\n // downloads — but a successful GET proves the end-to-end path\n // from click → download provider → registry.\n cy.wait('@tarballFetch', { timeout: 10000 }).its('response.statusCode').should('eq', 200);\n }\n );\n\n maybeIt(features.publish.rawViewer)(\n 'should open the raw manifest viewer when the raw button is clicked',\n () => {\n cy.wait('@pkgs');\n cy.getByTestId(pkg.title).first().click();\n cy.wait('@sidebar');\n\n // RawViewer is a full-screen MUI Dialog — initially unmounted\n // because `isOpen=false` keeps Dialog closed and Cypress won't\n // find it. Clicking the FAB flips `isOpen` to true.\n cy.getByTestId(pkg.rawBtn).should('be.visible').click();\n\n cy.getByTestId(pkg.rawViewerDialog, { timeout: 5000 }).should('be.visible');\n // The ReactJson viewer renders the package manifest — the\n // package name should appear somewhere in the serialized JSON.\n cy.getByTestId(pkg.rawViewerDialog).should('contain.text', pkgName);\n\n // Close via the X button and confirm the dialog goes away so\n // subsequent tests don't inherit an open overlay.\n cy.getByTestId(pkg.closeRawViewer).click();\n cy.getByTestId(pkg.rawViewerDialog).should('not.exist');\n }\n );\n });\n\n describe('private package tarball downloads', () => {\n const privatePkgName = '@private/tarball-fixture';\n let tempFolder: string | null = null;\n\n beforeEach(() => {\n cy.intercept('POST', '/-/verdaccio/sec/login').as('sign');\n cy.intercept('GET', '/-/verdaccio/data/packages').as('pkgs');\n cy.intercept('GET', '**/-/verdaccio/data/sidebar/@private/tarball-fixture*').as('sidebar');\n\n cy.task('publishPackage', {\n pkgName: privatePkgName,\n version: '1.0.0',\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n });\n\n loginOnce('private-tarball-suite');\n cy.visit(config.registryUrl);\n });\n\n afterEach(() => {\n cy.task('unpublishPackage', {\n pkgName: privatePkgName,\n tempFolder: tempFolder ?? undefined,\n });\n if (tempFolder) {\n cy.task('cleanupPublished', tempFolder);\n }\n tempFolder = null;\n });\n\n maybeIt(features.publish.privateDownloadTarball)(\n 'should fetch a private tarball from the home package list',\n () => {\n cy.intercept('GET', '**/tarball-fixture-*.tgz').as('privateTarballFetch');\n\n cy.wait('@pkgs');\n cy.contains(`[data-testid=\"${pkg.title}\"]`, privatePkgName)\n .should('be.visible')\n .closest(`[data-testid=\"${pkg.itemList}\"]`)\n .find(`[data-testid=\"${pkg.downloadTarball}\"]`)\n .should('be.visible')\n .click();\n\n cy.wait('@privateTarballFetch', { timeout: 10000 })\n .its('response.statusCode')\n .should('eq', 200);\n }\n );\n\n maybeIt(features.publish.privateDownloadTarball)(\n 'should fetch a private tarball from the package sidebar',\n () => {\n cy.intercept('GET', '**/tarball-fixture-*.tgz').as('privateTarballFetch');\n\n cy.wait('@pkgs');\n cy.contains(`[data-testid=\"${pkg.title}\"]`, privatePkgName).click();\n cy.wait('@sidebar');\n cy.getByTestId(pkg.downloadTarballBtn).should('be.visible').click();\n\n cy.wait('@privateTarballFetch', { timeout: 10000 })\n .its('response.statusCode')\n .should('eq', 200);\n }\n );\n });\n}\n"],"mappings":"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"}
@@ -29,13 +29,6 @@ function searchTests(config) {
29
29
  expect(interception.request.url).to.contain(query);
30
30
  });
31
31
  });
32
- it("should show a \"no results\" state for an impossible query", () => {
33
- const query = "xyzzy-no-such-package-" + Date.now();
34
- getSearchInput().clear().type(query, { delay: 30 });
35
- cy.wait(anySearchAlias(), { timeout: 1e4 });
36
- cy.wait(300);
37
- cy.contains(/no\s+(match|results|packages)/i, { timeout: 5e3 }).should("be.visible");
38
- });
39
32
  it("should clear the query and allow typing a new one", () => {
40
33
  getSearchInput().clear().type("first-query", { delay: 20 });
41
34
  cy.wait(anySearchAlias(), { timeout: 1e4 });
@@ -1 +1 @@
1
- {"version":3,"file":"search.js","names":[],"sources":["../../../src/tests/search.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * UI tests for the Verdaccio search box.\n *\n * These tests do NOT depend on any published package — they assert the\n * search *flow* (input exists, typing triggers the search API, results\n * region updates) rather than specific package metadata. Tests that need\n * a real package in the registry should live in publishTests instead.\n */\nexport function searchTests(config: RegistryConfig) {\n const { features } = config;\n const { package: pkg } = config.testIds;\n\n describe('search', () => {\n beforeEach(() => {\n // Verdaccio's search endpoint — covers both the web API and the\n // npm v1 search API, depending on which one the UI calls.\n cy.intercept('GET', '**/-/verdaccio/data/search/**').as('webSearch');\n cy.intercept('GET', '**/-/v1/search**').as('v1Search');\n cy.intercept('GET', '**/-/verdaccio/data/packages').as('pkgs');\n\n cy.visit(config.registryUrl);\n cy.get('body').should('be.visible');\n });\n\n it('should render the search input', () => {\n getSearchInput().should('be.visible');\n });\n\n it('should fire a search request when typing a query', () => {\n const query = 'verdaccio';\n\n getSearchInput().clear().type(query, { delay: 30 });\n\n // Whichever endpoint the UI is wired to, at least one should hit.\n cy.wait(anySearchAlias(), { timeout: 10000 }).then((interception: any) => {\n expect(interception.request.url).to.contain(query);\n });\n });\n\n it('should show a \"no results\" state for an impossible query', () => {\n const query = 'xyzzy-no-such-package-' + Date.now();\n\n getSearchInput().clear().type(query, { delay: 30 });\n cy.wait(anySearchAlias(), { timeout: 10000 });\n\n // Give the UI a tick to render the empty state.\n cy.wait(300);\n\n // The UI may render \"No Match\", \"No results\", or similar — match\n // loosely rather than binding to one exact string.\n cy.contains(/no\\s+(match|results|packages)/i, { timeout: 5000 }).should(\n 'be.visible'\n );\n });\n\n it('should clear the query and allow typing a new one', () => {\n getSearchInput().clear().type('first-query', { delay: 20 });\n cy.wait(anySearchAlias(), { timeout: 10000 });\n\n // Clearing should empty the input value. We deliberately do NOT\n // assert on the autocomplete dropdown's empty-state text: on a\n // registry with no packages published it lingers regardless of\n // the input value, which previously caused a false positive.\n getSearchInput().clear();\n getSearchInput().should('have.value', '');\n\n // Typing a fresh query must fire another search request so the\n // search box is still functional after a clear.\n getSearchInput().type('second-query', { delay: 20 });\n cy.wait(anySearchAlias(), { timeout: 10000 }).then(\n (interception: any) => {\n expect(interception.request.url).to.contain('second-query');\n }\n );\n });\n\n // ── Rendering assertions that require real package data ────────\n // Publishes a throwaway package before each test and unpublishes\n // after so the outer \"no results\" test still sees a clean registry.\n // The search query uses a substring of the package name to avoid\n // scope-parsing issues with `@` / `/` characters in the URL.\n describe('with a published package', () => {\n const pkgName = '@verdaccio/search-fixture';\n // Unique slug we can type into the search box — must be a\n // substring of pkgName so Verdaccio's search matches it.\n const pkgSlug = 'search-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 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 maybeIt(features.search.resultsDropdown)(\n 'should display the matching package in the results dropdown',\n () => {\n getSearchInput().clear().type(pkgSlug, { delay: 30 });\n\n // Wait for the search request to resolve with results.\n cy.wait(anySearchAlias(), { timeout: 10000 }).then(\n (interception: any) => {\n expect(interception.request.url).to.contain(pkgSlug);\n }\n );\n\n // MUI Autocomplete opens a listbox with role=\"listbox\" when\n // there are matching options. Each result renders with\n // role=\"option\". No data-testids on the dropdown itself, so\n // we lean on the ARIA roles which are stable across MUI\n // versions.\n cy.get('[role=\"listbox\"]', { timeout: 5000 }).should('be.visible');\n cy.get('[role=\"listbox\"] [role=\"option\"]').should(\n 'have.length.at.least',\n 1\n );\n // The result item must contain the full package name.\n cy.contains('[role=\"listbox\"] [role=\"option\"]', pkgName).should(\n 'be.visible'\n );\n }\n );\n\n maybeIt(features.search.resultClickNavigation)(\n 'should navigate to the package detail page when a result is clicked',\n () => {\n // Intercept the two data endpoints that the detail route\n // fetches on mount. Waiting on these is the most reliable\n // way to know the router actually resolved the new page\n // (not just changed the URL).\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}`).as(\n 'detailSidebar'\n );\n cy.intercept(\n 'GET',\n `/-/verdaccio/data/package/readme/${pkgName}`\n ).as('detailReadme');\n\n getSearchInput().clear().type(pkgSlug, { delay: 30 });\n cy.wait(anySearchAlias(), { timeout: 10000 });\n\n cy.contains('[role=\"listbox\"] [role=\"option\"]', pkgName)\n .should('be.visible')\n .click();\n\n // Verdaccio routes package detail under /-/web/detail/<pkg>.\n cy.location('pathname').should('contain', '/-/web/detail');\n cy.location('pathname').should('contain', 'search-fixture');\n\n // Wait for the detail page's own fetches to settle so\n // assertions don't race the async content.\n cy.wait('@detailSidebar', { timeout: 10000 });\n cy.wait('@detailReadme', { timeout: 10000 });\n\n // Detail page rendered both panes end-to-end.\n cy.getByTestId(pkg.sidebar).should('be.visible');\n cy.getByTestId(pkg.readme).should('be.visible');\n }\n );\n });\n });\n}\n\n/**\n * Resolve the search input. Verdaccio 6 renders it with different\n * data-testid values across versions, so we try a few in order before\n * falling back to a generic role/type selector.\n */\nfunction getSearchInput() {\n return cy.get(\n [\n '[data-testid=\"search-input\"]',\n '[data-testid=\"header--input-search\"]',\n '[data-testid=\"autoCompleteSearch\"] input',\n 'input[aria-label*=\"earch\"]',\n 'input[placeholder*=\"earch\"]',\n 'input[type=\"search\"]',\n ].join(', '),\n { timeout: 10000 }\n );\n}\n\n/**\n * Cypress `cy.wait` only accepts one alias at a time, so pick whichever\n * alias fires first. This helper lets us stay agnostic to which search\n * endpoint the UI is wired to.\n */\nfunction anySearchAlias(): string {\n // In practice most Verdaccio 6 builds call the web search endpoint;\n // prefer that one and let the test fail loud if neither fires.\n return '@webSearch';\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"search.js","names":[],"sources":["../../../src/tests/search.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * UI tests for the Verdaccio search box.\n *\n * These tests do NOT depend on any published package — they assert the\n * search *flow* (input exists, typing triggers the search API, results\n * region updates) rather than specific package metadata. Tests that need\n * a real package in the registry should live in publishTests instead.\n */\nexport function searchTests(config: RegistryConfig) {\n const { features } = config;\n const { package: pkg } = config.testIds;\n\n describe('search', () => {\n beforeEach(() => {\n // Verdaccio's search endpoint — covers both the web API and the\n // npm v1 search API, depending on which one the UI calls.\n cy.intercept('GET', '**/-/verdaccio/data/search/**').as('webSearch');\n cy.intercept('GET', '**/-/v1/search**').as('v1Search');\n cy.intercept('GET', '**/-/verdaccio/data/packages').as('pkgs');\n\n cy.visit(config.registryUrl);\n cy.get('body').should('be.visible');\n });\n\n it('should render the search input', () => {\n getSearchInput().should('be.visible');\n });\n\n it('should fire a search request when typing a query', () => {\n const query = 'verdaccio';\n\n getSearchInput().clear().type(query, { delay: 30 });\n\n // Whichever endpoint the UI is wired to, at least one should hit.\n cy.wait(anySearchAlias(), { timeout: 10000 }).then((interception: any) => {\n expect(interception.request.url).to.contain(query);\n });\n });\n\n // NOTE: there is intentionally no \"no results / empty state\" test.\n //\n // Verdaccio 7+ removed the `searchRemote` flag (verdaccio/verdaccio#5801)\n // and the Web UI search now ALWAYS queries the configured uplinks. The CI\n // config proxies `**` to registry.npmjs.org, and npmjs' `/-/v1/search`\n // returns fuzzy / fallback matches for ANY non-empty text — so there is no\n // query that reliably yields zero results and renders the autocomplete's\n // \"No results found\" state. (On verdaccio 6, where search stayed local by\n // default, an impossible query did return `[]`, so this test used to pass.)\n // Asserting the empty state here is therefore non-deterministic; the search\n // *flow* is covered by the tests above and the published-package tests below.\n\n it('should clear the query and allow typing a new one', () => {\n getSearchInput().clear().type('first-query', { delay: 20 });\n cy.wait(anySearchAlias(), { timeout: 10000 });\n\n // Clearing should empty the input value. We deliberately do NOT\n // assert on the autocomplete dropdown's empty-state text: on a\n // registry with no packages published it lingers regardless of\n // the input value, which previously caused a false positive.\n getSearchInput().clear();\n getSearchInput().should('have.value', '');\n\n // Typing a fresh query must fire another search request so the\n // search box is still functional after a clear.\n getSearchInput().type('second-query', { delay: 20 });\n cy.wait(anySearchAlias(), { timeout: 10000 }).then((interception: any) => {\n expect(interception.request.url).to.contain('second-query');\n });\n });\n\n // ── Rendering assertions that require real package data ────────\n // Publishes a throwaway package before each test and unpublishes\n // after so the outer \"no results\" test still sees a clean registry.\n // The search query uses a substring of the package name to avoid\n // scope-parsing issues with `@` / `/` characters in the URL.\n describe('with a published package', () => {\n const pkgName = '@verdaccio/search-fixture';\n // Unique slug we can type into the search box — must be a\n // substring of pkgName so Verdaccio's search matches it.\n const pkgSlug = 'search-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 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 maybeIt(features.search.resultsDropdown)(\n 'should display the matching package in the results dropdown',\n () => {\n getSearchInput().clear().type(pkgSlug, { delay: 30 });\n\n // Wait for the search request to resolve with results.\n cy.wait(anySearchAlias(), { timeout: 10000 }).then((interception: any) => {\n expect(interception.request.url).to.contain(pkgSlug);\n });\n\n // MUI Autocomplete opens a listbox with role=\"listbox\" when\n // there are matching options. Each result renders with\n // role=\"option\". No data-testids on the dropdown itself, so\n // we lean on the ARIA roles which are stable across MUI\n // versions.\n cy.get('[role=\"listbox\"]', { timeout: 5000 }).should('be.visible');\n cy.get('[role=\"listbox\"] [role=\"option\"]').should('have.length.at.least', 1);\n // The result item must contain the full package name.\n cy.contains('[role=\"listbox\"] [role=\"option\"]', pkgName).should('be.visible');\n }\n );\n\n maybeIt(features.search.resultClickNavigation)(\n 'should navigate to the package detail page when a result is clicked',\n () => {\n // Intercept the two data endpoints that the detail route\n // fetches on mount. Waiting on these is the most reliable\n // way to know the router actually resolved the new page\n // (not just changed the URL).\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}`).as('detailSidebar');\n cy.intercept('GET', `/-/verdaccio/data/package/readme/${pkgName}`).as('detailReadme');\n\n getSearchInput().clear().type(pkgSlug, { delay: 30 });\n cy.wait(anySearchAlias(), { timeout: 10000 });\n\n cy.contains('[role=\"listbox\"] [role=\"option\"]', pkgName).should('be.visible').click();\n\n // Verdaccio routes package detail under /-/web/detail/<pkg>.\n cy.location('pathname').should('contain', '/-/web/detail');\n cy.location('pathname').should('contain', 'search-fixture');\n\n // Wait for the detail page's own fetches to settle so\n // assertions don't race the async content.\n cy.wait('@detailSidebar', { timeout: 10000 });\n cy.wait('@detailReadme', { timeout: 10000 });\n\n // Detail page rendered both panes end-to-end.\n cy.getByTestId(pkg.sidebar).should('be.visible');\n cy.getByTestId(pkg.readme).should('be.visible');\n }\n );\n });\n });\n}\n\n/**\n * Resolve the search input. Verdaccio 6 renders it with different\n * data-testid values across versions, so we try a few in order before\n * falling back to a generic role/type selector.\n */\nfunction getSearchInput() {\n return cy.get(\n [\n '[data-testid=\"search-input\"]',\n '[data-testid=\"header--input-search\"]',\n '[data-testid=\"autoCompleteSearch\"] input',\n 'input[aria-label*=\"earch\"]',\n 'input[placeholder*=\"earch\"]',\n 'input[type=\"search\"]',\n ].join(', '),\n { timeout: 10000 }\n );\n}\n\n/**\n * Cypress `cy.wait` only accepts one alias at a time, so pick whichever\n * alias fires first. This helper lets us stay agnostic to which search\n * endpoint the UI is wired to.\n */\nfunction anySearchAlias(): `@${string}` {\n // In practice most Verdaccio 6 builds call the web search endpoint;\n // prefer that one and let the test fail loud if neither fires.\n return '@webSearch';\n}\n"],"mappings":"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"}