@verdaccio/e2e-ui 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/build/cjs/commands/index.cjs +7 -0
  2. package/build/cjs/commands/index.cjs.map +1 -1
  3. package/build/cjs/features.cjs +73 -0
  4. package/build/cjs/features.cjs.map +1 -0
  5. package/build/cjs/index.cjs +8 -1
  6. package/build/cjs/index.cjs.map +1 -1
  7. package/build/cjs/testIds.cjs +16 -1
  8. package/build/cjs/testIds.cjs.map +1 -1
  9. package/build/cjs/tests/change-password.cjs +133 -0
  10. package/build/cjs/tests/change-password.cjs.map +1 -0
  11. package/build/cjs/tests/home.cjs +3 -1
  12. package/build/cjs/tests/home.cjs.map +1 -1
  13. package/build/cjs/tests/layout.cjs +10 -0
  14. package/build/cjs/tests/layout.cjs.map +1 -1
  15. package/build/cjs/tests/publish.cjs +25 -2
  16. package/build/cjs/tests/publish.cjs.map +1 -1
  17. package/build/cjs/tests/search.cjs +48 -0
  18. package/build/cjs/tests/search.cjs.map +1 -1
  19. package/build/cjs/tests/settings.cjs +3 -1
  20. package/build/cjs/tests/settings.cjs.map +1 -1
  21. package/build/cjs/tests/signin.cjs +25 -3
  22. package/build/cjs/tests/signin.cjs.map +1 -1
  23. package/build/commands/index.d.ts +11 -0
  24. package/build/esm/commands/index.js +7 -0
  25. package/build/esm/commands/index.js.map +1 -1
  26. package/build/esm/features.js +71 -0
  27. package/build/esm/features.js.map +1 -0
  28. package/build/esm/index.js +6 -2
  29. package/build/esm/index.js.map +1 -1
  30. package/build/esm/testIds.js +16 -1
  31. package/build/esm/testIds.js.map +1 -1
  32. package/build/esm/tests/change-password.js +133 -0
  33. package/build/esm/tests/change-password.js.map +1 -0
  34. package/build/esm/tests/home.js +3 -1
  35. package/build/esm/tests/home.js.map +1 -1
  36. package/build/esm/tests/layout.js +10 -0
  37. package/build/esm/tests/layout.js.map +1 -1
  38. package/build/esm/tests/publish.js +25 -2
  39. package/build/esm/tests/publish.js.map +1 -1
  40. package/build/esm/tests/search.js +48 -0
  41. package/build/esm/tests/search.js.map +1 -1
  42. package/build/esm/tests/settings.js +3 -1
  43. package/build/esm/tests/settings.js.map +1 -1
  44. package/build/esm/tests/signin.js +25 -3
  45. package/build/esm/tests/signin.js.map +1 -1
  46. package/build/features.d.ts +129 -0
  47. package/build/index.d.ts +3 -1
  48. package/build/testIds.d.ts +28 -0
  49. package/build/tests/change-password.d.ts +29 -0
  50. package/build/tests/index.d.ts +1 -0
  51. package/build/types.d.ts +8 -0
  52. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"settings.js","names":[],"sources":["../../../src/tests/settings.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\nimport { RegistryConfig } from '../types';\n\n/**\n * Tests for the header Settings dialog and the Translations (language)\n * picker inside it.\n *\n * Requires `web.showSettings: true` in the registry config — the gear\n * icon only renders when the flag is on (see HeaderRight in Verdaccio's\n * ui-components package).\n *\n * Selector strategy: the language cards in LanguageSwitch do NOT have\n * data-testids, so this suite leans on:\n * - MUI's ARIA roles: `[role=\"dialog\"]`, `[role=\"tab\"]`\n * - The stable English labels \"Configuration\", \"Translations\",\n * \"English\", \"German\", and the language description text, which are\n * bundled into the ui-theme via `@verdaccio/ui-i18n`\n * - The settings trigger's testid from Verdaccio's HeaderToolTipIcon:\n * `header--tooltip-settings`\n */\nexport function settingsTests(config: RegistryConfig) {\n const { header } = config.testIds;\n\n describe('settings & language', () => {\n beforeEach(() => {\n cy.visit(config.registryUrl);\n cy.get('body').should('be.visible');\n });\n\n it('should open the settings dialog from the header', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n // Dialog title key: dialog.settings.title → \"Configuration\" in EN.\n cy.contains('[role=\"dialog\"]', 'Configuration').should('be.visible');\n });\n\n it('should show both Package Managers and Translations tabs', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.get('[role=\"dialog\"] [role=\"tab\"]').should('have.length', 2);\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').should(\n 'be.visible'\n );\n });\n\n it('should switch to the Translations tab and list languages', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').click();\n // language.description text is bundled in EN — good sentinel.\n cy.contains(\n '[role=\"dialog\"]',\n /this is the configuration details for the language/i\n ).should('be.visible');\n // Sanity-check a couple of language cards are rendered.\n cy.contains('[role=\"dialog\"]', 'English').should('be.visible');\n cy.contains('[role=\"dialog\"]', 'German').should('be.visible');\n });\n\n it('should change the UI language when a language card is clicked', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').click();\n\n // Before switching, capture the current dialog title so we can\n // assert it changes. This avoids hard-coding the German string.\n cy.contains('[role=\"dialog\"]', 'Configuration')\n .invoke('text')\n .then((englishTitle) => {\n // Click the German card. We scope to the dialog + the MUI\n // card class to avoid matching other \"German\" text.\n cy.contains('[role=\"dialog\"] .MuiCard-root', 'German').click({\n force: true,\n });\n\n // The English tab label \"Translations\" must no longer be\n // present anywhere in the dialog — strongest proof the\n // language actually switched.\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').should(\n 'not.exist'\n );\n // And the original dialog title text should also be gone.\n cy.contains('[role=\"dialog\"]', englishTitle).should('not.exist');\n });\n });\n\n it('should close the settings dialog with Escape', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.get('body').type('{esc}');\n cy.get('[role=\"dialog\"]').should('not.exist');\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqBA,SAAgB,cAAc,QAAwB;CACpD,MAAM,EAAE,WAAW,OAAO;AAE1B,UAAS,6BAA6B;AACpC,mBAAiB;AACf,MAAG,MAAM,OAAO,YAAY;AAC5B,MAAG,IAAI,OAAO,CAAC,OAAO,aAAa;IACnC;AAEF,KAAG,yDAAyD;AAC1D,MAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO;AAC9C,MAAG,IAAI,oBAAkB,CAAC,OAAO,aAAa;AAE9C,MAAG,SAAS,qBAAmB,gBAAgB,CAAC,OAAO,aAAa;IACpE;AAEF,KAAG,iEAAiE;AAClE,MAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO;AAC9C,MAAG,IAAI,oBAAkB,CAAC,OAAO,aAAa;AAC9C,MAAG,IAAI,mCAA+B,CAAC,OAAO,eAAe,EAAE;AAC/D,MAAG,SAAS,oCAAgC,eAAe,CAAC,OAC1D,aACD;IACD;AAEF,KAAG,kEAAkE;AACnE,MAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO;AAC9C,MAAG,IAAI,oBAAkB,CAAC,OAAO,aAAa;AAC9C,MAAG,SAAS,oCAAgC,eAAe,CAAC,OAAO;AAEnE,MAAG,SACD,qBACA,sDACD,CAAC,OAAO,aAAa;AAEtB,MAAG,SAAS,qBAAmB,UAAU,CAAC,OAAO,aAAa;AAC9D,MAAG,SAAS,qBAAmB,SAAS,CAAC,OAAO,aAAa;IAC7D;AAEF,KAAG,uEAAuE;AACxE,MAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO;AAC9C,MAAG,IAAI,oBAAkB,CAAC,OAAO,aAAa;AAC9C,MAAG,SAAS,oCAAgC,eAAe,CAAC,OAAO;AAInE,MAAG,SAAS,qBAAmB,gBAAgB,CAC5C,OAAO,OAAO,CACd,MAAM,iBAAiB;AAGtB,OAAG,SAAS,mCAAiC,SAAS,CAAC,MAAM,EAC3D,OAAO,MACR,CAAC;AAKF,OAAG,SAAS,oCAAgC,eAAe,CAAC,OAC1D,YACD;AAED,OAAG,SAAS,qBAAmB,aAAa,CAAC,OAAO,YAAY;KAChE;IACJ;AAEF,KAAG,sDAAsD;AACvD,MAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO;AAC9C,MAAG,IAAI,oBAAkB,CAAC,OAAO,aAAa;AAC9C,MAAG,IAAI,OAAO,CAAC,KAAK,QAAQ;AAC5B,MAAG,IAAI,oBAAkB,CAAC,OAAO,YAAY;IAC7C;GACF"}
1
+ {"version":3,"file":"settings.js","names":[],"sources":["../../../src/tests/settings.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Tests for the header Settings dialog and the Translations (language)\n * picker inside it.\n *\n * Requires `web.showSettings: true` in the registry config — the gear\n * icon only renders when the flag is on (see HeaderRight in Verdaccio's\n * ui-components package).\n *\n * Selector strategy: the language cards in LanguageSwitch do NOT have\n * data-testids, so this suite leans on:\n * - MUI's ARIA roles: `[role=\"dialog\"]`, `[role=\"tab\"]`\n * - The stable English labels \"Configuration\", \"Translations\",\n * \"English\", \"German\", and the language description text, which are\n * bundled into the ui-theme via `@verdaccio/ui-i18n`\n * - The settings trigger's testid from Verdaccio's HeaderToolTipIcon:\n * `header--tooltip-settings`\n */\nexport function settingsTests(config: RegistryConfig) {\n const { header } = config.testIds;\n const { features } = config;\n\n describe('settings & language', () => {\n beforeEach(() => {\n cy.visit(config.registryUrl);\n cy.get('body').should('be.visible');\n });\n\n it('should open the settings dialog from the header', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n // Dialog title key: dialog.settings.title → \"Configuration\" in EN.\n cy.contains('[role=\"dialog\"]', 'Configuration').should('be.visible');\n });\n\n it('should show both Package Managers and Translations tabs', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.get('[role=\"dialog\"] [role=\"tab\"]').should('have.length', 2);\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').should(\n 'be.visible'\n );\n });\n\n it('should switch to the Translations tab and list languages', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').click();\n // language.description text is bundled in EN — good sentinel.\n cy.contains(\n '[role=\"dialog\"]',\n /this is the configuration details for the language/i\n ).should('be.visible');\n // Sanity-check a couple of language cards are rendered.\n cy.contains('[role=\"dialog\"]', 'English').should('be.visible');\n cy.contains('[role=\"dialog\"]', 'German').should('be.visible');\n });\n\n maybeIt(features.settings.languageSwitcher)(\n 'should change the UI language when a language card is clicked',\n () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').click();\n\n // Before switching, capture the current dialog title so we can\n // assert it changes. This avoids hard-coding the German string.\n cy.contains('[role=\"dialog\"]', 'Configuration')\n .invoke('text')\n .then((englishTitle) => {\n // Click the German card. We scope to the dialog + the MUI\n // card class to avoid matching other \"German\" text.\n cy.contains('[role=\"dialog\"] .MuiCard-root', 'German').click({\n force: true,\n });\n\n // The English tab label \"Translations\" must no longer be\n // present anywhere in the dialog — strongest proof the\n // language actually switched.\n cy.contains('[role=\"dialog\"] [role=\"tab\"]', 'Translations').should(\n 'not.exist'\n );\n // And the original dialog title text should also be gone.\n cy.contains('[role=\"dialog\"]', englishTitle).should('not.exist');\n });\n }\n );\n\n it('should close the settings dialog with Escape', () => {\n cy.getByTestId(header.settingsTooltip).click();\n cy.get('[role=\"dialog\"]').should('be.visible');\n cy.get('body').type('{esc}');\n cy.get('[role=\"dialog\"]').should('not.exist');\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,cAAc,QAAwB;CACpD,MAAM,EAAE,WAAW,OAAO;CAC1B,MAAM,EAAE,aAAa;AAErB,UAAS,6BAA6B;AACpC,mBAAiB;AACf,MAAG,MAAM,OAAO,YAAY;AAC5B,MAAG,IAAI,OAAO,CAAC,OAAO,aAAa;IACnC;AAEF,KAAG,yDAAyD;AAC1D,MAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO;AAC9C,MAAG,IAAI,oBAAkB,CAAC,OAAO,aAAa;AAE9C,MAAG,SAAS,qBAAmB,gBAAgB,CAAC,OAAO,aAAa;IACpE;AAEF,KAAG,iEAAiE;AAClE,MAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO;AAC9C,MAAG,IAAI,oBAAkB,CAAC,OAAO,aAAa;AAC9C,MAAG,IAAI,mCAA+B,CAAC,OAAO,eAAe,EAAE;AAC/D,MAAG,SAAS,oCAAgC,eAAe,CAAC,OAC1D,aACD;IACD;AAEF,KAAG,kEAAkE;AACnE,MAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO;AAC9C,MAAG,IAAI,oBAAkB,CAAC,OAAO,aAAa;AAC9C,MAAG,SAAS,oCAAgC,eAAe,CAAC,OAAO;AAEnE,MAAG,SACD,qBACA,sDACD,CAAC,OAAO,aAAa;AAEtB,MAAG,SAAS,qBAAmB,UAAU,CAAC,OAAO,aAAa;AAC9D,MAAG,SAAS,qBAAmB,SAAS,CAAC,OAAO,aAAa;IAC7D;AAEF,UAAQ,SAAS,SAAS,iBAAiB,CACzC,uEACM;AACN,MAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO;AAC9C,MAAG,IAAI,oBAAkB,CAAC,OAAO,aAAa;AAC9C,MAAG,SAAS,oCAAgC,eAAe,CAAC,OAAO;AAInE,MAAG,SAAS,qBAAmB,gBAAgB,CAC5C,OAAO,OAAO,CACd,MAAM,iBAAiB;AAGtB,OAAG,SAAS,mCAAiC,SAAS,CAAC,MAAM,EAC3D,OAAO,MACR,CAAC;AAKF,OAAG,SAAS,oCAAgC,eAAe,CAAC,OAC1D,YACD;AAED,OAAG,SAAS,qBAAmB,aAAa,CAAC,OAAO,YAAY;KAChE;IAEL;AAED,KAAG,sDAAsD;AACvD,MAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO;AAC9C,MAAG,IAAI,oBAAkB,CAAC,OAAO,aAAa;AAC9C,MAAG,IAAI,OAAO,CAAC,KAAK,QAAQ;AAC5B,MAAG,IAAI,oBAAkB,CAAC,OAAO,YAAY;IAC7C;GACF"}
@@ -1,13 +1,15 @@
1
+ import { maybeIt } from "../features.js";
1
2
  //#region src/tests/signin.ts
2
3
  function signinTests(config) {
3
- const { header } = config.testIds;
4
+ const { header, login } = config.testIds;
4
5
  const { loginDialog } = config.selectors;
6
+ const { features } = config;
5
7
  describe("sign in / sign out", () => {
6
8
  beforeEach(() => {
7
9
  cy.intercept("POST", "/-/verdaccio/sec/login").as("sign");
10
+ cy.visit(config.registryUrl);
8
11
  });
9
12
  it("should login user", () => {
10
- cy.visit(config.registryUrl);
11
13
  cy.login(config.credentials.user, config.credentials.password, {
12
14
  loginButton: header.loginButton,
13
15
  ...loginDialog
@@ -18,7 +20,6 @@ function signinTests(config) {
18
20
  cy.getByTestId(header.greetingsLabel).contains(config.credentials.user);
19
21
  });
20
22
  it("should logout a user", () => {
21
- cy.visit(config.registryUrl);
22
23
  cy.login(config.credentials.user, config.credentials.password, {
23
24
  loginButton: header.loginButton,
24
25
  ...loginDialog
@@ -30,6 +31,27 @@ function signinTests(config) {
30
31
  cy.wait(200);
31
32
  cy.getByTestId(header.loginButton).contains("Login");
32
33
  });
34
+ maybeIt(features.signin.validationTests)("should disable the submit button while the form is empty", () => {
35
+ cy.getByTestId(header.loginButton).click();
36
+ cy.getByTestId(login.dialog).should("be.visible");
37
+ cy.get(loginDialog.submitButton).should("be.disabled");
38
+ });
39
+ maybeIt(features.signin.validationTests)("should keep the submit button disabled when only the username is filled", () => {
40
+ cy.getByTestId(header.loginButton).click();
41
+ cy.getByTestId(login.dialog).should("be.visible");
42
+ cy.get(loginDialog.usernameInput).type(config.credentials.user);
43
+ cy.get(loginDialog.submitButton).should("be.disabled");
44
+ });
45
+ maybeIt(features.signin.validationTests)("should show an error banner on invalid credentials", () => {
46
+ cy.login(config.credentials.user, "definitely-wrong-password-xyz", {
47
+ loginButton: header.loginButton,
48
+ ...loginDialog
49
+ });
50
+ cy.wait("@sign").its("response.statusCode").should("not.eq", 200);
51
+ cy.getByTestId(login.error, { timeout: 5e3 }).should("be.visible");
52
+ cy.getByTestId(login.error).should("contain.text", "Invalid username or password");
53
+ cy.getByTestId(login.dialog).should("be.visible");
54
+ });
33
55
  });
34
56
  }
35
57
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"signin.js","names":[],"sources":["../../../src/tests/signin.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\nimport { RegistryConfig } from '../types';\n\nexport function signinTests(config: RegistryConfig) {\n const { header } = config.testIds;\n const { loginDialog } = config.selectors;\n\n describe('sign in / sign out', () => {\n beforeEach(() => {\n cy.intercept('POST', '/-/verdaccio/sec/login').as('sign');\n });\n\n it('should login user', () => {\n cy.visit(config.registryUrl);\n cy.login(config.credentials.user, config.credentials.password, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@sign');\n cy.getByTestId(header.logInDialogIcon).click();\n cy.wait(100);\n cy.getByTestId(header.greetingsLabel).contains(config.credentials.user);\n });\n\n it('should logout a user', () => {\n cy.visit(config.registryUrl);\n cy.login(config.credentials.user, config.credentials.password, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@sign');\n cy.getByTestId(header.logInDialogIcon).click();\n cy.wait(100);\n cy.getByTestId(header.logOutDialogIcon).click();\n cy.wait(200);\n cy.getByTestId(header.loginButton).contains('Login');\n });\n });\n}\n"],"mappings":";AAIA,SAAgB,YAAY,QAAwB;CAClD,MAAM,EAAE,WAAW,OAAO;CAC1B,MAAM,EAAE,gBAAgB,OAAO;AAE/B,UAAS,4BAA4B;AACnC,mBAAiB;AACf,MAAG,UAAU,QAAQ,yBAAyB,CAAC,GAAG,OAAO;IACzD;AAEF,KAAG,2BAA2B;AAC5B,MAAG,MAAM,OAAO,YAAY;AAC5B,MAAG,MAAM,OAAO,YAAY,MAAM,OAAO,YAAY,UAAU;IAC7D,aAAa,OAAO;IACpB,GAAG;IACJ,CAAC;AACF,MAAG,KAAK,QAAQ;AAChB,MAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO;AAC9C,MAAG,KAAK,IAAI;AACZ,MAAG,YAAY,OAAO,eAAe,CAAC,SAAS,OAAO,YAAY,KAAK;IACvE;AAEF,KAAG,8BAA8B;AAC/B,MAAG,MAAM,OAAO,YAAY;AAC5B,MAAG,MAAM,OAAO,YAAY,MAAM,OAAO,YAAY,UAAU;IAC7D,aAAa,OAAO;IACpB,GAAG;IACJ,CAAC;AACF,MAAG,KAAK,QAAQ;AAChB,MAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO;AAC9C,MAAG,KAAK,IAAI;AACZ,MAAG,YAAY,OAAO,iBAAiB,CAAC,OAAO;AAC/C,MAAG,KAAK,IAAI;AACZ,MAAG,YAAY,OAAO,YAAY,CAAC,SAAS,QAAQ;IACpD;GACF"}
1
+ {"version":3,"file":"signin.js","names":[],"sources":["../../../src/tests/signin.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\n\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\nexport function signinTests(config: RegistryConfig) {\n const { header, login } = config.testIds;\n const { loginDialog } = config.selectors;\n const { features } = config;\n\n describe('sign in / sign out', () => {\n beforeEach(() => {\n cy.intercept('POST', '/-/verdaccio/sec/login').as('sign');\n cy.visit(config.registryUrl);\n });\n\n it('should login user', () => {\n cy.login(config.credentials.user, config.credentials.password, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@sign');\n cy.getByTestId(header.logInDialogIcon).click();\n cy.wait(100);\n cy.getByTestId(header.greetingsLabel).contains(config.credentials.user);\n });\n\n it('should logout a user', () => {\n cy.login(config.credentials.user, config.credentials.password, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@sign');\n cy.getByTestId(header.logInDialogIcon).click();\n cy.wait(100);\n cy.getByTestId(header.logOutDialogIcon).click();\n cy.wait(200);\n cy.getByTestId(header.loginButton).contains('Login');\n });\n\n // ── Validation tests ───────────────────────────────────────────\n // The login form uses a yup schema (username >= 2 chars + URL-safe,\n // password >= 2 chars) and the submit button is `disabled={!isValid}`.\n // Server rejection renders an error banner with `data-testid=\"error\"`\n // inside `LoginDialogFormError`. Gated on\n // `features.signin.validationTests` so builds with a different\n // schema or error string can skip without forking.\n\n maybeIt(features.signin.validationTests)(\n 'should disable the submit button while the form is empty',\n () => {\n cy.getByTestId(header.loginButton).click();\n cy.getByTestId(login.dialog).should('be.visible');\n // Neither field filled → schema invalid → button disabled.\n cy.get(loginDialog.submitButton).should('be.disabled');\n }\n );\n\n maybeIt(features.signin.validationTests)(\n 'should keep the submit button disabled when only the username is filled',\n () => {\n cy.getByTestId(header.loginButton).click();\n cy.getByTestId(login.dialog).should('be.visible');\n cy.get(loginDialog.usernameInput).type(config.credentials.user);\n // Password still empty → schema still invalid.\n cy.get(loginDialog.submitButton).should('be.disabled');\n }\n );\n\n maybeIt(features.signin.validationTests)(\n 'should show an error banner on invalid credentials',\n () => {\n cy.login(config.credentials.user, 'definitely-wrong-password-xyz', {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n // The request goes through (valid schema), the server rejects it.\n cy.wait('@sign').its('response.statusCode').should('not.eq', 200);\n // The form sets errors.root which renders via LoginDialogFormError\n // with a fixed English string (\"Invalid username or password\").\n cy.getByTestId(login.error, { timeout: 5000 }).should('be.visible');\n cy.getByTestId(login.error).should(\n 'contain.text',\n 'Invalid username or password'\n );\n // Dialog should still be open so the user can retry.\n cy.getByTestId(login.dialog).should('be.visible');\n }\n );\n });\n}\n"],"mappings":";;AAKA,SAAgB,YAAY,QAAwB;CAClD,MAAM,EAAE,QAAQ,UAAU,OAAO;CACjC,MAAM,EAAE,gBAAgB,OAAO;CAC/B,MAAM,EAAE,aAAa;AAErB,UAAS,4BAA4B;AACnC,mBAAiB;AACf,MAAG,UAAU,QAAQ,yBAAyB,CAAC,GAAG,OAAO;AACzD,MAAG,MAAM,OAAO,YAAY;IAC5B;AAEF,KAAG,2BAA2B;AAC5B,MAAG,MAAM,OAAO,YAAY,MAAM,OAAO,YAAY,UAAU;IAC7D,aAAa,OAAO;IACpB,GAAG;IACJ,CAAC;AACF,MAAG,KAAK,QAAQ;AAChB,MAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO;AAC9C,MAAG,KAAK,IAAI;AACZ,MAAG,YAAY,OAAO,eAAe,CAAC,SAAS,OAAO,YAAY,KAAK;IACvE;AAEF,KAAG,8BAA8B;AAC/B,MAAG,MAAM,OAAO,YAAY,MAAM,OAAO,YAAY,UAAU;IAC7D,aAAa,OAAO;IACpB,GAAG;IACJ,CAAC;AACF,MAAG,KAAK,QAAQ;AAChB,MAAG,YAAY,OAAO,gBAAgB,CAAC,OAAO;AAC9C,MAAG,KAAK,IAAI;AACZ,MAAG,YAAY,OAAO,iBAAiB,CAAC,OAAO;AAC/C,MAAG,KAAK,IAAI;AACZ,MAAG,YAAY,OAAO,YAAY,CAAC,SAAS,QAAQ;IACpD;AAUF,UAAQ,SAAS,OAAO,gBAAgB,CACtC,kEACM;AACJ,MAAG,YAAY,OAAO,YAAY,CAAC,OAAO;AAC1C,MAAG,YAAY,MAAM,OAAO,CAAC,OAAO,aAAa;AAEjD,MAAG,IAAI,YAAY,aAAa,CAAC,OAAO,cAAc;IAEzD;AAED,UAAQ,SAAS,OAAO,gBAAgB,CACtC,iFACM;AACJ,MAAG,YAAY,OAAO,YAAY,CAAC,OAAO;AAC1C,MAAG,YAAY,MAAM,OAAO,CAAC,OAAO,aAAa;AACjD,MAAG,IAAI,YAAY,cAAc,CAAC,KAAK,OAAO,YAAY,KAAK;AAE/D,MAAG,IAAI,YAAY,aAAa,CAAC,OAAO,cAAc;IAEzD;AAED,UAAQ,SAAS,OAAO,gBAAgB,CACtC,4DACM;AACJ,MAAG,MAAM,OAAO,YAAY,MAAM,iCAAiC;IACjE,aAAa,OAAO;IACpB,GAAG;IACJ,CAAC;AAEF,MAAG,KAAK,QAAQ,CAAC,IAAI,sBAAsB,CAAC,OAAO,UAAU,IAAI;AAGjE,MAAG,YAAY,MAAM,OAAO,EAAE,SAAS,KAAM,CAAC,CAAC,OAAO,aAAa;AACnE,MAAG,YAAY,MAAM,MAAM,CAAC,OAC1B,gBACA,+BACD;AAED,MAAG,YAAY,MAAM,OAAO,CAAC,OAAO,aAAa;IAEpD;GACD"}
@@ -0,0 +1,129 @@
1
+ /// <reference types="cypress" />
2
+ /// <reference types="cypress" />
3
+ /**
4
+ * Per-test feature flags for enabling/disabling individual test cases
5
+ * in the e2e-ui suites.
6
+ *
7
+ * The Verdaccio UI behaves differently across branches — search result
8
+ * payload shape differs between the `/-/v1/search` and
9
+ * `/-/verdaccio/data/search/*` endpoints, the language picker was
10
+ * added in a specific minor, etc. Rather than forking the suite per
11
+ * branch, consumers can disable individual tests via
12
+ * `createRegistryConfig({ features: { … } })`.
13
+ *
14
+ * Every flag defaults to `true` (test runs). Override to `false` to
15
+ * convert the test into a Mocha `it.skip` call — the suite still
16
+ * reports the test but marks it as pending.
17
+ */
18
+ export interface Features {
19
+ search: {
20
+ /**
21
+ * Whether to run the "results dropdown renders a matching
22
+ * package" test. Disable on builds where the search result shape
23
+ * or the Autocomplete rendering differs from the default assumption.
24
+ */
25
+ resultsDropdown: boolean;
26
+ /**
27
+ * Whether to run the "clicking a result navigates to detail" test.
28
+ * Depends on the Autocomplete `onSelectItem` → router wiring.
29
+ */
30
+ resultClickNavigation: boolean;
31
+ };
32
+ home: {
33
+ /**
34
+ * Whether to run the `with a published package` sub-block inside
35
+ * `homeTests` (publishes a throwaway package and asserts it
36
+ * renders in the package list).
37
+ */
38
+ publishedPackageRendering: boolean;
39
+ };
40
+ settings: {
41
+ /**
42
+ * Whether to run the "change language via card click" test.
43
+ * Depends on the `LanguageSwitch` component layout and its
44
+ * translation sentinels ("Translations", "German"). Skip on
45
+ * builds that lag behind the upstream translation file.
46
+ */
47
+ languageSwitcher: boolean;
48
+ };
49
+ signin: {
50
+ /**
51
+ * Whether to run the three validation tests (disabled submit
52
+ * button, invalid credentials error banner). Depends on the
53
+ * current yup schema and the hard-coded "Invalid username or
54
+ * password" error string.
55
+ */
56
+ validationTests: boolean;
57
+ };
58
+ layout: {
59
+ /**
60
+ * Whether to run the dark/light theme switch test. Depends on
61
+ * `web.showThemeSwitch` (defaults to true in ui-theme) and the
62
+ * `header--button--light` / `header--button--dark` testids.
63
+ */
64
+ themeSwitch: boolean;
65
+ };
66
+ publish: {
67
+ /**
68
+ * Whether to run the "tarball download button fires a GET" test.
69
+ * Depends on `web.showDownloadTarball` (defaults to true) and
70
+ * the published package manifest having a valid `dist.tarball`.
71
+ */
72
+ downloadTarball: boolean;
73
+ /**
74
+ * Whether to run the "raw viewer dialog opens + closes" test.
75
+ * Depends on `web.showRaw` (defaults to true).
76
+ */
77
+ rawViewer: boolean;
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
+ };
114
+ }
115
+ /** Defaults: all flags on. */
116
+ export declare const DEFAULT_FEATURES: Features;
117
+ import type { DeepPartial } from './testIds';
118
+ /**
119
+ * Merge user overrides into the default feature flags. Per-section,
120
+ * one level deep — matching the style of `mergeTestIds`.
121
+ */
122
+ export declare function mergeFeatures(defaults: Features, overrides?: DeepPartial<Features>): Features;
123
+ /**
124
+ * Helper that returns either `it` or `it.skip` depending on an
125
+ * enabled flag. Usage:
126
+ *
127
+ * maybeIt(features.search.resultsDropdown)('…', () => { … });
128
+ */
129
+ export declare function maybeIt(enabled: boolean): Mocha.TestFunction | Mocha.PendingTestFunction;
package/build/index.d.ts CHANGED
@@ -3,9 +3,11 @@ import { RegistryConfig, VerdaccioUiOptions } from './types';
3
3
  export type { RegistryConfig, RegistryTaskResult, VerdaccioUiOptions, } from './types';
4
4
  export type { PublishPackageInput, PublishPackageTaskInput, PublishPackageResult, UnpublishPackageInput, UnpublishPackageResult, } from './tasks';
5
5
  export type { DeepPartial, Selectors, TestIds } from './testIds';
6
+ export type { Features } from './features';
6
7
  export { DEFAULT_SELECTORS, DEFAULT_TEST_IDS } from './testIds';
8
+ export { DEFAULT_FEATURES, maybeIt } from './features';
7
9
  export { publishPackage, cleanupPublished, unpublishPackage } from './tasks';
8
- export { homeTests, signinTests, publishTests, searchTests, settingsTests, layoutTests, } from './tests';
10
+ export { homeTests, signinTests, publishTests, searchTests, settingsTests, layoutTests, changePasswordTests, } from './tests';
9
11
  /**
10
12
  * Build a full RegistryConfig from user-provided options with defaults.
11
13
  *
@@ -41,6 +41,14 @@ export interface TestIds {
41
41
  settingsTooltip: string;
42
42
  /** Info icon that opens the registry info dialog. */
43
43
  infoTooltip: string;
44
+ /**
45
+ * Theme switch button shown while in LIGHT mode (clicking it
46
+ * flips to dark). The underlying component swaps between this
47
+ * and `themeSwitchDark` based on `isDarkMode`.
48
+ */
49
+ themeSwitchLight: string;
50
+ /** Theme switch button shown while in DARK mode. */
51
+ themeSwitchDark: string;
44
52
  /** Menu icon shown after login (opens the logged-in menu). */
45
53
  logInDialogIcon: string;
46
54
  /** "Log out" entry inside the logged-in menu. */
@@ -54,6 +62,18 @@ export interface TestIds {
54
62
  /** "Powered by" version text on the right side of the footer. */
55
63
  version: string;
56
64
  };
65
+ login: {
66
+ /** Login dialog container (the MUI Dialog root). */
67
+ dialog: string;
68
+ /** DialogContent wrapper inside the login dialog. */
69
+ dialogContent: string;
70
+ /**
71
+ * Error banner shown inside the login dialog when the server
72
+ * rejects credentials (or any other `errors.root` message the form
73
+ * sets). Renders inside the `LoginDialogFormError` component.
74
+ */
75
+ error: string;
76
+ };
57
77
  package: {
58
78
  /** Wrapper around the list of packages on the home page. */
59
79
  itemList: string;
@@ -85,6 +105,14 @@ export interface TestIds {
85
105
  uplinksTab: string;
86
106
  /** Empty-state message when the package has no uplinks. */
87
107
  noUplinks: string;
108
+ /** Action-bar tarball download FAB. */
109
+ downloadTarballBtn: string;
110
+ /** Action-bar "view raw manifest" FAB. */
111
+ rawBtn: string;
112
+ /** Full-screen dialog that opens when `rawBtn` is clicked. */
113
+ rawViewerDialog: string;
114
+ /** Close button inside the raw viewer dialog. */
115
+ closeRawViewer: string;
88
116
  };
89
117
  }
90
118
  /**
@@ -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;
@@ -4,3 +4,4 @@ export { publishTests } from './publish';
4
4
  export { searchTests } from './search';
5
5
  export { settingsTests } from './settings';
6
6
  export { layoutTests } from './layout';
7
+ export { changePasswordTests } from './change-password';
package/build/types.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /// <reference types="cypress" />
2
2
  /// <reference types="cypress" />
3
3
  /// <reference types="cypress" />
4
+ import type { Features } from './features';
4
5
  import type { PublishPackageResult, PublishPackageTaskInput, UnpublishPackageInput, UnpublishPackageResult } from './tasks';
5
6
  import type { DeepPartial, Selectors, TestIds } from './testIds';
6
7
  export type RegistryConfig = {
@@ -15,6 +16,11 @@ export type RegistryConfig = {
15
16
  testIds: TestIds;
16
17
  /** Fully-resolved CSS selector map (defaults merged with overrides). */
17
18
  selectors: Selectors;
19
+ /**
20
+ * Per-test feature flags — set to `false` to convert a specific
21
+ * test into `it.skip` without forking the suite.
22
+ */
23
+ features: Features;
18
24
  };
19
25
  export type VerdaccioUiOptions = {
20
26
  registryUrl: string;
@@ -28,6 +34,8 @@ export type VerdaccioUiOptions = {
28
34
  testIds?: DeepPartial<TestIds>;
29
35
  /** Partial override of the default CSS selector map. */
30
36
  selectors?: DeepPartial<Selectors>;
37
+ /** Partial override of the default feature flags. */
38
+ features?: DeepPartial<Features>;
31
39
  };
32
40
  /**
33
41
  * Return value of the `registry` Cypress task — exposed as a named
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verdaccio/e2e-ui",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Cypress plugin for Verdaccio UI e2e tests — run against any registry",