@verdaccio/e2e-ui 2.3.0 → 2.4.1
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.
- package/build/cjs/commands/index.cjs +7 -0
- package/build/cjs/commands/index.cjs.map +1 -1
- package/build/cjs/features.cjs +9 -0
- package/build/cjs/features.cjs.map +1 -1
- package/build/cjs/index.cjs +28 -0
- package/build/cjs/index.cjs.map +1 -1
- package/build/cjs/tests/change-password.cjs +133 -0
- package/build/cjs/tests/change-password.cjs.map +1 -0
- package/build/commands/index.d.ts +11 -0
- package/build/esm/commands/index.js +7 -0
- package/build/esm/commands/index.js.map +1 -1
- package/build/esm/features.js +9 -0
- package/build/esm/features.js.map +1 -1
- package/build/esm/index.js +28 -1
- package/build/esm/index.js.map +1 -1
- package/build/esm/tests/change-password.js +133 -0
- package/build/esm/tests/change-password.js.map +1 -0
- package/build/features.d.ts +35 -0
- package/build/index.d.ts +1 -1
- package/build/tests/change-password.d.ts +29 -0
- package/build/tests/index.d.ts +1 -0
- package/package.json +1 -1
|
@@ -14,6 +14,13 @@ var DEFAULT_LOGIN_SELECTORS = {
|
|
|
14
14
|
Cypress.Commands.add("getByTestId", (selector, ...args) => {
|
|
15
15
|
return cy.get(`[data-testid=${selector}]`, ...args);
|
|
16
16
|
});
|
|
17
|
+
Cypress.Commands.add("getByLabel", (text) => {
|
|
18
|
+
return cy.contains("label", text).then(($label) => {
|
|
19
|
+
const inputId = $label.attr("for");
|
|
20
|
+
if (!inputId) throw new Error(`getByLabel: matching label has no "for" attribute (text=${String(text)})`);
|
|
21
|
+
return cy.get(`#${inputId}`);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
17
24
|
Cypress.Commands.add("login", (user, password, selectors = {}) => {
|
|
18
25
|
const loginButton = selectors.loginButton ?? DEFAULT_LOGIN_SELECTORS.loginButton;
|
|
19
26
|
const usernameInput = selectors.usernameInput ?? DEFAULT_LOGIN_SELECTORS.usernameInput;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/commands/index.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\n/**\n * Default login form selectors. Kept in sync with `DEFAULT_SELECTORS`\n * in ../testIds.ts — duplicating them here (as plain constants) lets\n * `cy.login` call sites that don't pass an explicit selectors object\n * still work without importing anything.\n */\nconst DEFAULT_LOGIN_SELECTORS = {\n loginButton: 'header--button-login',\n usernameInput: '#login--dialog-username',\n passwordInput: '#login--dialog-password',\n submitButton: '#login--dialog-button-submit',\n};\n\nexport interface LoginSelectors {\n /** data-testid of the header \"Login\" button that opens the dialog. */\n loginButton?: string;\n /** CSS selector for the username input. */\n usernameInput?: string;\n /** CSS selector for the password input. */\n passwordInput?: string;\n /** CSS selector for the submit button. */\n submitButton?: string;\n}\n\ndeclare global {\n namespace Cypress {\n interface Chainable {\n /**\n * Find element by data-testid attribute.\n */\n getByTestId(selector: string, ...args: any[]): Chainable<JQuery<HTMLElement>>;\n /**\n * Login to Verdaccio UI. Selectors default to Verdaccio 6.x\n * conventions; pass `selectors` to override any subset for\n * non-default builds.\n */\n login(\n user: string,\n password: string,\n selectors?: LoginSelectors\n ): Chainable<void>;\n }\n }\n}\n\nCypress.Commands.add('getByTestId', (selector: string, ...args: any[]) => {\n return cy.get(`[data-testid=${selector}]`, ...args);\n});\n\nCypress.Commands.add(\n 'login',\n (user: string, password: string, selectors: LoginSelectors = {}) => {\n const loginButton = selectors.loginButton ?? DEFAULT_LOGIN_SELECTORS.loginButton;\n const usernameInput =\n selectors.usernameInput ?? DEFAULT_LOGIN_SELECTORS.usernameInput;\n const passwordInput =\n selectors.passwordInput ?? DEFAULT_LOGIN_SELECTORS.passwordInput;\n const submitButton =\n selectors.submitButton ?? DEFAULT_LOGIN_SELECTORS.submitButton;\n\n cy.getByTestId(loginButton).click();\n cy.wait(300);\n cy.get(usernameInput).type(user);\n cy.wait(200);\n cy.get(passwordInput).type(password);\n cy.wait(500);\n cy.get(submitButton).click();\n }\n);\n\nexport {};\n"],"mappings":";;;;;;;AAQA,IAAM,0BAA0B;CAC9B,aAAa;CACb,eAAe;CACf,eAAe;CACf,cAAc;CACf;
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/commands/index.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\n/**\n * Default login form selectors. Kept in sync with `DEFAULT_SELECTORS`\n * in ../testIds.ts — duplicating them here (as plain constants) lets\n * `cy.login` call sites that don't pass an explicit selectors object\n * still work without importing anything.\n */\nconst DEFAULT_LOGIN_SELECTORS = {\n loginButton: 'header--button-login',\n usernameInput: '#login--dialog-username',\n passwordInput: '#login--dialog-password',\n submitButton: '#login--dialog-button-submit',\n};\n\nexport interface LoginSelectors {\n /** data-testid of the header \"Login\" button that opens the dialog. */\n loginButton?: string;\n /** CSS selector for the username input. */\n usernameInput?: string;\n /** CSS selector for the password input. */\n passwordInput?: string;\n /** CSS selector for the submit button. */\n submitButton?: string;\n}\n\ndeclare global {\n namespace Cypress {\n interface Chainable {\n /**\n * Find element by data-testid attribute.\n */\n getByTestId(selector: string, ...args: any[]): Chainable<JQuery<HTMLElement>>;\n /**\n * Find a form input whose associated <label> text matches `text`.\n *\n * Used by suites that target pages without stable `id`/testid\n * selectors on their inputs (e.g. the ChangePassword form).\n * Resolves the label's `for` attribute and returns the input\n * it points to, so `.type(...)` / `.clear()` work directly.\n *\n * `text` may be a string (substring match) or a RegExp.\n */\n getByLabel(text: string | RegExp): Chainable<JQuery<HTMLElement>>;\n /**\n * Login to Verdaccio UI. Selectors default to Verdaccio 6.x\n * conventions; pass `selectors` to override any subset for\n * non-default builds.\n */\n login(\n user: string,\n password: string,\n selectors?: LoginSelectors\n ): Chainable<void>;\n }\n }\n}\n\nCypress.Commands.add('getByTestId', (selector: string, ...args: any[]) => {\n return cy.get(`[data-testid=${selector}]`, ...args);\n});\n\nCypress.Commands.add('getByLabel', (text: string | RegExp) => {\n // Resolve the associated input via the label's `for` attribute, which\n // MUI TextField sets to the auto-generated input id. Scoping through\n // `contains()` returns the <label> element itself.\n return cy.contains('label', text).then(($label) => {\n const inputId = $label.attr('for');\n if (!inputId) {\n throw new Error(\n `getByLabel: matching label has no \"for\" attribute (text=${String(text)})`\n );\n }\n return cy.get(`#${inputId}`);\n });\n});\n\nCypress.Commands.add(\n 'login',\n (user: string, password: string, selectors: LoginSelectors = {}) => {\n const loginButton = selectors.loginButton ?? DEFAULT_LOGIN_SELECTORS.loginButton;\n const usernameInput =\n selectors.usernameInput ?? DEFAULT_LOGIN_SELECTORS.usernameInput;\n const passwordInput =\n selectors.passwordInput ?? DEFAULT_LOGIN_SELECTORS.passwordInput;\n const submitButton =\n selectors.submitButton ?? DEFAULT_LOGIN_SELECTORS.submitButton;\n\n cy.getByTestId(loginButton).click();\n cy.wait(300);\n cy.get(usernameInput).type(user);\n cy.wait(200);\n cy.get(passwordInput).type(password);\n cy.wait(500);\n cy.get(submitButton).click();\n }\n);\n\nexport {};\n"],"mappings":";;;;;;;AAQA,IAAM,0BAA0B;CAC9B,aAAa;CACb,eAAe;CACf,eAAe;CACf,cAAc;CACf;AA6CD,QAAQ,SAAS,IAAI,gBAAgB,UAAkB,GAAG,SAAgB;AACxE,QAAO,GAAG,IAAI,gBAAgB,SAAS,IAAI,GAAG,KAAK;EACnD;AAEF,QAAQ,SAAS,IAAI,eAAe,SAA0B;AAI5D,QAAO,GAAG,SAAS,SAAS,KAAK,CAAC,MAAM,WAAW;EACjD,MAAM,UAAU,OAAO,KAAK,MAAM;AAClC,MAAI,CAAC,QACH,OAAM,IAAI,MACR,2DAA2D,OAAO,KAAK,CAAC,GACzE;AAEH,SAAO,GAAG,IAAI,IAAI,UAAU;GAC5B;EACF;AAEF,QAAQ,SAAS,IACf,UACC,MAAc,UAAkB,YAA4B,EAAE,KAAK;CAClE,MAAM,cAAc,UAAU,eAAe,wBAAwB;CACrE,MAAM,gBACJ,UAAU,iBAAiB,wBAAwB;CACrD,MAAM,gBACJ,UAAU,iBAAiB,wBAAwB;CACrD,MAAM,eACJ,UAAU,gBAAgB,wBAAwB;AAEpD,IAAG,YAAY,YAAY,CAAC,OAAO;AACnC,IAAG,KAAK,IAAI;AACZ,IAAG,IAAI,cAAc,CAAC,KAAK,KAAK;AAChC,IAAG,KAAK,IAAI;AACZ,IAAG,IAAI,cAAc,CAAC,KAAK,SAAS;AACpC,IAAG,KAAK,IAAI;AACZ,IAAG,IAAI,aAAa,CAAC,OAAO;EAE/B"}
|
package/build/cjs/features.cjs
CHANGED
|
@@ -12,6 +12,11 @@ var DEFAULT_FEATURES = {
|
|
|
12
12
|
publish: {
|
|
13
13
|
downloadTarball: true,
|
|
14
14
|
rawViewer: true
|
|
15
|
+
},
|
|
16
|
+
changePassword: {
|
|
17
|
+
happyPath: true,
|
|
18
|
+
validation: true,
|
|
19
|
+
wrongOldPassword: true
|
|
15
20
|
}
|
|
16
21
|
};
|
|
17
22
|
/**
|
|
@@ -44,6 +49,10 @@ function mergeFeatures(defaults, overrides) {
|
|
|
44
49
|
publish: {
|
|
45
50
|
...defaults.publish,
|
|
46
51
|
...overrides.publish
|
|
52
|
+
},
|
|
53
|
+
changePassword: {
|
|
54
|
+
...defaults.changePassword,
|
|
55
|
+
...overrides.changePassword
|
|
47
56
|
}
|
|
48
57
|
};
|
|
49
58
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"features.cjs","names":[],"sources":["../../src/features.ts"],"sourcesContent":["/**\n * Per-test feature flags for enabling/disabling individual test cases\n * in the e2e-ui suites.\n *\n * The Verdaccio UI behaves differently across branches — search result\n * payload shape differs between the `/-/v1/search` and\n * `/-/verdaccio/data/search/*` endpoints, the language picker was\n * added in a specific minor, etc. Rather than forking the suite per\n * branch, consumers can disable individual tests via\n * `createRegistryConfig({ features: { … } })`.\n *\n * Every flag defaults to `true` (test runs). Override to `false` to\n * convert the test into a Mocha `it.skip` call — the suite still\n * reports the test but marks it as pending.\n */\nexport interface Features {\n search: {\n /**\n * Whether to run the \"results dropdown renders a matching\n * package\" test. Disable on builds where the search result shape\n * or the Autocomplete rendering differs from the default assumption.\n */\n resultsDropdown: boolean;\n /**\n * Whether to run the \"clicking a result navigates to detail\" test.\n * Depends on the Autocomplete `onSelectItem` → router wiring.\n */\n resultClickNavigation: boolean;\n };\n home: {\n /**\n * Whether to run the `with a published package` sub-block inside\n * `homeTests` (publishes a throwaway package and asserts it\n * renders in the package list).\n */\n publishedPackageRendering: boolean;\n };\n settings: {\n /**\n * Whether to run the \"change language via card click\" test.\n * Depends on the `LanguageSwitch` component layout and its\n * translation sentinels (\"Translations\", \"German\"). Skip on\n * builds that lag behind the upstream translation file.\n */\n languageSwitcher: boolean;\n };\n signin: {\n /**\n * Whether to run the three validation tests (disabled submit\n * button, invalid credentials error banner). Depends on the\n * current yup schema and the hard-coded \"Invalid username or\n * password\" error string.\n */\n validationTests: boolean;\n };\n layout: {\n /**\n * Whether to run the dark/light theme switch test. Depends on\n * `web.showThemeSwitch` (defaults to true in ui-theme) and the\n * `header--button--light` / `header--button--dark` testids.\n */\n themeSwitch: boolean;\n };\n publish: {\n /**\n * Whether to run the \"tarball download button fires a GET\" test.\n * Depends on `web.showDownloadTarball` (defaults to true) and\n * the published package manifest having a valid `dist.tarball`.\n */\n downloadTarball: boolean;\n /**\n * Whether to run the \"raw viewer dialog opens + closes\" test.\n * Depends on `web.showRaw` (defaults to true).\n */\n rawViewer: boolean;\n };\n}\n\n/** Defaults: all flags on. */\nexport const DEFAULT_FEATURES: Features = {\n search: {\n resultsDropdown: true,\n resultClickNavigation: true,\n },\n home: {\n publishedPackageRendering: true,\n },\n settings: {\n languageSwitcher: true,\n },\n signin: {\n validationTests: true,\n },\n layout: {\n themeSwitch: true,\n },\n publish: {\n downloadTarball: true,\n rawViewer: true,\n },\n};\n\nimport type { DeepPartial } from './testIds';\n\n/**\n * Merge user overrides into the default feature flags. Per-section,\n * one level deep — matching the style of `mergeTestIds`.\n */\nexport function mergeFeatures(\n defaults: Features,\n overrides?: DeepPartial<Features>\n): Features {\n if (!overrides) return defaults;\n return {\n search: { ...defaults.search, ...overrides.search },\n home: { ...defaults.home, ...overrides.home },\n settings: { ...defaults.settings, ...overrides.settings },\n signin: { ...defaults.signin, ...overrides.signin },\n layout: { ...defaults.layout, ...overrides.layout },\n publish: { ...defaults.publish, ...overrides.publish },\n };\n}\n\n/**\n * Helper that returns either `it` or `it.skip` depending on an\n * enabled flag. Usage:\n *\n * maybeIt(features.search.resultsDropdown)('…', () => { … });\n */\nexport function maybeIt(enabled: boolean): Mocha.TestFunction | Mocha.PendingTestFunction {\n return enabled ? it : it.skip;\n}\n"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"features.cjs","names":[],"sources":["../../src/features.ts"],"sourcesContent":["/**\n * Per-test feature flags for enabling/disabling individual test cases\n * in the e2e-ui suites.\n *\n * The Verdaccio UI behaves differently across branches — search result\n * payload shape differs between the `/-/v1/search` and\n * `/-/verdaccio/data/search/*` endpoints, the language picker was\n * added in a specific minor, etc. Rather than forking the suite per\n * branch, consumers can disable individual tests via\n * `createRegistryConfig({ features: { … } })`.\n *\n * Every flag defaults to `true` (test runs). Override to `false` to\n * convert the test into a Mocha `it.skip` call — the suite still\n * reports the test but marks it as pending.\n */\nexport interface Features {\n search: {\n /**\n * Whether to run the \"results dropdown renders a matching\n * package\" test. Disable on builds where the search result shape\n * or the Autocomplete rendering differs from the default assumption.\n */\n resultsDropdown: boolean;\n /**\n * Whether to run the \"clicking a result navigates to detail\" test.\n * Depends on the Autocomplete `onSelectItem` → router wiring.\n */\n resultClickNavigation: boolean;\n };\n home: {\n /**\n * Whether to run the `with a published package` sub-block inside\n * `homeTests` (publishes a throwaway package and asserts it\n * renders in the package list).\n */\n publishedPackageRendering: boolean;\n };\n settings: {\n /**\n * Whether to run the \"change language via card click\" test.\n * Depends on the `LanguageSwitch` component layout and its\n * translation sentinels (\"Translations\", \"German\"). Skip on\n * builds that lag behind the upstream translation file.\n */\n languageSwitcher: boolean;\n };\n signin: {\n /**\n * Whether to run the three validation tests (disabled submit\n * button, invalid credentials error banner). Depends on the\n * current yup schema and the hard-coded \"Invalid username or\n * password\" error string.\n */\n validationTests: boolean;\n };\n layout: {\n /**\n * Whether to run the dark/light theme switch test. Depends on\n * `web.showThemeSwitch` (defaults to true in ui-theme) and the\n * `header--button--light` / `header--button--dark` testids.\n */\n themeSwitch: boolean;\n };\n publish: {\n /**\n * Whether to run the \"tarball download button fires a GET\" test.\n * Depends on `web.showDownloadTarball` (defaults to true) and\n * the published package manifest having a valid `dist.tarball`.\n */\n downloadTarball: boolean;\n /**\n * Whether to run the \"raw viewer dialog opens + closes\" test.\n * Depends on `web.showRaw` (defaults to true).\n */\n rawViewer: boolean;\n };\n changePassword: {\n /**\n * Whether to run the happy-path test (submit valid change,\n * expect navigation to the success page, then restore the\n * original password in `after()`).\n *\n * The suite targets /-/web/change-password, which renders only\n * when the server is configured with `flags.changePassword: true`.\n * Disable on registries that do not enable the flag.\n *\n * Also disable on **published verdaccio 6.x** (all lines through\n * 6.5.0): the reset_password handler in\n * `verdaccio/build/api/web/api/user.js` ships with an inverted\n * conditional — `validatePassword(...) === false` gates the\n * `auth.changePassword(...)` call, so a *valid* new password\n * always returns HTTP 400 (`PASSWORD_VALIDATION`). The bug is\n * fixed on the development branch but has not been released\n * in any 6.x tag, so the happy path cannot succeed against an\n * `npm install verdaccio@6` runtime.\n */\n happyPath: boolean;\n /**\n * Whether to run the client-side validation tests (submit button\n * stays disabled while fields are empty / mismatched confirm).\n * Depends on the yup `changePasswordSchema`.\n */\n validation: boolean;\n /**\n * Whether to run the \"wrong old password shows error banner\" test.\n * Depends on the server rejecting the call and the onSubmit catch\n * block surfacing `\"Failed to change password\"` via\n * `LoginDialogFormError`.\n */\n wrongOldPassword: boolean;\n };\n}\n\n/** Defaults: all flags on. */\nexport const DEFAULT_FEATURES: Features = {\n search: {\n resultsDropdown: true,\n resultClickNavigation: true,\n },\n home: {\n publishedPackageRendering: true,\n },\n settings: {\n languageSwitcher: true,\n },\n signin: {\n validationTests: true,\n },\n layout: {\n themeSwitch: true,\n },\n publish: {\n downloadTarball: true,\n rawViewer: true,\n },\n changePassword: {\n happyPath: true,\n validation: true,\n wrongOldPassword: true,\n },\n};\n\nimport type { DeepPartial } from './testIds';\n\n/**\n * Merge user overrides into the default feature flags. Per-section,\n * one level deep — matching the style of `mergeTestIds`.\n */\nexport function mergeFeatures(\n defaults: Features,\n overrides?: DeepPartial<Features>\n): Features {\n if (!overrides) return defaults;\n return {\n search: { ...defaults.search, ...overrides.search },\n home: { ...defaults.home, ...overrides.home },\n settings: { ...defaults.settings, ...overrides.settings },\n signin: { ...defaults.signin, ...overrides.signin },\n layout: { ...defaults.layout, ...overrides.layout },\n publish: { ...defaults.publish, ...overrides.publish },\n changePassword: { ...defaults.changePassword, ...overrides.changePassword },\n };\n}\n\n/**\n * Helper that returns either `it` or `it.skip` depending on an\n * enabled flag. Usage:\n *\n * maybeIt(features.search.resultsDropdown)('…', () => { … });\n */\nexport function maybeIt(enabled: boolean): Mocha.TestFunction | Mocha.PendingTestFunction {\n return enabled ? it : it.skip;\n}\n"],"mappings":";;AAkHA,IAAa,mBAA6B;CACxC,QAAQ;EACN,iBAAiB;EACjB,uBAAuB;EACxB;CACD,MAAM,EACJ,2BAA2B,MAC5B;CACD,UAAU,EACR,kBAAkB,MACnB;CACD,QAAQ,EACN,iBAAiB,MAClB;CACD,QAAQ,EACN,aAAa,MACd;CACD,SAAS;EACP,iBAAiB;EACjB,WAAW;EACZ;CACD,gBAAgB;EACd,WAAW;EACX,YAAY;EACZ,kBAAkB;EACnB;CACF;;;;;AAQD,SAAgB,cACd,UACA,WACU;AACV,KAAI,CAAC,UAAW,QAAO;AACvB,QAAO;EACL,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;GAAQ;EACnD,MAAM;GAAE,GAAG,SAAS;GAAM,GAAG,UAAU;GAAM;EAC7C,UAAU;GAAE,GAAG,SAAS;GAAU,GAAG,UAAU;GAAU;EACzD,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;GAAQ;EACnD,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;GAAQ;EACnD,SAAS;GAAE,GAAG,SAAS;GAAS,GAAG,UAAU;GAAS;EACtD,gBAAgB;GAAE,GAAG,SAAS;GAAgB,GAAG,UAAU;GAAgB;EAC5E;;;;;;;;AASH,SAAgB,QAAQ,SAAkE;AACxF,QAAO,UAAU,KAAK,GAAG"}
|
package/build/cjs/index.cjs
CHANGED
|
@@ -8,6 +8,8 @@ const require_publish$1 = require("./tests/publish.cjs");
|
|
|
8
8
|
const require_search = require("./tests/search.cjs");
|
|
9
9
|
const require_settings = require("./tests/settings.cjs");
|
|
10
10
|
const require_layout = require("./tests/layout.cjs");
|
|
11
|
+
const require_change_password = require("./tests/change-password.cjs");
|
|
12
|
+
let fs = require("fs");
|
|
11
13
|
//#region src/index.ts
|
|
12
14
|
/**
|
|
13
15
|
* Build a full RegistryConfig from user-provided options with defaults.
|
|
@@ -54,6 +56,30 @@ function createRegistryConfig(options) {
|
|
|
54
56
|
*/
|
|
55
57
|
function setupVerdaccioTasks(on, options) {
|
|
56
58
|
const config = createRegistryConfig(options);
|
|
59
|
+
on("after:run", (results) => {
|
|
60
|
+
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
|
|
61
|
+
if (!summaryFile || !results || !("runs" in results)) return;
|
|
62
|
+
const cypressResults = results;
|
|
63
|
+
const lines = [];
|
|
64
|
+
lines.push("## UI E2E Test Results\n");
|
|
65
|
+
lines.push("| Spec | Tests | Passing | Failing | Pending | Duration |");
|
|
66
|
+
lines.push("|------|-------|---------|---------|---------|----------|");
|
|
67
|
+
for (const run of cypressResults.runs) {
|
|
68
|
+
const specName = run.spec.relative || run.spec.name;
|
|
69
|
+
const stats = run.stats;
|
|
70
|
+
const icon = stats.failures > 0 ? "❌" : "✅";
|
|
71
|
+
const dur = stats.duration ? `${(stats.duration / 1e3).toFixed(1)}s` : "-";
|
|
72
|
+
lines.push(`| ${icon} ${specName} | ${stats.tests} | ${stats.passes} | ${stats.failures} | ${stats.pending} | ${dur} |`);
|
|
73
|
+
}
|
|
74
|
+
lines.push("");
|
|
75
|
+
const totals = cypressResults.totalTests ?? 0;
|
|
76
|
+
const passed = cypressResults.totalPassed ?? 0;
|
|
77
|
+
const failed = cypressResults.totalFailed ?? 0;
|
|
78
|
+
const pending = cypressResults.totalPending ?? 0;
|
|
79
|
+
const emoji = failed > 0 ? "❌" : "✅";
|
|
80
|
+
lines.push(`${emoji} **${passed} passed**, **${failed} failed**, **${pending} pending** (${totals} total)\n`);
|
|
81
|
+
(0, fs.appendFileSync)(summaryFile, lines.join("\n"));
|
|
82
|
+
});
|
|
57
83
|
on("task", {
|
|
58
84
|
registry() {
|
|
59
85
|
return {
|
|
@@ -110,11 +136,13 @@ function registerAllTests(config) {
|
|
|
110
136
|
require_search.searchTests(config);
|
|
111
137
|
require_settings.settingsTests(config);
|
|
112
138
|
require_publish$1.publishTests(config);
|
|
139
|
+
require_change_password.changePasswordTests(config);
|
|
113
140
|
}
|
|
114
141
|
//#endregion
|
|
115
142
|
exports.DEFAULT_FEATURES = require_features.DEFAULT_FEATURES;
|
|
116
143
|
exports.DEFAULT_SELECTORS = require_testIds.DEFAULT_SELECTORS;
|
|
117
144
|
exports.DEFAULT_TEST_IDS = require_testIds.DEFAULT_TEST_IDS;
|
|
145
|
+
exports.changePasswordTests = require_change_password.changePasswordTests;
|
|
118
146
|
exports.cleanupPublished = require_publish.cleanupPublished;
|
|
119
147
|
exports.createRegistryConfig = createRegistryConfig;
|
|
120
148
|
exports.homeTests = require_home.homeTests;
|
package/build/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import {\n cleanupPublished,\n publishPackage,\n PublishPackageResult,\n PublishPackageTaskInput,\n unpublishPackage,\n UnpublishPackageInput,\n UnpublishPackageResult,\n} from './tasks';\nimport { DEFAULT_FEATURES, mergeFeatures } from './features';\nimport {\n DEFAULT_SELECTORS,\n DEFAULT_TEST_IDS,\n mergeSelectors,\n mergeTestIds,\n} from './testIds';\nimport { RegistryConfig, RegistryTaskResult, VerdaccioUiOptions } from './types';\n\nexport type {\n RegistryConfig,\n RegistryTaskResult,\n VerdaccioUiOptions,\n} from './types';\nexport type {\n PublishPackageInput,\n PublishPackageTaskInput,\n PublishPackageResult,\n UnpublishPackageInput,\n UnpublishPackageResult,\n} from './tasks';\nexport type { DeepPartial, Selectors, TestIds } from './testIds';\nexport type { Features } from './features';\nexport { DEFAULT_SELECTORS, DEFAULT_TEST_IDS } from './testIds';\nexport { DEFAULT_FEATURES, maybeIt } from './features';\nexport { publishPackage, cleanupPublished, unpublishPackage } from './tasks';\nexport {\n homeTests,\n signinTests,\n publishTests,\n searchTests,\n settingsTests,\n layoutTests,\n} from './tests';\n\n/**\n * Build a full RegistryConfig from user-provided options with defaults.\n *\n * `testIds` and `selectors` are deep-merged per section with the\n * defaults in `./testIds`. Consumers targeting a non-default Verdaccio\n * build can override just the fields that drifted:\n *\n * createRegistryConfig({\n * registryUrl: 'http://localhost:4873',\n * testIds: {\n * header: { settingsTooltip: 'my-new-settings-btn' },\n * },\n * });\n */\nexport function createRegistryConfig(options: VerdaccioUiOptions): RegistryConfig {\n const url = new URL(options.registryUrl);\n return {\n registryUrl: options.registryUrl,\n port: options.port ?? (parseInt(url.port, 10) || 4873),\n credentials: options.credentials ?? { user: 'test', password: 'test' },\n title: options.title ?? 'Verdaccio',\n testIds: mergeTestIds(DEFAULT_TEST_IDS, options.testIds),\n selectors: mergeSelectors(DEFAULT_SELECTORS, options.selectors),\n features: mergeFeatures(DEFAULT_FEATURES, options.features),\n };\n}\n\n/**\n * Register Verdaccio Cypress tasks in setupNodeEvents.\n *\n * Usage in cypress.config.ts:\n * import { setupVerdaccioTasks } from '@verdaccio/e2e-ui';\n *\n * export default defineConfig({\n * e2e: {\n * setupNodeEvents(on) {\n * setupVerdaccioTasks(on, { registryUrl: 'http://localhost:4873' });\n * },\n * },\n * });\n */\nexport function setupVerdaccioTasks(\n on: Cypress.PluginEvents,\n options: VerdaccioUiOptions\n): void {\n const config = createRegistryConfig(options);\n\n on('task', {\n registry(): RegistryTaskResult {\n return {\n registryUrl: config.registryUrl,\n port: config.port,\n };\n },\n /**\n * Publish a throwaway package to the registry.\n *\n * Usage from a Cypress spec:\n *\n * cy.task('publishPackage', { pkgName: '@verdaccio/pkg-scoped' });\n *\n * Any field not provided falls back to the values configured here\n * (registryUrl, credentials). The task returns a PublishPackageResult.\n */\n async publishPackage(\n input: PublishPackageTaskInput\n ): Promise<PublishPackageResult> {\n return publishPackage({\n registryUrl: input.registryUrl ?? config.registryUrl,\n credentials: input.credentials ?? config.credentials,\n pkgName: input.pkgName,\n version: input.version,\n dependencies: input.dependencies,\n devDependencies: input.devDependencies,\n unique: input.unique,\n });\n },\n /**\n * Remove a temp project folder returned by a previous publishPackage\n * call. Returns null (Cypress tasks must return something serializable).\n *\n * cy.task('cleanupPublished', result.tempFolder);\n */\n async cleanupPublished(tempFolder: string): Promise<null> {\n await cleanupPublished(tempFolder);\n return null;\n },\n /**\n * Unpublish a package from the registry so the next test starts\n * from a clean slate. Accepts either a package name or a full input\n * object (to reuse a prior tempFolder's token).\n *\n * cy.task('unpublishPackage', '@verdaccio/pkg-scoped');\n * cy.task('unpublishPackage', { pkgName, tempFolder });\n */\n async unpublishPackage(\n input: string | (Omit<UnpublishPackageInput, 'registryUrl'> & {\n registryUrl?: string;\n })\n ): Promise<UnpublishPackageResult> {\n const normalized =\n typeof input === 'string'\n ? { pkgName: input, registryUrl: config.registryUrl }\n : { ...input, registryUrl: input.registryUrl ?? config.registryUrl };\n return unpublishPackage(normalized);\n },\n });\n}\n\n/**\n * Register all Verdaccio UI tests.\n *\n * Usage in a spec file:\n * import { registerAllTests, createRegistryConfig } from '@verdaccio/e2e-ui';\n *\n * const config = createRegistryConfig({ registryUrl: 'http://localhost:4873' });\n * registerAllTests(config);\n *\n * Or pick individual suites:\n * import { homeTests, signinTests } from '@verdaccio/e2e-ui';\n *\n * const config = createRegistryConfig({ registryUrl: 'http://localhost:4873' });\n * homeTests(config);\n * signinTests(config);\n */\nexport function registerAllTests(config: RegistryConfig): void {\n homeTests(config);\n signinTests(config);\n layoutTests(config);\n searchTests(config);\n settingsTests(config);\n publishTests(config);\n}\n\n// Re-export for convenience\nimport {\n homeTests,\n signinTests,\n publishTests,\n searchTests,\n settingsTests,\n layoutTests,\n} from './tests';\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import { appendFileSync } from 'fs';\n\nimport {\n cleanupPublished,\n publishPackage,\n PublishPackageResult,\n PublishPackageTaskInput,\n unpublishPackage,\n UnpublishPackageInput,\n UnpublishPackageResult,\n} from './tasks';\nimport { DEFAULT_FEATURES, mergeFeatures } from './features';\nimport {\n DEFAULT_SELECTORS,\n DEFAULT_TEST_IDS,\n mergeSelectors,\n mergeTestIds,\n} from './testIds';\nimport { RegistryConfig, RegistryTaskResult, VerdaccioUiOptions } from './types';\n\nexport type {\n RegistryConfig,\n RegistryTaskResult,\n VerdaccioUiOptions,\n} from './types';\nexport type {\n PublishPackageInput,\n PublishPackageTaskInput,\n PublishPackageResult,\n UnpublishPackageInput,\n UnpublishPackageResult,\n} from './tasks';\nexport type { DeepPartial, Selectors, TestIds } from './testIds';\nexport type { Features } from './features';\nexport { DEFAULT_SELECTORS, DEFAULT_TEST_IDS } from './testIds';\nexport { DEFAULT_FEATURES, maybeIt } from './features';\nexport { publishPackage, cleanupPublished, unpublishPackage } from './tasks';\nexport {\n homeTests,\n signinTests,\n publishTests,\n searchTests,\n settingsTests,\n layoutTests,\n changePasswordTests,\n} from './tests';\n\n/**\n * Build a full RegistryConfig from user-provided options with defaults.\n *\n * `testIds` and `selectors` are deep-merged per section with the\n * defaults in `./testIds`. Consumers targeting a non-default Verdaccio\n * build can override just the fields that drifted:\n *\n * createRegistryConfig({\n * registryUrl: 'http://localhost:4873',\n * testIds: {\n * header: { settingsTooltip: 'my-new-settings-btn' },\n * },\n * });\n */\nexport function createRegistryConfig(options: VerdaccioUiOptions): RegistryConfig {\n const url = new URL(options.registryUrl);\n return {\n registryUrl: options.registryUrl,\n port: options.port ?? (parseInt(url.port, 10) || 4873),\n credentials: options.credentials ?? { user: 'test', password: 'test' },\n title: options.title ?? 'Verdaccio',\n testIds: mergeTestIds(DEFAULT_TEST_IDS, options.testIds),\n selectors: mergeSelectors(DEFAULT_SELECTORS, options.selectors),\n features: mergeFeatures(DEFAULT_FEATURES, options.features),\n };\n}\n\n/**\n * Register Verdaccio Cypress tasks in setupNodeEvents.\n *\n * Usage in cypress.config.ts:\n * import { setupVerdaccioTasks } from '@verdaccio/e2e-ui';\n *\n * export default defineConfig({\n * e2e: {\n * setupNodeEvents(on) {\n * setupVerdaccioTasks(on, { registryUrl: 'http://localhost:4873' });\n * },\n * },\n * });\n */\nexport function setupVerdaccioTasks(\n on: Cypress.PluginEvents,\n options: VerdaccioUiOptions\n): void {\n const config = createRegistryConfig(options);\n\n // GitHub Actions Step Summary\n on('after:run', (results) => {\n const summaryFile = process.env.GITHUB_STEP_SUMMARY;\n if (!summaryFile || !results || !('runs' in results)) return;\n\n const cypressResults = results as CypressCommandLine.CypressRunResult;\n const lines: string[] = [];\n\n lines.push('## UI E2E Test Results\\n');\n lines.push('| Spec | Tests | Passing | Failing | Pending | Duration |');\n lines.push('|------|-------|---------|---------|---------|----------|');\n\n for (const run of cypressResults.runs) {\n const specName = run.spec.relative || run.spec.name;\n const stats = run.stats;\n const icon = stats.failures > 0 ? '❌' : '✅';\n const dur = stats.duration ? `${(stats.duration / 1000).toFixed(1)}s` : '-';\n lines.push(\n `| ${icon} ${specName} | ${stats.tests} | ${stats.passes} | ${stats.failures} | ${stats.pending} | ${dur} |`\n );\n }\n\n lines.push('');\n const totals = cypressResults.totalTests ?? 0;\n const passed = cypressResults.totalPassed ?? 0;\n const failed = cypressResults.totalFailed ?? 0;\n const pending = cypressResults.totalPending ?? 0;\n const emoji = failed > 0 ? '❌' : '✅';\n lines.push(\n `${emoji} **${passed} passed**, **${failed} failed**, **${pending} pending** (${totals} total)\\n`\n );\n\n appendFileSync(summaryFile, lines.join('\\n'));\n });\n\n on('task', {\n registry(): RegistryTaskResult {\n return {\n registryUrl: config.registryUrl,\n port: config.port,\n };\n },\n /**\n * Publish a throwaway package to the registry.\n *\n * Usage from a Cypress spec:\n *\n * cy.task('publishPackage', { pkgName: '@verdaccio/pkg-scoped' });\n *\n * Any field not provided falls back to the values configured here\n * (registryUrl, credentials). The task returns a PublishPackageResult.\n */\n async publishPackage(\n input: PublishPackageTaskInput\n ): Promise<PublishPackageResult> {\n return publishPackage({\n registryUrl: input.registryUrl ?? config.registryUrl,\n credentials: input.credentials ?? config.credentials,\n pkgName: input.pkgName,\n version: input.version,\n dependencies: input.dependencies,\n devDependencies: input.devDependencies,\n unique: input.unique,\n });\n },\n /**\n * Remove a temp project folder returned by a previous publishPackage\n * call. Returns null (Cypress tasks must return something serializable).\n *\n * cy.task('cleanupPublished', result.tempFolder);\n */\n async cleanupPublished(tempFolder: string): Promise<null> {\n await cleanupPublished(tempFolder);\n return null;\n },\n /**\n * Unpublish a package from the registry so the next test starts\n * from a clean slate. Accepts either a package name or a full input\n * object (to reuse a prior tempFolder's token).\n *\n * cy.task('unpublishPackage', '@verdaccio/pkg-scoped');\n * cy.task('unpublishPackage', { pkgName, tempFolder });\n */\n async unpublishPackage(\n input: string | (Omit<UnpublishPackageInput, 'registryUrl'> & {\n registryUrl?: string;\n })\n ): Promise<UnpublishPackageResult> {\n const normalized =\n typeof input === 'string'\n ? { pkgName: input, registryUrl: config.registryUrl }\n : { ...input, registryUrl: input.registryUrl ?? config.registryUrl };\n return unpublishPackage(normalized);\n },\n });\n}\n\n/**\n * Register all Verdaccio UI tests.\n *\n * Usage in a spec file:\n * import { registerAllTests, createRegistryConfig } from '@verdaccio/e2e-ui';\n *\n * const config = createRegistryConfig({ registryUrl: 'http://localhost:4873' });\n * registerAllTests(config);\n *\n * Or pick individual suites:\n * import { homeTests, signinTests } from '@verdaccio/e2e-ui';\n *\n * const config = createRegistryConfig({ registryUrl: 'http://localhost:4873' });\n * homeTests(config);\n * signinTests(config);\n */\nexport function registerAllTests(config: RegistryConfig): void {\n homeTests(config);\n signinTests(config);\n layoutTests(config);\n searchTests(config);\n settingsTests(config);\n publishTests(config);\n changePasswordTests(config);\n}\n\n// Re-export for convenience\nimport {\n homeTests,\n signinTests,\n publishTests,\n searchTests,\n settingsTests,\n layoutTests,\n changePasswordTests,\n} from './tests';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,SAAgB,qBAAqB,SAA6C;CAChF,MAAM,MAAM,IAAI,IAAI,QAAQ,YAAY;AACxC,QAAO;EACL,aAAa,QAAQ;EACrB,MAAM,QAAQ,SAAS,SAAS,IAAI,MAAM,GAAG,IAAI;EACjD,aAAa,QAAQ,eAAe;GAAE,MAAM;GAAQ,UAAU;GAAQ;EACtE,OAAO,QAAQ,SAAS;EACxB,SAAS,gBAAA,aAAa,gBAAA,kBAAkB,QAAQ,QAAQ;EACxD,WAAW,gBAAA,eAAe,gBAAA,mBAAmB,QAAQ,UAAU;EAC/D,UAAU,iBAAA,cAAc,iBAAA,kBAAkB,QAAQ,SAAS;EAC5D;;;;;;;;;;;;;;;;AAiBH,SAAgB,oBACd,IACA,SACM;CACN,MAAM,SAAS,qBAAqB,QAAQ;AAG5C,IAAG,cAAc,YAAY;EAC3B,MAAM,cAAc,QAAQ,IAAI;AAChC,MAAI,CAAC,eAAe,CAAC,WAAW,EAAE,UAAU,SAAU;EAEtD,MAAM,iBAAiB;EACvB,MAAM,QAAkB,EAAE;AAE1B,QAAM,KAAK,2BAA2B;AACtC,QAAM,KAAK,4DAA4D;AACvE,QAAM,KAAK,4DAA4D;AAEvE,OAAK,MAAM,OAAO,eAAe,MAAM;GACrC,MAAM,WAAW,IAAI,KAAK,YAAY,IAAI,KAAK;GAC/C,MAAM,QAAQ,IAAI;GAClB,MAAM,OAAO,MAAM,WAAW,IAAI,MAAM;GACxC,MAAM,MAAM,MAAM,WAAW,IAAI,MAAM,WAAW,KAAM,QAAQ,EAAE,CAAC,KAAK;AACxE,SAAM,KACJ,KAAK,KAAK,GAAG,SAAS,KAAK,MAAM,MAAM,KAAK,MAAM,OAAO,KAAK,MAAM,SAAS,KAAK,MAAM,QAAQ,KAAK,IAAI,IAC1G;;AAGH,QAAM,KAAK,GAAG;EACd,MAAM,SAAS,eAAe,cAAc;EAC5C,MAAM,SAAS,eAAe,eAAe;EAC7C,MAAM,SAAS,eAAe,eAAe;EAC7C,MAAM,UAAU,eAAe,gBAAgB;EAC/C,MAAM,QAAQ,SAAS,IAAI,MAAM;AACjC,QAAM,KACJ,GAAG,MAAM,KAAK,OAAO,eAAe,OAAO,eAAe,QAAQ,cAAc,OAAO,WACxF;AAED,GAAA,GAAA,GAAA,gBAAe,aAAa,MAAM,KAAK,KAAK,CAAC;GAC7C;AAEF,IAAG,QAAQ;EACT,WAA+B;AAC7B,UAAO;IACL,aAAa,OAAO;IACpB,MAAM,OAAO;IACd;;EAYH,MAAM,eACJ,OAC+B;AAC/B,UAAO,gBAAA,eAAe;IACpB,aAAa,MAAM,eAAe,OAAO;IACzC,aAAa,MAAM,eAAe,OAAO;IACzC,SAAS,MAAM;IACf,SAAS,MAAM;IACf,cAAc,MAAM;IACpB,iBAAiB,MAAM;IACvB,QAAQ,MAAM;IACf,CAAC;;EAQJ,MAAM,iBAAiB,YAAmC;AACxD,SAAM,gBAAA,iBAAiB,WAAW;AAClC,UAAO;;EAUT,MAAM,iBACJ,OAGiC;AAKjC,UAAO,gBAAA,iBAHL,OAAO,UAAU,WACb;IAAE,SAAS;IAAO,aAAa,OAAO;IAAa,GACnD;IAAE,GAAG;IAAO,aAAa,MAAM,eAAe,OAAO;IAAa,CACrC;;EAEtC,CAAC;;;;;;;;;;;;;;;;;;AAmBJ,SAAgB,iBAAiB,QAA8B;AAC7D,cAAA,UAAU,OAAO;AACjB,gBAAA,YAAY,OAAO;AACnB,gBAAA,YAAY,OAAO;AACnB,gBAAA,YAAY,OAAO;AACnB,kBAAA,cAAc,OAAO;AACrB,mBAAA,aAAa,OAAO;AACpB,yBAAA,oBAAoB,OAAO"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const require_features = require("../features.cjs");
|
|
2
|
+
//#region src/tests/change-password.ts
|
|
3
|
+
/**
|
|
4
|
+
* Tests for the Change Password page at /-/web/change-password.
|
|
5
|
+
*
|
|
6
|
+
* The page renders only when the server is started with
|
|
7
|
+
* `flags.changePassword: true` (otherwise the React component
|
|
8
|
+
* redirects to `/` on mount). Each test logs in first, navigates
|
|
9
|
+
* directly to the page, and drives the form.
|
|
10
|
+
*
|
|
11
|
+
* Selector strategy: the ChangePassword form does not ship stable
|
|
12
|
+
* `id`/testid attributes on its inputs, but every field is registered
|
|
13
|
+
* via react-hook-form's `register('<name>')`, which sets a stable
|
|
14
|
+
* `name` attribute on the underlying `<input>`. The labels themselves
|
|
15
|
+
* are `t('security.changePassword.*')` calls — when the i18n bundle
|
|
16
|
+
* hasn't finished loading (or isn't loaded at all on this route), MUI
|
|
17
|
+
* renders the literal i18n key as the label, so any selector that
|
|
18
|
+
* matches on visible label text silently misses every field and the
|
|
19
|
+
* form stays empty (which would also make the mismatch test "pass"
|
|
20
|
+
* for the wrong reason: the submit button is disabled because the
|
|
21
|
+
* form is empty, not because yup rejected the mismatch).
|
|
22
|
+
*
|
|
23
|
+
* The stable, i18n-independent contract from ChangePassword.tsx is:
|
|
24
|
+
* register('username') → input[name="username"]
|
|
25
|
+
* register('oldPassword') → input[name="oldPassword"]
|
|
26
|
+
* register('newPassword') → input[name="newPassword"]
|
|
27
|
+
* register('confirmPassword') → input[name="confirmPassword"]
|
|
28
|
+
* submit button → form button[type="submit"]
|
|
29
|
+
*/
|
|
30
|
+
function changePasswordTests(config) {
|
|
31
|
+
const { header, login } = config.testIds;
|
|
32
|
+
const { loginDialog } = config.selectors;
|
|
33
|
+
const { features } = config;
|
|
34
|
+
const GENERIC_FAILURE_TEXT = "Failed to change password";
|
|
35
|
+
describe("change password", () => {
|
|
36
|
+
const CHANGE_PASSWORD_PATH = "/-/web/change-password";
|
|
37
|
+
const { user, password } = config.credentials;
|
|
38
|
+
/**
|
|
39
|
+
* Tests mutate the user's password. We track the "current" value
|
|
40
|
+
* across tests so the `after()` hook can restore the original,
|
|
41
|
+
* leaving the registry in the same state other suites assume.
|
|
42
|
+
*/
|
|
43
|
+
let currentPassword = password;
|
|
44
|
+
/**
|
|
45
|
+
* Capability check. The ChangePassword page's `useEffect` redirects
|
|
46
|
+
* to `/` whenever `configuration.flags.changePassword` is not truthy
|
|
47
|
+
* — which is the case on any registry that either (a) didn't set
|
|
48
|
+
* `flags.changePassword: true` in its config, or (b) runs a
|
|
49
|
+
* verdaccio build whose middleware doesn't yet propagate the flag
|
|
50
|
+
* into `__VERDACCIO_BASENAME_UI_OPTIONS` (older `verdaccio:6`
|
|
51
|
+
* tagged images fall in this bucket).
|
|
52
|
+
*
|
|
53
|
+
* In either case the suite has nothing to exercise, so skip the
|
|
54
|
+
* whole describe with a clear reason rather than burning five
|
|
55
|
+
* seconds per test on cy.contains timeouts.
|
|
56
|
+
*/
|
|
57
|
+
before(function() {
|
|
58
|
+
cy.visit(config.registryUrl);
|
|
59
|
+
cy.window().then((win) => {
|
|
60
|
+
const opts = win.__VERDACCIO_BASENAME_UI_OPTIONS;
|
|
61
|
+
if (!!!opts?.flags?.changePassword) {
|
|
62
|
+
console.warn("[change-password] server did not advertise flags.changePassword=true — skipping suite. ui-options.flags: " + JSON.stringify(opts?.flags ?? {}));
|
|
63
|
+
this.skip();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
cy.intercept("POST", "/-/verdaccio/sec/login").as("signChangePwd");
|
|
69
|
+
cy.visit(config.registryUrl);
|
|
70
|
+
cy.login(user, currentPassword, {
|
|
71
|
+
loginButton: header.loginButton,
|
|
72
|
+
...loginDialog
|
|
73
|
+
});
|
|
74
|
+
cy.wait("@signChangePwd").its("response.statusCode").should("eq", 200);
|
|
75
|
+
cy.visit(CHANGE_PASSWORD_PATH);
|
|
76
|
+
cy.get("form button[type=\"submit\"]", { timeout: 5e3 }).should("be.visible");
|
|
77
|
+
});
|
|
78
|
+
after(() => {
|
|
79
|
+
if (currentPassword === password) return;
|
|
80
|
+
cy.intercept("POST", "/-/verdaccio/sec/login").as("signChangePwdRestore");
|
|
81
|
+
cy.visit(config.registryUrl);
|
|
82
|
+
cy.login(user, currentPassword, {
|
|
83
|
+
loginButton: header.loginButton,
|
|
84
|
+
...loginDialog
|
|
85
|
+
});
|
|
86
|
+
cy.wait("@signChangePwdRestore").its("response.statusCode").should("eq", 200);
|
|
87
|
+
cy.visit(CHANGE_PASSWORD_PATH);
|
|
88
|
+
cy.get("input[name=\"username\"]").type(user);
|
|
89
|
+
cy.get("input[name=\"oldPassword\"]").type(currentPassword);
|
|
90
|
+
cy.get("input[name=\"newPassword\"]").type(password);
|
|
91
|
+
cy.get("input[name=\"confirmPassword\"]").type(password);
|
|
92
|
+
cy.get("form button[type=\"submit\"]").click();
|
|
93
|
+
currentPassword = password;
|
|
94
|
+
});
|
|
95
|
+
require_features.maybeIt(features.changePassword.validation)("should disable the submit button while the form is empty", () => {
|
|
96
|
+
cy.get("form button[type=\"submit\"]").should("be.disabled");
|
|
97
|
+
});
|
|
98
|
+
require_features.maybeIt(features.changePassword.validation)("should keep submit disabled when new and confirm passwords mismatch", () => {
|
|
99
|
+
cy.get("input[name=\"username\"]").type(user);
|
|
100
|
+
cy.get("input[name=\"oldPassword\"]").type(currentPassword);
|
|
101
|
+
cy.get("input[name=\"newPassword\"]").type("newSecretPass123");
|
|
102
|
+
cy.get("input[name=\"confirmPassword\"]").type("different-value");
|
|
103
|
+
cy.get("form button[type=\"submit\"]").should("be.disabled");
|
|
104
|
+
});
|
|
105
|
+
require_features.maybeIt(features.changePassword.wrongOldPassword)("should show an error banner when the old password is wrong", () => {
|
|
106
|
+
cy.intercept("PUT", "/-/verdaccio/sec/reset_password").as("reset");
|
|
107
|
+
cy.get("input[name=\"username\"]").type(user);
|
|
108
|
+
cy.get("input[name=\"oldPassword\"]").type("definitely-wrong-xyz");
|
|
109
|
+
cy.get("input[name=\"newPassword\"]").type("newSecretPass123");
|
|
110
|
+
cy.get("input[name=\"confirmPassword\"]").type("newSecretPass123");
|
|
111
|
+
cy.get("form button[type=\"submit\"]").should("not.be.disabled").click();
|
|
112
|
+
cy.wait("@reset").its("response.statusCode").should("not.eq", 200);
|
|
113
|
+
cy.getByTestId(login.error, { timeout: 5e3 }).should("be.visible").and("contain.text", GENERIC_FAILURE_TEXT);
|
|
114
|
+
cy.location("pathname").should("include", CHANGE_PASSWORD_PATH);
|
|
115
|
+
});
|
|
116
|
+
require_features.maybeIt(features.changePassword.happyPath)("should change the password and navigate to the success page", () => {
|
|
117
|
+
const newPassword = `${currentPassword}-rotated`;
|
|
118
|
+
cy.intercept("PUT", "/-/verdaccio/sec/reset_password").as("reset");
|
|
119
|
+
cy.get("input[name=\"username\"]").type(user);
|
|
120
|
+
cy.get("input[name=\"oldPassword\"]").type(currentPassword);
|
|
121
|
+
cy.get("input[name=\"newPassword\"]").type(newPassword);
|
|
122
|
+
cy.get("input[name=\"confirmPassword\"]").type(newPassword);
|
|
123
|
+
cy.get("form button[type=\"submit\"]").should("not.be.disabled").click();
|
|
124
|
+
cy.wait("@reset").its("response.statusCode").should("eq", 200);
|
|
125
|
+
cy.location("pathname", { timeout: 5e3 }).should("include", "/-/web/success");
|
|
126
|
+
currentPassword = newPassword;
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
exports.changePasswordTests = changePasswordTests;
|
|
132
|
+
|
|
133
|
+
//# sourceMappingURL=change-password.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"change-password.cjs","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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"}
|
|
@@ -17,6 +17,17 @@ declare global {
|
|
|
17
17
|
* Find element by data-testid attribute.
|
|
18
18
|
*/
|
|
19
19
|
getByTestId(selector: string, ...args: any[]): Chainable<JQuery<HTMLElement>>;
|
|
20
|
+
/**
|
|
21
|
+
* Find a form input whose associated <label> text matches `text`.
|
|
22
|
+
*
|
|
23
|
+
* Used by suites that target pages without stable `id`/testid
|
|
24
|
+
* selectors on their inputs (e.g. the ChangePassword form).
|
|
25
|
+
* Resolves the label's `for` attribute and returns the input
|
|
26
|
+
* it points to, so `.type(...)` / `.clear()` work directly.
|
|
27
|
+
*
|
|
28
|
+
* `text` may be a string (substring match) or a RegExp.
|
|
29
|
+
*/
|
|
30
|
+
getByLabel(text: string | RegExp): Chainable<JQuery<HTMLElement>>;
|
|
20
31
|
/**
|
|
21
32
|
* Login to Verdaccio UI. Selectors default to Verdaccio 6.x
|
|
22
33
|
* conventions; pass `selectors` to override any subset for
|
|
@@ -14,6 +14,13 @@ var DEFAULT_LOGIN_SELECTORS = {
|
|
|
14
14
|
Cypress.Commands.add("getByTestId", (selector, ...args) => {
|
|
15
15
|
return cy.get(`[data-testid=${selector}]`, ...args);
|
|
16
16
|
});
|
|
17
|
+
Cypress.Commands.add("getByLabel", (text) => {
|
|
18
|
+
return cy.contains("label", text).then(($label) => {
|
|
19
|
+
const inputId = $label.attr("for");
|
|
20
|
+
if (!inputId) throw new Error(`getByLabel: matching label has no "for" attribute (text=${String(text)})`);
|
|
21
|
+
return cy.get(`#${inputId}`);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
17
24
|
Cypress.Commands.add("login", (user, password, selectors = {}) => {
|
|
18
25
|
const loginButton = selectors.loginButton ?? DEFAULT_LOGIN_SELECTORS.loginButton;
|
|
19
26
|
const usernameInput = selectors.usernameInput ?? DEFAULT_LOGIN_SELECTORS.usernameInput;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/commands/index.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\n/**\n * Default login form selectors. Kept in sync with `DEFAULT_SELECTORS`\n * in ../testIds.ts — duplicating them here (as plain constants) lets\n * `cy.login` call sites that don't pass an explicit selectors object\n * still work without importing anything.\n */\nconst DEFAULT_LOGIN_SELECTORS = {\n loginButton: 'header--button-login',\n usernameInput: '#login--dialog-username',\n passwordInput: '#login--dialog-password',\n submitButton: '#login--dialog-button-submit',\n};\n\nexport interface LoginSelectors {\n /** data-testid of the header \"Login\" button that opens the dialog. */\n loginButton?: string;\n /** CSS selector for the username input. */\n usernameInput?: string;\n /** CSS selector for the password input. */\n passwordInput?: string;\n /** CSS selector for the submit button. */\n submitButton?: string;\n}\n\ndeclare global {\n namespace Cypress {\n interface Chainable {\n /**\n * Find element by data-testid attribute.\n */\n getByTestId(selector: string, ...args: any[]): Chainable<JQuery<HTMLElement>>;\n /**\n * Login to Verdaccio UI. Selectors default to Verdaccio 6.x\n * conventions; pass `selectors` to override any subset for\n * non-default builds.\n */\n login(\n user: string,\n password: string,\n selectors?: LoginSelectors\n ): Chainable<void>;\n }\n }\n}\n\nCypress.Commands.add('getByTestId', (selector: string, ...args: any[]) => {\n return cy.get(`[data-testid=${selector}]`, ...args);\n});\n\nCypress.Commands.add(\n 'login',\n (user: string, password: string, selectors: LoginSelectors = {}) => {\n const loginButton = selectors.loginButton ?? DEFAULT_LOGIN_SELECTORS.loginButton;\n const usernameInput =\n selectors.usernameInput ?? DEFAULT_LOGIN_SELECTORS.usernameInput;\n const passwordInput =\n selectors.passwordInput ?? DEFAULT_LOGIN_SELECTORS.passwordInput;\n const submitButton =\n selectors.submitButton ?? DEFAULT_LOGIN_SELECTORS.submitButton;\n\n cy.getByTestId(loginButton).click();\n cy.wait(300);\n cy.get(usernameInput).type(user);\n cy.wait(200);\n cy.get(passwordInput).type(password);\n cy.wait(500);\n cy.get(submitButton).click();\n }\n);\n\nexport {};\n"],"mappings":";;;;;;;AAQA,IAAM,0BAA0B;CAC9B,aAAa;CACb,eAAe;CACf,eAAe;CACf,cAAc;CACf;
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/commands/index.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\n/**\n * Default login form selectors. Kept in sync with `DEFAULT_SELECTORS`\n * in ../testIds.ts — duplicating them here (as plain constants) lets\n * `cy.login` call sites that don't pass an explicit selectors object\n * still work without importing anything.\n */\nconst DEFAULT_LOGIN_SELECTORS = {\n loginButton: 'header--button-login',\n usernameInput: '#login--dialog-username',\n passwordInput: '#login--dialog-password',\n submitButton: '#login--dialog-button-submit',\n};\n\nexport interface LoginSelectors {\n /** data-testid of the header \"Login\" button that opens the dialog. */\n loginButton?: string;\n /** CSS selector for the username input. */\n usernameInput?: string;\n /** CSS selector for the password input. */\n passwordInput?: string;\n /** CSS selector for the submit button. */\n submitButton?: string;\n}\n\ndeclare global {\n namespace Cypress {\n interface Chainable {\n /**\n * Find element by data-testid attribute.\n */\n getByTestId(selector: string, ...args: any[]): Chainable<JQuery<HTMLElement>>;\n /**\n * Find a form input whose associated <label> text matches `text`.\n *\n * Used by suites that target pages without stable `id`/testid\n * selectors on their inputs (e.g. the ChangePassword form).\n * Resolves the label's `for` attribute and returns the input\n * it points to, so `.type(...)` / `.clear()` work directly.\n *\n * `text` may be a string (substring match) or a RegExp.\n */\n getByLabel(text: string | RegExp): Chainable<JQuery<HTMLElement>>;\n /**\n * Login to Verdaccio UI. Selectors default to Verdaccio 6.x\n * conventions; pass `selectors` to override any subset for\n * non-default builds.\n */\n login(\n user: string,\n password: string,\n selectors?: LoginSelectors\n ): Chainable<void>;\n }\n }\n}\n\nCypress.Commands.add('getByTestId', (selector: string, ...args: any[]) => {\n return cy.get(`[data-testid=${selector}]`, ...args);\n});\n\nCypress.Commands.add('getByLabel', (text: string | RegExp) => {\n // Resolve the associated input via the label's `for` attribute, which\n // MUI TextField sets to the auto-generated input id. Scoping through\n // `contains()` returns the <label> element itself.\n return cy.contains('label', text).then(($label) => {\n const inputId = $label.attr('for');\n if (!inputId) {\n throw new Error(\n `getByLabel: matching label has no \"for\" attribute (text=${String(text)})`\n );\n }\n return cy.get(`#${inputId}`);\n });\n});\n\nCypress.Commands.add(\n 'login',\n (user: string, password: string, selectors: LoginSelectors = {}) => {\n const loginButton = selectors.loginButton ?? DEFAULT_LOGIN_SELECTORS.loginButton;\n const usernameInput =\n selectors.usernameInput ?? DEFAULT_LOGIN_SELECTORS.usernameInput;\n const passwordInput =\n selectors.passwordInput ?? DEFAULT_LOGIN_SELECTORS.passwordInput;\n const submitButton =\n selectors.submitButton ?? DEFAULT_LOGIN_SELECTORS.submitButton;\n\n cy.getByTestId(loginButton).click();\n cy.wait(300);\n cy.get(usernameInput).type(user);\n cy.wait(200);\n cy.get(passwordInput).type(password);\n cy.wait(500);\n cy.get(submitButton).click();\n }\n);\n\nexport {};\n"],"mappings":";;;;;;;AAQA,IAAM,0BAA0B;CAC9B,aAAa;CACb,eAAe;CACf,eAAe;CACf,cAAc;CACf;AA6CD,QAAQ,SAAS,IAAI,gBAAgB,UAAkB,GAAG,SAAgB;AACxE,QAAO,GAAG,IAAI,gBAAgB,SAAS,IAAI,GAAG,KAAK;EACnD;AAEF,QAAQ,SAAS,IAAI,eAAe,SAA0B;AAI5D,QAAO,GAAG,SAAS,SAAS,KAAK,CAAC,MAAM,WAAW;EACjD,MAAM,UAAU,OAAO,KAAK,MAAM;AAClC,MAAI,CAAC,QACH,OAAM,IAAI,MACR,2DAA2D,OAAO,KAAK,CAAC,GACzE;AAEH,SAAO,GAAG,IAAI,IAAI,UAAU;GAC5B;EACF;AAEF,QAAQ,SAAS,IACf,UACC,MAAc,UAAkB,YAA4B,EAAE,KAAK;CAClE,MAAM,cAAc,UAAU,eAAe,wBAAwB;CACrE,MAAM,gBACJ,UAAU,iBAAiB,wBAAwB;CACrD,MAAM,gBACJ,UAAU,iBAAiB,wBAAwB;CACrD,MAAM,eACJ,UAAU,gBAAgB,wBAAwB;AAEpD,IAAG,YAAY,YAAY,CAAC,OAAO;AACnC,IAAG,KAAK,IAAI;AACZ,IAAG,IAAI,cAAc,CAAC,KAAK,KAAK;AAChC,IAAG,KAAK,IAAI;AACZ,IAAG,IAAI,cAAc,CAAC,KAAK,SAAS;AACpC,IAAG,KAAK,IAAI;AACZ,IAAG,IAAI,aAAa,CAAC,OAAO;EAE/B"}
|
package/build/esm/features.js
CHANGED
|
@@ -12,6 +12,11 @@ var DEFAULT_FEATURES = {
|
|
|
12
12
|
publish: {
|
|
13
13
|
downloadTarball: true,
|
|
14
14
|
rawViewer: true
|
|
15
|
+
},
|
|
16
|
+
changePassword: {
|
|
17
|
+
happyPath: true,
|
|
18
|
+
validation: true,
|
|
19
|
+
wrongOldPassword: true
|
|
15
20
|
}
|
|
16
21
|
};
|
|
17
22
|
/**
|
|
@@ -44,6 +49,10 @@ function mergeFeatures(defaults, overrides) {
|
|
|
44
49
|
publish: {
|
|
45
50
|
...defaults.publish,
|
|
46
51
|
...overrides.publish
|
|
52
|
+
},
|
|
53
|
+
changePassword: {
|
|
54
|
+
...defaults.changePassword,
|
|
55
|
+
...overrides.changePassword
|
|
47
56
|
}
|
|
48
57
|
};
|
|
49
58
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"features.js","names":[],"sources":["../../src/features.ts"],"sourcesContent":["/**\n * Per-test feature flags for enabling/disabling individual test cases\n * in the e2e-ui suites.\n *\n * The Verdaccio UI behaves differently across branches — search result\n * payload shape differs between the `/-/v1/search` and\n * `/-/verdaccio/data/search/*` endpoints, the language picker was\n * added in a specific minor, etc. Rather than forking the suite per\n * branch, consumers can disable individual tests via\n * `createRegistryConfig({ features: { … } })`.\n *\n * Every flag defaults to `true` (test runs). Override to `false` to\n * convert the test into a Mocha `it.skip` call — the suite still\n * reports the test but marks it as pending.\n */\nexport interface Features {\n search: {\n /**\n * Whether to run the \"results dropdown renders a matching\n * package\" test. Disable on builds where the search result shape\n * or the Autocomplete rendering differs from the default assumption.\n */\n resultsDropdown: boolean;\n /**\n * Whether to run the \"clicking a result navigates to detail\" test.\n * Depends on the Autocomplete `onSelectItem` → router wiring.\n */\n resultClickNavigation: boolean;\n };\n home: {\n /**\n * Whether to run the `with a published package` sub-block inside\n * `homeTests` (publishes a throwaway package and asserts it\n * renders in the package list).\n */\n publishedPackageRendering: boolean;\n };\n settings: {\n /**\n * Whether to run the \"change language via card click\" test.\n * Depends on the `LanguageSwitch` component layout and its\n * translation sentinels (\"Translations\", \"German\"). Skip on\n * builds that lag behind the upstream translation file.\n */\n languageSwitcher: boolean;\n };\n signin: {\n /**\n * Whether to run the three validation tests (disabled submit\n * button, invalid credentials error banner). Depends on the\n * current yup schema and the hard-coded \"Invalid username or\n * password\" error string.\n */\n validationTests: boolean;\n };\n layout: {\n /**\n * Whether to run the dark/light theme switch test. Depends on\n * `web.showThemeSwitch` (defaults to true in ui-theme) and the\n * `header--button--light` / `header--button--dark` testids.\n */\n themeSwitch: boolean;\n };\n publish: {\n /**\n * Whether to run the \"tarball download button fires a GET\" test.\n * Depends on `web.showDownloadTarball` (defaults to true) and\n * the published package manifest having a valid `dist.tarball`.\n */\n downloadTarball: boolean;\n /**\n * Whether to run the \"raw viewer dialog opens + closes\" test.\n * Depends on `web.showRaw` (defaults to true).\n */\n rawViewer: boolean;\n };\n}\n\n/** Defaults: all flags on. */\nexport const DEFAULT_FEATURES: Features = {\n search: {\n resultsDropdown: true,\n resultClickNavigation: true,\n },\n home: {\n publishedPackageRendering: true,\n },\n settings: {\n languageSwitcher: true,\n },\n signin: {\n validationTests: true,\n },\n layout: {\n themeSwitch: true,\n },\n publish: {\n downloadTarball: true,\n rawViewer: true,\n },\n};\n\nimport type { DeepPartial } from './testIds';\n\n/**\n * Merge user overrides into the default feature flags. Per-section,\n * one level deep — matching the style of `mergeTestIds`.\n */\nexport function mergeFeatures(\n defaults: Features,\n overrides?: DeepPartial<Features>\n): Features {\n if (!overrides) return defaults;\n return {\n search: { ...defaults.search, ...overrides.search },\n home: { ...defaults.home, ...overrides.home },\n settings: { ...defaults.settings, ...overrides.settings },\n signin: { ...defaults.signin, ...overrides.signin },\n layout: { ...defaults.layout, ...overrides.layout },\n publish: { ...defaults.publish, ...overrides.publish },\n };\n}\n\n/**\n * Helper that returns either `it` or `it.skip` depending on an\n * enabled flag. Usage:\n *\n * maybeIt(features.search.resultsDropdown)('…', () => { … });\n */\nexport function maybeIt(enabled: boolean): Mocha.TestFunction | Mocha.PendingTestFunction {\n return enabled ? it : it.skip;\n}\n"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"features.js","names":[],"sources":["../../src/features.ts"],"sourcesContent":["/**\n * Per-test feature flags for enabling/disabling individual test cases\n * in the e2e-ui suites.\n *\n * The Verdaccio UI behaves differently across branches — search result\n * payload shape differs between the `/-/v1/search` and\n * `/-/verdaccio/data/search/*` endpoints, the language picker was\n * added in a specific minor, etc. Rather than forking the suite per\n * branch, consumers can disable individual tests via\n * `createRegistryConfig({ features: { … } })`.\n *\n * Every flag defaults to `true` (test runs). Override to `false` to\n * convert the test into a Mocha `it.skip` call — the suite still\n * reports the test but marks it as pending.\n */\nexport interface Features {\n search: {\n /**\n * Whether to run the \"results dropdown renders a matching\n * package\" test. Disable on builds where the search result shape\n * or the Autocomplete rendering differs from the default assumption.\n */\n resultsDropdown: boolean;\n /**\n * Whether to run the \"clicking a result navigates to detail\" test.\n * Depends on the Autocomplete `onSelectItem` → router wiring.\n */\n resultClickNavigation: boolean;\n };\n home: {\n /**\n * Whether to run the `with a published package` sub-block inside\n * `homeTests` (publishes a throwaway package and asserts it\n * renders in the package list).\n */\n publishedPackageRendering: boolean;\n };\n settings: {\n /**\n * Whether to run the \"change language via card click\" test.\n * Depends on the `LanguageSwitch` component layout and its\n * translation sentinels (\"Translations\", \"German\"). Skip on\n * builds that lag behind the upstream translation file.\n */\n languageSwitcher: boolean;\n };\n signin: {\n /**\n * Whether to run the three validation tests (disabled submit\n * button, invalid credentials error banner). Depends on the\n * current yup schema and the hard-coded \"Invalid username or\n * password\" error string.\n */\n validationTests: boolean;\n };\n layout: {\n /**\n * Whether to run the dark/light theme switch test. Depends on\n * `web.showThemeSwitch` (defaults to true in ui-theme) and the\n * `header--button--light` / `header--button--dark` testids.\n */\n themeSwitch: boolean;\n };\n publish: {\n /**\n * Whether to run the \"tarball download button fires a GET\" test.\n * Depends on `web.showDownloadTarball` (defaults to true) and\n * the published package manifest having a valid `dist.tarball`.\n */\n downloadTarball: boolean;\n /**\n * Whether to run the \"raw viewer dialog opens + closes\" test.\n * Depends on `web.showRaw` (defaults to true).\n */\n rawViewer: boolean;\n };\n changePassword: {\n /**\n * Whether to run the happy-path test (submit valid change,\n * expect navigation to the success page, then restore the\n * original password in `after()`).\n *\n * The suite targets /-/web/change-password, which renders only\n * when the server is configured with `flags.changePassword: true`.\n * Disable on registries that do not enable the flag.\n *\n * Also disable on **published verdaccio 6.x** (all lines through\n * 6.5.0): the reset_password handler in\n * `verdaccio/build/api/web/api/user.js` ships with an inverted\n * conditional — `validatePassword(...) === false` gates the\n * `auth.changePassword(...)` call, so a *valid* new password\n * always returns HTTP 400 (`PASSWORD_VALIDATION`). The bug is\n * fixed on the development branch but has not been released\n * in any 6.x tag, so the happy path cannot succeed against an\n * `npm install verdaccio@6` runtime.\n */\n happyPath: boolean;\n /**\n * Whether to run the client-side validation tests (submit button\n * stays disabled while fields are empty / mismatched confirm).\n * Depends on the yup `changePasswordSchema`.\n */\n validation: boolean;\n /**\n * Whether to run the \"wrong old password shows error banner\" test.\n * Depends on the server rejecting the call and the onSubmit catch\n * block surfacing `\"Failed to change password\"` via\n * `LoginDialogFormError`.\n */\n wrongOldPassword: boolean;\n };\n}\n\n/** Defaults: all flags on. */\nexport const DEFAULT_FEATURES: Features = {\n search: {\n resultsDropdown: true,\n resultClickNavigation: true,\n },\n home: {\n publishedPackageRendering: true,\n },\n settings: {\n languageSwitcher: true,\n },\n signin: {\n validationTests: true,\n },\n layout: {\n themeSwitch: true,\n },\n publish: {\n downloadTarball: true,\n rawViewer: true,\n },\n changePassword: {\n happyPath: true,\n validation: true,\n wrongOldPassword: true,\n },\n};\n\nimport type { DeepPartial } from './testIds';\n\n/**\n * Merge user overrides into the default feature flags. Per-section,\n * one level deep — matching the style of `mergeTestIds`.\n */\nexport function mergeFeatures(\n defaults: Features,\n overrides?: DeepPartial<Features>\n): Features {\n if (!overrides) return defaults;\n return {\n search: { ...defaults.search, ...overrides.search },\n home: { ...defaults.home, ...overrides.home },\n settings: { ...defaults.settings, ...overrides.settings },\n signin: { ...defaults.signin, ...overrides.signin },\n layout: { ...defaults.layout, ...overrides.layout },\n publish: { ...defaults.publish, ...overrides.publish },\n changePassword: { ...defaults.changePassword, ...overrides.changePassword },\n };\n}\n\n/**\n * Helper that returns either `it` or `it.skip` depending on an\n * enabled flag. Usage:\n *\n * maybeIt(features.search.resultsDropdown)('…', () => { … });\n */\nexport function maybeIt(enabled: boolean): Mocha.TestFunction | Mocha.PendingTestFunction {\n return enabled ? it : it.skip;\n}\n"],"mappings":";;AAkHA,IAAa,mBAA6B;CACxC,QAAQ;EACN,iBAAiB;EACjB,uBAAuB;EACxB;CACD,MAAM,EACJ,2BAA2B,MAC5B;CACD,UAAU,EACR,kBAAkB,MACnB;CACD,QAAQ,EACN,iBAAiB,MAClB;CACD,QAAQ,EACN,aAAa,MACd;CACD,SAAS;EACP,iBAAiB;EACjB,WAAW;EACZ;CACD,gBAAgB;EACd,WAAW;EACX,YAAY;EACZ,kBAAkB;EACnB;CACF;;;;;AAQD,SAAgB,cACd,UACA,WACU;AACV,KAAI,CAAC,UAAW,QAAO;AACvB,QAAO;EACL,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;GAAQ;EACnD,MAAM;GAAE,GAAG,SAAS;GAAM,GAAG,UAAU;GAAM;EAC7C,UAAU;GAAE,GAAG,SAAS;GAAU,GAAG,UAAU;GAAU;EACzD,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;GAAQ;EACnD,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;GAAQ;EACnD,SAAS;GAAE,GAAG,SAAS;GAAS,GAAG,UAAU;GAAS;EACtD,gBAAgB;GAAE,GAAG,SAAS;GAAgB,GAAG,UAAU;GAAgB;EAC5E;;;;;;;;AASH,SAAgB,QAAQ,SAAkE;AACxF,QAAO,UAAU,KAAK,GAAG"}
|
package/build/esm/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import { publishTests } from "./tests/publish.js";
|
|
|
7
7
|
import { searchTests } from "./tests/search.js";
|
|
8
8
|
import { settingsTests } from "./tests/settings.js";
|
|
9
9
|
import { layoutTests } from "./tests/layout.js";
|
|
10
|
+
import { changePasswordTests } from "./tests/change-password.js";
|
|
11
|
+
import { appendFileSync } from "fs";
|
|
10
12
|
//#region src/index.ts
|
|
11
13
|
/**
|
|
12
14
|
* Build a full RegistryConfig from user-provided options with defaults.
|
|
@@ -53,6 +55,30 @@ function createRegistryConfig(options) {
|
|
|
53
55
|
*/
|
|
54
56
|
function setupVerdaccioTasks(on, options) {
|
|
55
57
|
const config = createRegistryConfig(options);
|
|
58
|
+
on("after:run", (results) => {
|
|
59
|
+
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
|
|
60
|
+
if (!summaryFile || !results || !("runs" in results)) return;
|
|
61
|
+
const cypressResults = results;
|
|
62
|
+
const lines = [];
|
|
63
|
+
lines.push("## UI E2E Test Results\n");
|
|
64
|
+
lines.push("| Spec | Tests | Passing | Failing | Pending | Duration |");
|
|
65
|
+
lines.push("|------|-------|---------|---------|---------|----------|");
|
|
66
|
+
for (const run of cypressResults.runs) {
|
|
67
|
+
const specName = run.spec.relative || run.spec.name;
|
|
68
|
+
const stats = run.stats;
|
|
69
|
+
const icon = stats.failures > 0 ? "❌" : "✅";
|
|
70
|
+
const dur = stats.duration ? `${(stats.duration / 1e3).toFixed(1)}s` : "-";
|
|
71
|
+
lines.push(`| ${icon} ${specName} | ${stats.tests} | ${stats.passes} | ${stats.failures} | ${stats.pending} | ${dur} |`);
|
|
72
|
+
}
|
|
73
|
+
lines.push("");
|
|
74
|
+
const totals = cypressResults.totalTests ?? 0;
|
|
75
|
+
const passed = cypressResults.totalPassed ?? 0;
|
|
76
|
+
const failed = cypressResults.totalFailed ?? 0;
|
|
77
|
+
const pending = cypressResults.totalPending ?? 0;
|
|
78
|
+
const emoji = failed > 0 ? "❌" : "✅";
|
|
79
|
+
lines.push(`${emoji} **${passed} passed**, **${failed} failed**, **${pending} pending** (${totals} total)\n`);
|
|
80
|
+
appendFileSync(summaryFile, lines.join("\n"));
|
|
81
|
+
});
|
|
56
82
|
on("task", {
|
|
57
83
|
registry() {
|
|
58
84
|
return {
|
|
@@ -109,8 +135,9 @@ function registerAllTests(config) {
|
|
|
109
135
|
searchTests(config);
|
|
110
136
|
settingsTests(config);
|
|
111
137
|
publishTests(config);
|
|
138
|
+
changePasswordTests(config);
|
|
112
139
|
}
|
|
113
140
|
//#endregion
|
|
114
|
-
export { DEFAULT_FEATURES, DEFAULT_SELECTORS, DEFAULT_TEST_IDS, cleanupPublished, createRegistryConfig, homeTests, layoutTests, maybeIt, publishPackage, publishTests, registerAllTests, searchTests, settingsTests, setupVerdaccioTasks, signinTests, unpublishPackage };
|
|
141
|
+
export { DEFAULT_FEATURES, DEFAULT_SELECTORS, DEFAULT_TEST_IDS, changePasswordTests, cleanupPublished, createRegistryConfig, homeTests, layoutTests, maybeIt, publishPackage, publishTests, registerAllTests, searchTests, settingsTests, setupVerdaccioTasks, signinTests, unpublishPackage };
|
|
115
142
|
|
|
116
143
|
//# sourceMappingURL=index.js.map
|
package/build/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import {\n cleanupPublished,\n publishPackage,\n PublishPackageResult,\n PublishPackageTaskInput,\n unpublishPackage,\n UnpublishPackageInput,\n UnpublishPackageResult,\n} from './tasks';\nimport { DEFAULT_FEATURES, mergeFeatures } from './features';\nimport {\n DEFAULT_SELECTORS,\n DEFAULT_TEST_IDS,\n mergeSelectors,\n mergeTestIds,\n} from './testIds';\nimport { RegistryConfig, RegistryTaskResult, VerdaccioUiOptions } from './types';\n\nexport type {\n RegistryConfig,\n RegistryTaskResult,\n VerdaccioUiOptions,\n} from './types';\nexport type {\n PublishPackageInput,\n PublishPackageTaskInput,\n PublishPackageResult,\n UnpublishPackageInput,\n UnpublishPackageResult,\n} from './tasks';\nexport type { DeepPartial, Selectors, TestIds } from './testIds';\nexport type { Features } from './features';\nexport { DEFAULT_SELECTORS, DEFAULT_TEST_IDS } from './testIds';\nexport { DEFAULT_FEATURES, maybeIt } from './features';\nexport { publishPackage, cleanupPublished, unpublishPackage } from './tasks';\nexport {\n homeTests,\n signinTests,\n publishTests,\n searchTests,\n settingsTests,\n layoutTests,\n} from './tests';\n\n/**\n * Build a full RegistryConfig from user-provided options with defaults.\n *\n * `testIds` and `selectors` are deep-merged per section with the\n * defaults in `./testIds`. Consumers targeting a non-default Verdaccio\n * build can override just the fields that drifted:\n *\n * createRegistryConfig({\n * registryUrl: 'http://localhost:4873',\n * testIds: {\n * header: { settingsTooltip: 'my-new-settings-btn' },\n * },\n * });\n */\nexport function createRegistryConfig(options: VerdaccioUiOptions): RegistryConfig {\n const url = new URL(options.registryUrl);\n return {\n registryUrl: options.registryUrl,\n port: options.port ?? (parseInt(url.port, 10) || 4873),\n credentials: options.credentials ?? { user: 'test', password: 'test' },\n title: options.title ?? 'Verdaccio',\n testIds: mergeTestIds(DEFAULT_TEST_IDS, options.testIds),\n selectors: mergeSelectors(DEFAULT_SELECTORS, options.selectors),\n features: mergeFeatures(DEFAULT_FEATURES, options.features),\n };\n}\n\n/**\n * Register Verdaccio Cypress tasks in setupNodeEvents.\n *\n * Usage in cypress.config.ts:\n * import { setupVerdaccioTasks } from '@verdaccio/e2e-ui';\n *\n * export default defineConfig({\n * e2e: {\n * setupNodeEvents(on) {\n * setupVerdaccioTasks(on, { registryUrl: 'http://localhost:4873' });\n * },\n * },\n * });\n */\nexport function setupVerdaccioTasks(\n on: Cypress.PluginEvents,\n options: VerdaccioUiOptions\n): void {\n const config = createRegistryConfig(options);\n\n on('task', {\n registry(): RegistryTaskResult {\n return {\n registryUrl: config.registryUrl,\n port: config.port,\n };\n },\n /**\n * Publish a throwaway package to the registry.\n *\n * Usage from a Cypress spec:\n *\n * cy.task('publishPackage', { pkgName: '@verdaccio/pkg-scoped' });\n *\n * Any field not provided falls back to the values configured here\n * (registryUrl, credentials). The task returns a PublishPackageResult.\n */\n async publishPackage(\n input: PublishPackageTaskInput\n ): Promise<PublishPackageResult> {\n return publishPackage({\n registryUrl: input.registryUrl ?? config.registryUrl,\n credentials: input.credentials ?? config.credentials,\n pkgName: input.pkgName,\n version: input.version,\n dependencies: input.dependencies,\n devDependencies: input.devDependencies,\n unique: input.unique,\n });\n },\n /**\n * Remove a temp project folder returned by a previous publishPackage\n * call. Returns null (Cypress tasks must return something serializable).\n *\n * cy.task('cleanupPublished', result.tempFolder);\n */\n async cleanupPublished(tempFolder: string): Promise<null> {\n await cleanupPublished(tempFolder);\n return null;\n },\n /**\n * Unpublish a package from the registry so the next test starts\n * from a clean slate. Accepts either a package name or a full input\n * object (to reuse a prior tempFolder's token).\n *\n * cy.task('unpublishPackage', '@verdaccio/pkg-scoped');\n * cy.task('unpublishPackage', { pkgName, tempFolder });\n */\n async unpublishPackage(\n input: string | (Omit<UnpublishPackageInput, 'registryUrl'> & {\n registryUrl?: string;\n })\n ): Promise<UnpublishPackageResult> {\n const normalized =\n typeof input === 'string'\n ? { pkgName: input, registryUrl: config.registryUrl }\n : { ...input, registryUrl: input.registryUrl ?? config.registryUrl };\n return unpublishPackage(normalized);\n },\n });\n}\n\n/**\n * Register all Verdaccio UI tests.\n *\n * Usage in a spec file:\n * import { registerAllTests, createRegistryConfig } from '@verdaccio/e2e-ui';\n *\n * const config = createRegistryConfig({ registryUrl: 'http://localhost:4873' });\n * registerAllTests(config);\n *\n * Or pick individual suites:\n * import { homeTests, signinTests } from '@verdaccio/e2e-ui';\n *\n * const config = createRegistryConfig({ registryUrl: 'http://localhost:4873' });\n * homeTests(config);\n * signinTests(config);\n */\nexport function registerAllTests(config: RegistryConfig): void {\n homeTests(config);\n signinTests(config);\n layoutTests(config);\n searchTests(config);\n settingsTests(config);\n publishTests(config);\n}\n\n// Re-export for convenience\nimport {\n homeTests,\n signinTests,\n publishTests,\n searchTests,\n settingsTests,\n layoutTests,\n} from './tests';\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import { appendFileSync } from 'fs';\n\nimport {\n cleanupPublished,\n publishPackage,\n PublishPackageResult,\n PublishPackageTaskInput,\n unpublishPackage,\n UnpublishPackageInput,\n UnpublishPackageResult,\n} from './tasks';\nimport { DEFAULT_FEATURES, mergeFeatures } from './features';\nimport {\n DEFAULT_SELECTORS,\n DEFAULT_TEST_IDS,\n mergeSelectors,\n mergeTestIds,\n} from './testIds';\nimport { RegistryConfig, RegistryTaskResult, VerdaccioUiOptions } from './types';\n\nexport type {\n RegistryConfig,\n RegistryTaskResult,\n VerdaccioUiOptions,\n} from './types';\nexport type {\n PublishPackageInput,\n PublishPackageTaskInput,\n PublishPackageResult,\n UnpublishPackageInput,\n UnpublishPackageResult,\n} from './tasks';\nexport type { DeepPartial, Selectors, TestIds } from './testIds';\nexport type { Features } from './features';\nexport { DEFAULT_SELECTORS, DEFAULT_TEST_IDS } from './testIds';\nexport { DEFAULT_FEATURES, maybeIt } from './features';\nexport { publishPackage, cleanupPublished, unpublishPackage } from './tasks';\nexport {\n homeTests,\n signinTests,\n publishTests,\n searchTests,\n settingsTests,\n layoutTests,\n changePasswordTests,\n} from './tests';\n\n/**\n * Build a full RegistryConfig from user-provided options with defaults.\n *\n * `testIds` and `selectors` are deep-merged per section with the\n * defaults in `./testIds`. Consumers targeting a non-default Verdaccio\n * build can override just the fields that drifted:\n *\n * createRegistryConfig({\n * registryUrl: 'http://localhost:4873',\n * testIds: {\n * header: { settingsTooltip: 'my-new-settings-btn' },\n * },\n * });\n */\nexport function createRegistryConfig(options: VerdaccioUiOptions): RegistryConfig {\n const url = new URL(options.registryUrl);\n return {\n registryUrl: options.registryUrl,\n port: options.port ?? (parseInt(url.port, 10) || 4873),\n credentials: options.credentials ?? { user: 'test', password: 'test' },\n title: options.title ?? 'Verdaccio',\n testIds: mergeTestIds(DEFAULT_TEST_IDS, options.testIds),\n selectors: mergeSelectors(DEFAULT_SELECTORS, options.selectors),\n features: mergeFeatures(DEFAULT_FEATURES, options.features),\n };\n}\n\n/**\n * Register Verdaccio Cypress tasks in setupNodeEvents.\n *\n * Usage in cypress.config.ts:\n * import { setupVerdaccioTasks } from '@verdaccio/e2e-ui';\n *\n * export default defineConfig({\n * e2e: {\n * setupNodeEvents(on) {\n * setupVerdaccioTasks(on, { registryUrl: 'http://localhost:4873' });\n * },\n * },\n * });\n */\nexport function setupVerdaccioTasks(\n on: Cypress.PluginEvents,\n options: VerdaccioUiOptions\n): void {\n const config = createRegistryConfig(options);\n\n // GitHub Actions Step Summary\n on('after:run', (results) => {\n const summaryFile = process.env.GITHUB_STEP_SUMMARY;\n if (!summaryFile || !results || !('runs' in results)) return;\n\n const cypressResults = results as CypressCommandLine.CypressRunResult;\n const lines: string[] = [];\n\n lines.push('## UI E2E Test Results\\n');\n lines.push('| Spec | Tests | Passing | Failing | Pending | Duration |');\n lines.push('|------|-------|---------|---------|---------|----------|');\n\n for (const run of cypressResults.runs) {\n const specName = run.spec.relative || run.spec.name;\n const stats = run.stats;\n const icon = stats.failures > 0 ? '❌' : '✅';\n const dur = stats.duration ? `${(stats.duration / 1000).toFixed(1)}s` : '-';\n lines.push(\n `| ${icon} ${specName} | ${stats.tests} | ${stats.passes} | ${stats.failures} | ${stats.pending} | ${dur} |`\n );\n }\n\n lines.push('');\n const totals = cypressResults.totalTests ?? 0;\n const passed = cypressResults.totalPassed ?? 0;\n const failed = cypressResults.totalFailed ?? 0;\n const pending = cypressResults.totalPending ?? 0;\n const emoji = failed > 0 ? '❌' : '✅';\n lines.push(\n `${emoji} **${passed} passed**, **${failed} failed**, **${pending} pending** (${totals} total)\\n`\n );\n\n appendFileSync(summaryFile, lines.join('\\n'));\n });\n\n on('task', {\n registry(): RegistryTaskResult {\n return {\n registryUrl: config.registryUrl,\n port: config.port,\n };\n },\n /**\n * Publish a throwaway package to the registry.\n *\n * Usage from a Cypress spec:\n *\n * cy.task('publishPackage', { pkgName: '@verdaccio/pkg-scoped' });\n *\n * Any field not provided falls back to the values configured here\n * (registryUrl, credentials). The task returns a PublishPackageResult.\n */\n async publishPackage(\n input: PublishPackageTaskInput\n ): Promise<PublishPackageResult> {\n return publishPackage({\n registryUrl: input.registryUrl ?? config.registryUrl,\n credentials: input.credentials ?? config.credentials,\n pkgName: input.pkgName,\n version: input.version,\n dependencies: input.dependencies,\n devDependencies: input.devDependencies,\n unique: input.unique,\n });\n },\n /**\n * Remove a temp project folder returned by a previous publishPackage\n * call. Returns null (Cypress tasks must return something serializable).\n *\n * cy.task('cleanupPublished', result.tempFolder);\n */\n async cleanupPublished(tempFolder: string): Promise<null> {\n await cleanupPublished(tempFolder);\n return null;\n },\n /**\n * Unpublish a package from the registry so the next test starts\n * from a clean slate. Accepts either a package name or a full input\n * object (to reuse a prior tempFolder's token).\n *\n * cy.task('unpublishPackage', '@verdaccio/pkg-scoped');\n * cy.task('unpublishPackage', { pkgName, tempFolder });\n */\n async unpublishPackage(\n input: string | (Omit<UnpublishPackageInput, 'registryUrl'> & {\n registryUrl?: string;\n })\n ): Promise<UnpublishPackageResult> {\n const normalized =\n typeof input === 'string'\n ? { pkgName: input, registryUrl: config.registryUrl }\n : { ...input, registryUrl: input.registryUrl ?? config.registryUrl };\n return unpublishPackage(normalized);\n },\n });\n}\n\n/**\n * Register all Verdaccio UI tests.\n *\n * Usage in a spec file:\n * import { registerAllTests, createRegistryConfig } from '@verdaccio/e2e-ui';\n *\n * const config = createRegistryConfig({ registryUrl: 'http://localhost:4873' });\n * registerAllTests(config);\n *\n * Or pick individual suites:\n * import { homeTests, signinTests } from '@verdaccio/e2e-ui';\n *\n * const config = createRegistryConfig({ registryUrl: 'http://localhost:4873' });\n * homeTests(config);\n * signinTests(config);\n */\nexport function registerAllTests(config: RegistryConfig): void {\n homeTests(config);\n signinTests(config);\n layoutTests(config);\n searchTests(config);\n settingsTests(config);\n publishTests(config);\n changePasswordTests(config);\n}\n\n// Re-export for convenience\nimport {\n homeTests,\n signinTests,\n publishTests,\n searchTests,\n settingsTests,\n layoutTests,\n changePasswordTests,\n} from './tests';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,SAAgB,qBAAqB,SAA6C;CAChF,MAAM,MAAM,IAAI,IAAI,QAAQ,YAAY;AACxC,QAAO;EACL,aAAa,QAAQ;EACrB,MAAM,QAAQ,SAAS,SAAS,IAAI,MAAM,GAAG,IAAI;EACjD,aAAa,QAAQ,eAAe;GAAE,MAAM;GAAQ,UAAU;GAAQ;EACtE,OAAO,QAAQ,SAAS;EACxB,SAAS,aAAa,kBAAkB,QAAQ,QAAQ;EACxD,WAAW,eAAe,mBAAmB,QAAQ,UAAU;EAC/D,UAAU,cAAc,kBAAkB,QAAQ,SAAS;EAC5D;;;;;;;;;;;;;;;;AAiBH,SAAgB,oBACd,IACA,SACM;CACN,MAAM,SAAS,qBAAqB,QAAQ;AAG5C,IAAG,cAAc,YAAY;EAC3B,MAAM,cAAc,QAAQ,IAAI;AAChC,MAAI,CAAC,eAAe,CAAC,WAAW,EAAE,UAAU,SAAU;EAEtD,MAAM,iBAAiB;EACvB,MAAM,QAAkB,EAAE;AAE1B,QAAM,KAAK,2BAA2B;AACtC,QAAM,KAAK,4DAA4D;AACvE,QAAM,KAAK,4DAA4D;AAEvE,OAAK,MAAM,OAAO,eAAe,MAAM;GACrC,MAAM,WAAW,IAAI,KAAK,YAAY,IAAI,KAAK;GAC/C,MAAM,QAAQ,IAAI;GAClB,MAAM,OAAO,MAAM,WAAW,IAAI,MAAM;GACxC,MAAM,MAAM,MAAM,WAAW,IAAI,MAAM,WAAW,KAAM,QAAQ,EAAE,CAAC,KAAK;AACxE,SAAM,KACJ,KAAK,KAAK,GAAG,SAAS,KAAK,MAAM,MAAM,KAAK,MAAM,OAAO,KAAK,MAAM,SAAS,KAAK,MAAM,QAAQ,KAAK,IAAI,IAC1G;;AAGH,QAAM,KAAK,GAAG;EACd,MAAM,SAAS,eAAe,cAAc;EAC5C,MAAM,SAAS,eAAe,eAAe;EAC7C,MAAM,SAAS,eAAe,eAAe;EAC7C,MAAM,UAAU,eAAe,gBAAgB;EAC/C,MAAM,QAAQ,SAAS,IAAI,MAAM;AACjC,QAAM,KACJ,GAAG,MAAM,KAAK,OAAO,eAAe,OAAO,eAAe,QAAQ,cAAc,OAAO,WACxF;AAED,iBAAe,aAAa,MAAM,KAAK,KAAK,CAAC;GAC7C;AAEF,IAAG,QAAQ;EACT,WAA+B;AAC7B,UAAO;IACL,aAAa,OAAO;IACpB,MAAM,OAAO;IACd;;EAYH,MAAM,eACJ,OAC+B;AAC/B,UAAO,eAAe;IACpB,aAAa,MAAM,eAAe,OAAO;IACzC,aAAa,MAAM,eAAe,OAAO;IACzC,SAAS,MAAM;IACf,SAAS,MAAM;IACf,cAAc,MAAM;IACpB,iBAAiB,MAAM;IACvB,QAAQ,MAAM;IACf,CAAC;;EAQJ,MAAM,iBAAiB,YAAmC;AACxD,SAAM,iBAAiB,WAAW;AAClC,UAAO;;EAUT,MAAM,iBACJ,OAGiC;AAKjC,UAAO,iBAHL,OAAO,UAAU,WACb;IAAE,SAAS;IAAO,aAAa,OAAO;IAAa,GACnD;IAAE,GAAG;IAAO,aAAa,MAAM,eAAe,OAAO;IAAa,CACrC;;EAEtC,CAAC;;;;;;;;;;;;;;;;;;AAmBJ,SAAgB,iBAAiB,QAA8B;AAC7D,WAAU,OAAO;AACjB,aAAY,OAAO;AACnB,aAAY,OAAO;AACnB,aAAY,OAAO;AACnB,eAAc,OAAO;AACrB,cAAa,OAAO;AACpB,qBAAoB,OAAO"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { maybeIt } from "../features.js";
|
|
2
|
+
//#region src/tests/change-password.ts
|
|
3
|
+
/**
|
|
4
|
+
* Tests for the Change Password page at /-/web/change-password.
|
|
5
|
+
*
|
|
6
|
+
* The page renders only when the server is started with
|
|
7
|
+
* `flags.changePassword: true` (otherwise the React component
|
|
8
|
+
* redirects to `/` on mount). Each test logs in first, navigates
|
|
9
|
+
* directly to the page, and drives the form.
|
|
10
|
+
*
|
|
11
|
+
* Selector strategy: the ChangePassword form does not ship stable
|
|
12
|
+
* `id`/testid attributes on its inputs, but every field is registered
|
|
13
|
+
* via react-hook-form's `register('<name>')`, which sets a stable
|
|
14
|
+
* `name` attribute on the underlying `<input>`. The labels themselves
|
|
15
|
+
* are `t('security.changePassword.*')` calls — when the i18n bundle
|
|
16
|
+
* hasn't finished loading (or isn't loaded at all on this route), MUI
|
|
17
|
+
* renders the literal i18n key as the label, so any selector that
|
|
18
|
+
* matches on visible label text silently misses every field and the
|
|
19
|
+
* form stays empty (which would also make the mismatch test "pass"
|
|
20
|
+
* for the wrong reason: the submit button is disabled because the
|
|
21
|
+
* form is empty, not because yup rejected the mismatch).
|
|
22
|
+
*
|
|
23
|
+
* The stable, i18n-independent contract from ChangePassword.tsx is:
|
|
24
|
+
* register('username') → input[name="username"]
|
|
25
|
+
* register('oldPassword') → input[name="oldPassword"]
|
|
26
|
+
* register('newPassword') → input[name="newPassword"]
|
|
27
|
+
* register('confirmPassword') → input[name="confirmPassword"]
|
|
28
|
+
* submit button → form button[type="submit"]
|
|
29
|
+
*/
|
|
30
|
+
function changePasswordTests(config) {
|
|
31
|
+
const { header, login } = config.testIds;
|
|
32
|
+
const { loginDialog } = config.selectors;
|
|
33
|
+
const { features } = config;
|
|
34
|
+
const GENERIC_FAILURE_TEXT = "Failed to change password";
|
|
35
|
+
describe("change password", () => {
|
|
36
|
+
const CHANGE_PASSWORD_PATH = "/-/web/change-password";
|
|
37
|
+
const { user, password } = config.credentials;
|
|
38
|
+
/**
|
|
39
|
+
* Tests mutate the user's password. We track the "current" value
|
|
40
|
+
* across tests so the `after()` hook can restore the original,
|
|
41
|
+
* leaving the registry in the same state other suites assume.
|
|
42
|
+
*/
|
|
43
|
+
let currentPassword = password;
|
|
44
|
+
/**
|
|
45
|
+
* Capability check. The ChangePassword page's `useEffect` redirects
|
|
46
|
+
* to `/` whenever `configuration.flags.changePassword` is not truthy
|
|
47
|
+
* — which is the case on any registry that either (a) didn't set
|
|
48
|
+
* `flags.changePassword: true` in its config, or (b) runs a
|
|
49
|
+
* verdaccio build whose middleware doesn't yet propagate the flag
|
|
50
|
+
* into `__VERDACCIO_BASENAME_UI_OPTIONS` (older `verdaccio:6`
|
|
51
|
+
* tagged images fall in this bucket).
|
|
52
|
+
*
|
|
53
|
+
* In either case the suite has nothing to exercise, so skip the
|
|
54
|
+
* whole describe with a clear reason rather than burning five
|
|
55
|
+
* seconds per test on cy.contains timeouts.
|
|
56
|
+
*/
|
|
57
|
+
before(function() {
|
|
58
|
+
cy.visit(config.registryUrl);
|
|
59
|
+
cy.window().then((win) => {
|
|
60
|
+
const opts = win.__VERDACCIO_BASENAME_UI_OPTIONS;
|
|
61
|
+
if (!!!opts?.flags?.changePassword) {
|
|
62
|
+
console.warn("[change-password] server did not advertise flags.changePassword=true — skipping suite. ui-options.flags: " + JSON.stringify(opts?.flags ?? {}));
|
|
63
|
+
this.skip();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
cy.intercept("POST", "/-/verdaccio/sec/login").as("signChangePwd");
|
|
69
|
+
cy.visit(config.registryUrl);
|
|
70
|
+
cy.login(user, currentPassword, {
|
|
71
|
+
loginButton: header.loginButton,
|
|
72
|
+
...loginDialog
|
|
73
|
+
});
|
|
74
|
+
cy.wait("@signChangePwd").its("response.statusCode").should("eq", 200);
|
|
75
|
+
cy.visit(CHANGE_PASSWORD_PATH);
|
|
76
|
+
cy.get("form button[type=\"submit\"]", { timeout: 5e3 }).should("be.visible");
|
|
77
|
+
});
|
|
78
|
+
after(() => {
|
|
79
|
+
if (currentPassword === password) return;
|
|
80
|
+
cy.intercept("POST", "/-/verdaccio/sec/login").as("signChangePwdRestore");
|
|
81
|
+
cy.visit(config.registryUrl);
|
|
82
|
+
cy.login(user, currentPassword, {
|
|
83
|
+
loginButton: header.loginButton,
|
|
84
|
+
...loginDialog
|
|
85
|
+
});
|
|
86
|
+
cy.wait("@signChangePwdRestore").its("response.statusCode").should("eq", 200);
|
|
87
|
+
cy.visit(CHANGE_PASSWORD_PATH);
|
|
88
|
+
cy.get("input[name=\"username\"]").type(user);
|
|
89
|
+
cy.get("input[name=\"oldPassword\"]").type(currentPassword);
|
|
90
|
+
cy.get("input[name=\"newPassword\"]").type(password);
|
|
91
|
+
cy.get("input[name=\"confirmPassword\"]").type(password);
|
|
92
|
+
cy.get("form button[type=\"submit\"]").click();
|
|
93
|
+
currentPassword = password;
|
|
94
|
+
});
|
|
95
|
+
maybeIt(features.changePassword.validation)("should disable the submit button while the form is empty", () => {
|
|
96
|
+
cy.get("form button[type=\"submit\"]").should("be.disabled");
|
|
97
|
+
});
|
|
98
|
+
maybeIt(features.changePassword.validation)("should keep submit disabled when new and confirm passwords mismatch", () => {
|
|
99
|
+
cy.get("input[name=\"username\"]").type(user);
|
|
100
|
+
cy.get("input[name=\"oldPassword\"]").type(currentPassword);
|
|
101
|
+
cy.get("input[name=\"newPassword\"]").type("newSecretPass123");
|
|
102
|
+
cy.get("input[name=\"confirmPassword\"]").type("different-value");
|
|
103
|
+
cy.get("form button[type=\"submit\"]").should("be.disabled");
|
|
104
|
+
});
|
|
105
|
+
maybeIt(features.changePassword.wrongOldPassword)("should show an error banner when the old password is wrong", () => {
|
|
106
|
+
cy.intercept("PUT", "/-/verdaccio/sec/reset_password").as("reset");
|
|
107
|
+
cy.get("input[name=\"username\"]").type(user);
|
|
108
|
+
cy.get("input[name=\"oldPassword\"]").type("definitely-wrong-xyz");
|
|
109
|
+
cy.get("input[name=\"newPassword\"]").type("newSecretPass123");
|
|
110
|
+
cy.get("input[name=\"confirmPassword\"]").type("newSecretPass123");
|
|
111
|
+
cy.get("form button[type=\"submit\"]").should("not.be.disabled").click();
|
|
112
|
+
cy.wait("@reset").its("response.statusCode").should("not.eq", 200);
|
|
113
|
+
cy.getByTestId(login.error, { timeout: 5e3 }).should("be.visible").and("contain.text", GENERIC_FAILURE_TEXT);
|
|
114
|
+
cy.location("pathname").should("include", CHANGE_PASSWORD_PATH);
|
|
115
|
+
});
|
|
116
|
+
maybeIt(features.changePassword.happyPath)("should change the password and navigate to the success page", () => {
|
|
117
|
+
const newPassword = `${currentPassword}-rotated`;
|
|
118
|
+
cy.intercept("PUT", "/-/verdaccio/sec/reset_password").as("reset");
|
|
119
|
+
cy.get("input[name=\"username\"]").type(user);
|
|
120
|
+
cy.get("input[name=\"oldPassword\"]").type(currentPassword);
|
|
121
|
+
cy.get("input[name=\"newPassword\"]").type(newPassword);
|
|
122
|
+
cy.get("input[name=\"confirmPassword\"]").type(newPassword);
|
|
123
|
+
cy.get("form button[type=\"submit\"]").should("not.be.disabled").click();
|
|
124
|
+
cy.wait("@reset").its("response.statusCode").should("eq", 200);
|
|
125
|
+
cy.location("pathname", { timeout: 5e3 }).should("include", "/-/web/success");
|
|
126
|
+
currentPassword = newPassword;
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
export { changePasswordTests };
|
|
132
|
+
|
|
133
|
+
//# sourceMappingURL=change-password.js.map
|
|
@@ -0,0 +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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"}
|
package/build/features.d.ts
CHANGED
|
@@ -76,6 +76,41 @@ export interface Features {
|
|
|
76
76
|
*/
|
|
77
77
|
rawViewer: boolean;
|
|
78
78
|
};
|
|
79
|
+
changePassword: {
|
|
80
|
+
/**
|
|
81
|
+
* Whether to run the happy-path test (submit valid change,
|
|
82
|
+
* expect navigation to the success page, then restore the
|
|
83
|
+
* original password in `after()`).
|
|
84
|
+
*
|
|
85
|
+
* The suite targets /-/web/change-password, which renders only
|
|
86
|
+
* when the server is configured with `flags.changePassword: true`.
|
|
87
|
+
* Disable on registries that do not enable the flag.
|
|
88
|
+
*
|
|
89
|
+
* Also disable on **published verdaccio 6.x** (all lines through
|
|
90
|
+
* 6.5.0): the reset_password handler in
|
|
91
|
+
* `verdaccio/build/api/web/api/user.js` ships with an inverted
|
|
92
|
+
* conditional — `validatePassword(...) === false` gates the
|
|
93
|
+
* `auth.changePassword(...)` call, so a *valid* new password
|
|
94
|
+
* always returns HTTP 400 (`PASSWORD_VALIDATION`). The bug is
|
|
95
|
+
* fixed on the development branch but has not been released
|
|
96
|
+
* in any 6.x tag, so the happy path cannot succeed against an
|
|
97
|
+
* `npm install verdaccio@6` runtime.
|
|
98
|
+
*/
|
|
99
|
+
happyPath: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Whether to run the client-side validation tests (submit button
|
|
102
|
+
* stays disabled while fields are empty / mismatched confirm).
|
|
103
|
+
* Depends on the yup `changePasswordSchema`.
|
|
104
|
+
*/
|
|
105
|
+
validation: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Whether to run the "wrong old password shows error banner" test.
|
|
108
|
+
* Depends on the server rejecting the call and the onSubmit catch
|
|
109
|
+
* block surfacing `"Failed to change password"` via
|
|
110
|
+
* `LoginDialogFormError`.
|
|
111
|
+
*/
|
|
112
|
+
wrongOldPassword: boolean;
|
|
113
|
+
};
|
|
79
114
|
}
|
|
80
115
|
/** Defaults: all flags on. */
|
|
81
116
|
export declare const DEFAULT_FEATURES: Features;
|
package/build/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type { Features } from './features';
|
|
|
7
7
|
export { DEFAULT_SELECTORS, DEFAULT_TEST_IDS } from './testIds';
|
|
8
8
|
export { DEFAULT_FEATURES, maybeIt } from './features';
|
|
9
9
|
export { publishPackage, cleanupPublished, unpublishPackage } from './tasks';
|
|
10
|
-
export { homeTests, signinTests, publishTests, searchTests, settingsTests, layoutTests, } from './tests';
|
|
10
|
+
export { homeTests, signinTests, publishTests, searchTests, settingsTests, layoutTests, changePasswordTests, } from './tests';
|
|
11
11
|
/**
|
|
12
12
|
* Build a full RegistryConfig from user-provided options with defaults.
|
|
13
13
|
*
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { RegistryConfig } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Tests for the Change Password page at /-/web/change-password.
|
|
4
|
+
*
|
|
5
|
+
* The page renders only when the server is started with
|
|
6
|
+
* `flags.changePassword: true` (otherwise the React component
|
|
7
|
+
* redirects to `/` on mount). Each test logs in first, navigates
|
|
8
|
+
* directly to the page, and drives the form.
|
|
9
|
+
*
|
|
10
|
+
* Selector strategy: the ChangePassword form does not ship stable
|
|
11
|
+
* `id`/testid attributes on its inputs, but every field is registered
|
|
12
|
+
* via react-hook-form's `register('<name>')`, which sets a stable
|
|
13
|
+
* `name` attribute on the underlying `<input>`. The labels themselves
|
|
14
|
+
* are `t('security.changePassword.*')` calls — when the i18n bundle
|
|
15
|
+
* hasn't finished loading (or isn't loaded at all on this route), MUI
|
|
16
|
+
* renders the literal i18n key as the label, so any selector that
|
|
17
|
+
* matches on visible label text silently misses every field and the
|
|
18
|
+
* form stays empty (which would also make the mismatch test "pass"
|
|
19
|
+
* for the wrong reason: the submit button is disabled because the
|
|
20
|
+
* form is empty, not because yup rejected the mismatch).
|
|
21
|
+
*
|
|
22
|
+
* The stable, i18n-independent contract from ChangePassword.tsx is:
|
|
23
|
+
* register('username') → input[name="username"]
|
|
24
|
+
* register('oldPassword') → input[name="oldPassword"]
|
|
25
|
+
* register('newPassword') → input[name="newPassword"]
|
|
26
|
+
* register('confirmPassword') → input[name="confirmPassword"]
|
|
27
|
+
* submit button → form button[type="submit"]
|
|
28
|
+
*/
|
|
29
|
+
export declare function changePasswordTests(config: RegistryConfig): void;
|
package/build/tests/index.d.ts
CHANGED