@verdaccio/e2e-ui 2.6.0 → 2.8.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.
- package/build/cjs/features.cjs +40 -3
- package/build/cjs/features.cjs.map +1 -1
- package/build/cjs/index.cjs +10 -0
- package/build/cjs/index.cjs.map +1 -1
- package/build/cjs/tasks/publish.cjs +5 -4
- package/build/cjs/tasks/publish.cjs.map +1 -1
- package/build/cjs/testIds.cjs +23 -1
- package/build/cjs/testIds.cjs.map +1 -1
- package/build/cjs/tests/change-password.cjs +20 -7
- package/build/cjs/tests/change-password.cjs.map +1 -1
- package/build/cjs/tests/detail.cjs +33 -0
- package/build/cjs/tests/detail.cjs.map +1 -1
- package/build/cjs/tests/failure-modes.cjs +22 -0
- package/build/cjs/tests/failure-modes.cjs.map +1 -1
- package/build/cjs/tests/home-detail-ux.cjs +166 -0
- package/build/cjs/tests/home-detail-ux.cjs.map +1 -0
- package/build/cjs/tests/index.cjs +6 -0
- package/build/cjs/tests/manifest-rendering.cjs +73 -0
- package/build/cjs/tests/manifest-rendering.cjs.map +1 -0
- package/build/cjs/tests/publish.cjs +1 -1
- package/build/cjs/tests/publish.cjs.map +1 -1
- package/build/cjs/tests/session.cjs +49 -0
- package/build/cjs/tests/session.cjs.map +1 -0
- package/build/cjs/tests/settings.cjs +33 -2
- package/build/cjs/tests/settings.cjs.map +1 -1
- package/build/esm/features.js +40 -3
- package/build/esm/features.js.map +1 -1
- package/build/esm/index.js +8 -1
- package/build/esm/index.js.map +1 -1
- package/build/esm/tasks/publish.js +5 -4
- package/build/esm/tasks/publish.js.map +1 -1
- package/build/esm/testIds.js +23 -1
- package/build/esm/testIds.js.map +1 -1
- package/build/esm/tests/change-password.js +20 -7
- package/build/esm/tests/change-password.js.map +1 -1
- package/build/esm/tests/detail.js +33 -0
- package/build/esm/tests/detail.js.map +1 -1
- package/build/esm/tests/failure-modes.js +22 -0
- package/build/esm/tests/failure-modes.js.map +1 -1
- package/build/esm/tests/home-detail-ux.js +166 -0
- package/build/esm/tests/home-detail-ux.js.map +1 -0
- package/build/esm/tests/index.js +4 -1
- package/build/esm/tests/manifest-rendering.js +73 -0
- package/build/esm/tests/manifest-rendering.js.map +1 -0
- package/build/esm/tests/publish.js +1 -1
- package/build/esm/tests/publish.js.map +1 -1
- package/build/esm/tests/session.js +49 -0
- package/build/esm/tests/session.js.map +1 -0
- package/build/esm/tests/settings.js +33 -2
- package/build/esm/tests/settings.js.map +1 -1
- package/build/features.d.ts +105 -0
- package/build/index.d.ts +1 -1
- package/build/tasks/publish.d.ts +9 -0
- package/build/testIds.d.ts +34 -0
- package/build/tests/home-detail-ux.d.ts +10 -0
- package/build/tests/index.d.ts +3 -0
- package/build/tests/manifest-rendering.d.ts +14 -0
- package/build/tests/session.d.ts +2 -0
- package/package.json +1 -1
package/build/esm/testIds.js
CHANGED
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
var DEFAULT_TEST_IDS = {
|
|
7
7
|
home: {
|
|
8
8
|
helpCard: "help-card",
|
|
9
|
-
notFound: "404"
|
|
9
|
+
notFound: "404",
|
|
10
|
+
packageListToolbar: "package-list-toolbar",
|
|
11
|
+
packageListFilter: "package-list-filter",
|
|
12
|
+
packageListScope: "package-list-scope",
|
|
13
|
+
packageListNoMatch: "package-list-no-match",
|
|
14
|
+
helpSetupTabPrefix: "help-setup-",
|
|
15
|
+
helpUplinksNote: "help-uplinks-note",
|
|
16
|
+
helpGuide: "help-guide"
|
|
10
17
|
},
|
|
11
18
|
header: {
|
|
12
19
|
container: "header",
|
|
@@ -17,6 +24,8 @@ var DEFAULT_TEST_IDS = {
|
|
|
17
24
|
customLogo: "custom-logo",
|
|
18
25
|
loginButton: "header--button-login",
|
|
19
26
|
settingsTooltip: "header--tooltip-settings",
|
|
27
|
+
settingsPublishTab: "settings-publish-tab",
|
|
28
|
+
settingsPublishPanel: "settings-publish",
|
|
20
29
|
infoTooltip: "header--tooltip-info",
|
|
21
30
|
themeSwitchLight: "header--button--light",
|
|
22
31
|
themeSwitchDark: "header--button--dark",
|
|
@@ -36,12 +45,17 @@ var DEFAULT_TEST_IDS = {
|
|
|
36
45
|
package: {
|
|
37
46
|
itemList: "package-item-list",
|
|
38
47
|
title: "package-title",
|
|
48
|
+
versionLink: "package-version-link",
|
|
39
49
|
readme: "readme",
|
|
40
50
|
sidebar: "sidebar",
|
|
41
51
|
installList: "installList",
|
|
42
52
|
installNpm: "installListItem-npm",
|
|
43
53
|
installYarn: "installListItem-yarn",
|
|
44
54
|
installPnpm: "installListItem-pnpm",
|
|
55
|
+
installBun: "installListItem-bun",
|
|
56
|
+
installNpmrc: "install-npmrc",
|
|
57
|
+
installNpmrcCopy: "installNpmrc",
|
|
58
|
+
readmeTab: "readme-tab",
|
|
45
59
|
keywordList: "keyword-list",
|
|
46
60
|
dependenciesTab: "dependencies-tab",
|
|
47
61
|
dependencies: "dependencies",
|
|
@@ -55,6 +69,10 @@ var DEFAULT_TEST_IDS = {
|
|
|
55
69
|
rawViewerDialog: "rawViewer--dialog",
|
|
56
70
|
closeRawViewer: "close-raw-viewer"
|
|
57
71
|
},
|
|
72
|
+
search: {
|
|
73
|
+
groupPrefix: "search-group-",
|
|
74
|
+
originPrefix: "search-origin-"
|
|
75
|
+
},
|
|
58
76
|
errors: { genericError: "generic-error" }
|
|
59
77
|
};
|
|
60
78
|
/**
|
|
@@ -105,6 +123,10 @@ function mergeTestIds(defaults, overrides) {
|
|
|
105
123
|
...defaults.package,
|
|
106
124
|
...overrides.package
|
|
107
125
|
},
|
|
126
|
+
search: {
|
|
127
|
+
...defaults.search,
|
|
128
|
+
...overrides.search
|
|
129
|
+
},
|
|
108
130
|
errors: {
|
|
109
131
|
...defaults.errors,
|
|
110
132
|
...overrides.errors
|
package/build/esm/testIds.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testIds.js","names":[],"sources":["../../src/testIds.ts"],"sourcesContent":["/**\n * Configurable DOM selectors used by the e2e-ui test suites.\n *\n * The Verdaccio UI is a moving target — data-testids can and do change\n * between majors — so every selector referenced by a test lives here\n * and can be overridden by consumers of `@verdaccio/e2e-ui` via\n * `createRegistryConfig({ testIds, selectors })`.\n *\n * The defaults below match Verdaccio 6.x as of the last time we audited\n * the ui-components source. If a selector moves, override just the\n * affected field instead of forking the suite.\n */\n\n/**\n * Map of data-testid values used by the test suites, grouped by UI\n * section. Each field holds the bare testid string (no `data-testid=\"…\"`\n * wrapping) — the test helpers pass it through `cy.getByTestId(...)`.\n */\nexport interface TestIds {\n home: {\n /** Help card shown on the empty-registry landing page. */\n helpCard: string;\n /** 404 \"not found\" container. */\n notFound: string;\n };\n header: {\n /** Outermost `<NavBar>` element. */\n container: string;\n /** Inner wrapper inside the nav bar. */\n innerNavBar: string;\n /** Right-side action cluster wrapper. */\n right: string;\n /** Wrapper around the header search input. */\n searchContainer: string;\n /** Default SVG Verdaccio logo. */\n defaultLogo: string;\n /** Custom (user-provided) logo image. */\n customLogo: string;\n /** \"Login\" button shown when logged out. */\n loginButton: string;\n /** Gear icon that opens the settings dialog. */\n settingsTooltip: string;\n /** Info icon that opens the registry info dialog. */\n infoTooltip: string;\n /**\n * Theme switch button shown while in LIGHT mode (clicking it\n * flips to dark). The underlying component swaps between this\n * and `themeSwitchDark` based on `isDarkMode`.\n */\n themeSwitchLight: string;\n /** Theme switch button shown while in DARK mode. */\n themeSwitchDark: string;\n /** Menu icon shown after login (opens the logged-in menu). */\n logInDialogIcon: string;\n /** \"Log out\" entry inside the logged-in menu. */\n logOutDialogIcon: string;\n /** \"Hi <username>\" label inside the logged-in menu. */\n greetingsLabel: string;\n };\n footer: {\n /** Outer footer wrapper. */\n container: string;\n /** \"Powered by\" version text on the right side of the footer. */\n version: string;\n };\n login: {\n /** Login dialog container (the MUI Dialog root). */\n dialog: string;\n /** DialogContent wrapper inside the login dialog. */\n dialogContent: string;\n /**\n * Error banner shown inside the login dialog when the server\n * rejects credentials (or any other `errors.root` message the form\n * sets). Renders inside the `LoginDialogFormError` component.\n */\n error: string;\n };\n package: {\n /** Wrapper around the list of packages on the home page. */\n itemList: string;\n /** Package name link in the package list (home + search results). */\n title: string;\n /** Readme container on the package detail page. */\n readme: string;\n /** Sidebar container on the package detail page. */\n sidebar: string;\n /** Install commands section list. */\n installList: string;\n /** Individual install line for npm. */\n installNpm: string;\n /** Individual install line for yarn. */\n installYarn: string;\n /** Individual install line for pnpm. */\n installPnpm: string;\n /** Keyword list below the install section. */\n keywordList: string;\n /** Tab that reveals the dependencies view. */\n dependenciesTab: string;\n /** Dependencies list wrapper (one entry per dep). */\n dependencies: string;\n /** Tab that reveals the versions view. */\n versionsTab: string;\n /** \"latest\" tag row inside the versions view. */\n tagLatest: string;\n /** Tab that reveals the uplinks view. */\n uplinksTab: string;\n /** Empty-state message when the package has no uplinks. */\n noUplinks: string;\n /** Action-bar tarball download FAB. */\n downloadTarballBtn: string;\n /** Package list tarball download button. */\n downloadTarball: string;\n /** Action-bar \"view raw manifest\" FAB. */\n rawBtn: string;\n /** Full-screen dialog that opens when `rawBtn` is clicked. */\n rawViewerDialog: string;\n /** Close button inside the raw viewer dialog. */\n closeRawViewer: string;\n };\n errors: {\n /**\n * Generic error page shown when a data request fails without a\n * specific status (5xx, network failure). Added by\n * verdaccio/verdaccio#6210 — only present on builds with that fix.\n */\n genericError: string;\n };\n}\n\n/**\n * CSS selectors (not data-testids) used by the test suites. These are\n * things like form-field IDs and framework-specific class names that\n * Verdaccio's UI exposes as plain selectors rather than testids.\n */\nexport interface Selectors {\n /** Class applied to the parsed README markdown body. */\n markdownBody: string;\n loginDialog: {\n /** Username text input inside the login dialog. */\n usernameInput: string;\n /** Password text input inside the login dialog. */\n passwordInput: string;\n /** Submit button inside the login dialog. */\n submitButton: string;\n };\n signup: {\n /** Username input on /-/web/add-user (shares ids with the login form). */\n usernameInput: string;\n /** Password input on /-/web/add-user. */\n passwordInput: string;\n /** Email input on /-/web/add-user (only email-typed input on the page). */\n emailInput: string;\n /** Submit button on /-/web/add-user. */\n submitButton: string;\n };\n}\n\n/**\n * Defaults matching Verdaccio 6.x (bundled ui-theme@9.0.0-next-9.x).\n * Overridable via `createRegistryConfig({ testIds: { ... } })`.\n */\nexport const DEFAULT_TEST_IDS: TestIds = {\n home: {\n helpCard: 'help-card',\n notFound: '404',\n },\n header: {\n container: 'header',\n innerNavBar: 'inner-nav-bar',\n right: 'header-right',\n searchContainer: 'search-container',\n defaultLogo: 'default-logo',\n customLogo: 'custom-logo',\n loginButton: 'header--button-login',\n settingsTooltip: 'header--tooltip-settings',\n infoTooltip: 'header--tooltip-info',\n themeSwitchLight: 'header--button--light',\n themeSwitchDark: 'header--button--dark',\n logInDialogIcon: 'logInDialogIcon',\n logOutDialogIcon: 'logOutDialogIcon',\n greetingsLabel: 'greetings-label',\n },\n footer: {\n container: 'footer',\n version: 'version-footer',\n },\n login: {\n dialog: 'login--dialog',\n dialogContent: 'dialogContentLogin',\n error: 'error',\n },\n package: {\n itemList: 'package-item-list',\n title: 'package-title',\n readme: 'readme',\n sidebar: 'sidebar',\n installList: 'installList',\n installNpm: 'installListItem-npm',\n installYarn: 'installListItem-yarn',\n installPnpm: 'installListItem-pnpm',\n keywordList: 'keyword-list',\n dependenciesTab: 'dependencies-tab',\n dependencies: 'dependencies',\n versionsTab: 'versions-tab',\n tagLatest: 'tag-latest',\n uplinksTab: 'uplinks-tab',\n noUplinks: 'no-uplinks',\n downloadTarballBtn: 'download-tarball-btn',\n downloadTarball: 'download-tarball',\n rawBtn: 'raw-btn',\n rawViewerDialog: 'rawViewer--dialog',\n closeRawViewer: 'close-raw-viewer',\n },\n errors: {\n genericError: 'generic-error',\n },\n};\n\n/**\n * Defaults for non-testid CSS selectors. Overridable via\n * `createRegistryConfig({ selectors: { ... } })`.\n */\nexport const DEFAULT_SELECTORS: Selectors = {\n markdownBody: '.markdown-body',\n loginDialog: {\n usernameInput: '#login--dialog-username',\n passwordInput: '#login--dialog-password',\n submitButton: '#login--dialog-button-submit',\n },\n signup: {\n usernameInput: '#login--dialog-username',\n passwordInput: '#login--dialog-password',\n emailInput: 'input[type=\"email\"]',\n submitButton: 'button[type=\"submit\"]',\n },\n};\n\n/** Deep-partial helper — every field of a nested object becomes optional. */\nexport type DeepPartial<T> = {\n [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];\n};\n\n/**\n * Merge user overrides into the default testIds map. Merging is\n * per-section (one level deep): `overrides.header` replaces individual\n * fields under `defaults.header` without touching `defaults.footer`.\n * The shape is fixed and small, so we enumerate sections by hand\n * rather than relying on a recursive generic merger.\n */\nexport function mergeTestIds(defaults: TestIds, overrides?: DeepPartial<TestIds>): TestIds {\n if (!overrides) return defaults;\n return {\n home: { ...defaults.home, ...overrides.home },\n header: { ...defaults.header, ...overrides.header },\n footer: { ...defaults.footer, ...overrides.footer },\n login: { ...defaults.login, ...overrides.login },\n package: { ...defaults.package, ...overrides.package },\n errors: { ...defaults.errors, ...overrides.errors },\n };\n}\n\n/** Same idea as `mergeTestIds`, for the CSS-selector block. */\nexport function mergeSelectors(defaults: Selectors, overrides?: DeepPartial<Selectors>): Selectors {\n if (!overrides) return defaults;\n return {\n markdownBody: overrides.markdownBody ?? defaults.markdownBody,\n loginDialog: { ...defaults.loginDialog, ...overrides.loginDialog },\n signup: { ...defaults.signup, ...overrides.signup },\n };\n}\n"],"mappings":";;;;;AAiKA,IAAa,mBAA4B;CACvC,MAAM;EACJ,UAAU;EACV,UAAU;CACZ;CACA,QAAQ;EACN,WAAW;EACX,aAAa;EACb,OAAO;EACP,iBAAiB;EACjB,aAAa;EACb,YAAY;EACZ,aAAa;EACb,iBAAiB;EACjB,aAAa;EACb,kBAAkB;EAClB,iBAAiB;EACjB,iBAAiB;EACjB,kBAAkB;EAClB,gBAAgB;CAClB;CACA,QAAQ;EACN,WAAW;EACX,SAAS;CACX;CACA,OAAO;EACL,QAAQ;EACR,eAAe;EACf,OAAO;CACT;CACA,SAAS;EACP,UAAU;EACV,OAAO;EACP,QAAQ;EACR,SAAS;EACT,aAAa;EACb,YAAY;EACZ,aAAa;EACb,aAAa;EACb,aAAa;EACb,iBAAiB;EACjB,cAAc;EACd,aAAa;EACb,WAAW;EACX,YAAY;EACZ,WAAW;EACX,oBAAoB;EACpB,iBAAiB;EACjB,QAAQ;EACR,iBAAiB;EACjB,gBAAgB;CAClB;CACA,QAAQ,EACN,cAAc,gBAChB;AACF;;;;;AAMA,IAAa,oBAA+B;CAC1C,cAAc;CACd,aAAa;EACX,eAAe;EACf,eAAe;EACf,cAAc;CAChB;CACA,QAAQ;EACN,eAAe;EACf,eAAe;EACf,YAAY;EACZ,cAAc;CAChB;AACF;;;;;;;;AAcA,SAAgB,aAAa,UAAmB,WAA2C;CACzF,IAAI,CAAC,WAAW,OAAO;CACvB,OAAO;EACL,MAAM;GAAE,GAAG,SAAS;GAAM,GAAG,UAAU;EAAK;EAC5C,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;EAAO;EAClD,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;EAAO;EAClD,OAAO;GAAE,GAAG,SAAS;GAAO,GAAG,UAAU;EAAM;EAC/C,SAAS;GAAE,GAAG,SAAS;GAAS,GAAG,UAAU;EAAQ;EACrD,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;EAAO;CACpD;AACF;;AAGA,SAAgB,eAAe,UAAqB,WAA+C;CACjG,IAAI,CAAC,WAAW,OAAO;CACvB,OAAO;EACL,cAAc,UAAU,gBAAgB,SAAS;EACjD,aAAa;GAAE,GAAG,SAAS;GAAa,GAAG,UAAU;EAAY;EACjE,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;EAAO;CACpD;AACF"}
|
|
1
|
+
{"version":3,"file":"testIds.js","names":[],"sources":["../../src/testIds.ts"],"sourcesContent":["/**\n * Configurable DOM selectors used by the e2e-ui test suites.\n *\n * The Verdaccio UI is a moving target — data-testids can and do change\n * between majors — so every selector referenced by a test lives here\n * and can be overridden by consumers of `@verdaccio/e2e-ui` via\n * `createRegistryConfig({ testIds, selectors })`.\n *\n * The defaults below match Verdaccio 6.x as of the last time we audited\n * the ui-components source. If a selector moves, override just the\n * affected field instead of forking the suite.\n */\n\n/**\n * Map of data-testid values used by the test suites, grouped by UI\n * section. Each field holds the bare testid string (no `data-testid=\"…\"`\n * wrapping) — the test helpers pass it through `cy.getByTestId(...)`.\n */\nexport interface TestIds {\n home: {\n /** Help card shown on the empty-registry landing page. */\n helpCard: string;\n /** 404 \"not found\" container. */\n notFound: string;\n /** Filter / scope / sort bar above the package list (9.x). */\n packageListToolbar: string;\n /** Text filter input inside the toolbar. */\n packageListFilter: string;\n /** Native input of the scope select (only with more than one scope). */\n packageListScope: string;\n /** Message shown when the filter matches no package. */\n packageListNoMatch: string;\n /** Prefix of the welcome card setup tabs: `<prefix><manager>`. */\n helpSetupTabPrefix: string;\n /** Note on the welcome card explaining cached uplink packages. */\n helpUplinksNote: string;\n /** Step list of the publishing guide, in the welcome card and the settings (9.x). */\n helpGuide: string;\n };\n header: {\n /** Outermost `<NavBar>` element. */\n container: string;\n /** Inner wrapper inside the nav bar. */\n innerNavBar: string;\n /** Right-side action cluster wrapper. */\n right: string;\n /** Wrapper around the header search input. */\n searchContainer: string;\n /** Default SVG Verdaccio logo. */\n defaultLogo: string;\n /** Custom (user-provided) logo image. */\n customLogo: string;\n /** \"Login\" button shown when logged out. */\n loginButton: string;\n /** Gear icon that opens the settings dialog. */\n settingsTooltip: string;\n /** \"Publishing\" tab of the settings dialog (9.x). */\n settingsPublishTab: string;\n /** Panel of the \"Publishing\" tab (9.x). */\n settingsPublishPanel: string;\n /** Info icon that opens the registry info dialog. */\n infoTooltip: string;\n /**\n * Theme switch button shown while in LIGHT mode (clicking it\n * flips to dark). The underlying component swaps between this\n * and `themeSwitchDark` based on `isDarkMode`.\n */\n themeSwitchLight: string;\n /** Theme switch button shown while in DARK mode. */\n themeSwitchDark: string;\n /** Menu icon shown after login (opens the logged-in menu). */\n logInDialogIcon: string;\n /** \"Log out\" entry inside the logged-in menu. */\n logOutDialogIcon: string;\n /** \"Hi <username>\" label inside the logged-in menu. */\n greetingsLabel: string;\n };\n footer: {\n /** Outer footer wrapper. */\n container: string;\n /** \"Powered by\" version text on the right side of the footer. */\n version: string;\n };\n login: {\n /** Login dialog container (the MUI Dialog root). */\n dialog: string;\n /** DialogContent wrapper inside the login dialog. */\n dialogContent: string;\n /**\n * Error banner shown inside the login dialog when the server\n * rejects credentials (or any other `errors.root` message the form\n * sets). Renders inside the `LoginDialogFormError` component.\n */\n error: string;\n };\n package: {\n /** Wrapper around the list of packages on the home page. */\n itemList: string;\n /** Package name link in the package list (home + search results). */\n title: string;\n /** Version link of a package list item, to its version page (9.x). */\n versionLink: string;\n /** Readme container on the package detail page. */\n readme: string;\n /** Sidebar container on the package detail page. */\n sidebar: string;\n /** Install commands section list. */\n installList: string;\n /** Individual install line for npm. */\n installNpm: string;\n /** Individual install line for yarn. */\n installYarn: string;\n /** Individual install line for pnpm. */\n installPnpm: string;\n /** Individual install line for bun (9.x). */\n installBun: string;\n /** `.npmrc` registry line below the install commands (9.x). */\n installNpmrc: string;\n /** Copy button of the `.npmrc` registry line. */\n installNpmrcCopy: string;\n /** Tab that shows the readme. */\n readmeTab: string;\n /** Keyword list below the install section. */\n keywordList: string;\n /** Tab that reveals the dependencies view. */\n dependenciesTab: string;\n /** Dependencies list wrapper (one entry per dep). */\n dependencies: string;\n /** Tab that reveals the versions view. */\n versionsTab: string;\n /** \"latest\" tag row inside the versions view. */\n tagLatest: string;\n /** Tab that reveals the uplinks view. */\n uplinksTab: string;\n /** Empty-state message when the package has no uplinks. */\n noUplinks: string;\n /** Action-bar tarball download FAB. */\n downloadTarballBtn: string;\n /** Package list tarball download button. */\n downloadTarball: string;\n /** Action-bar \"view raw manifest\" FAB. */\n rawBtn: string;\n /** Full-screen dialog that opens when `rawBtn` is clicked. */\n rawViewerDialog: string;\n /** Close button inside the raw viewer dialog. */\n closeRawViewer: string;\n };\n search: {\n /** Prefix of the origin group headers: `<prefix>private|cached|remote`. */\n groupPrefix: string;\n /** Prefix of the origin icon on each result: `<prefix>private|...`. */\n originPrefix: string;\n };\n errors: {\n /**\n * Generic error page shown when a data request fails without a\n * specific status (5xx, network failure). Added by\n * verdaccio/verdaccio#6210 — only present on builds with that fix.\n */\n genericError: string;\n };\n}\n\n/**\n * CSS selectors (not data-testids) used by the test suites. These are\n * things like form-field IDs and framework-specific class names that\n * Verdaccio's UI exposes as plain selectors rather than testids.\n */\nexport interface Selectors {\n /** Class applied to the parsed README markdown body. */\n markdownBody: string;\n loginDialog: {\n /** Username text input inside the login dialog. */\n usernameInput: string;\n /** Password text input inside the login dialog. */\n passwordInput: string;\n /** Submit button inside the login dialog. */\n submitButton: string;\n };\n signup: {\n /** Username input on /-/web/add-user (shares ids with the login form). */\n usernameInput: string;\n /** Password input on /-/web/add-user. */\n passwordInput: string;\n /** Email input on /-/web/add-user (only email-typed input on the page). */\n emailInput: string;\n /** Submit button on /-/web/add-user. */\n submitButton: string;\n };\n}\n\n/**\n * Defaults matching Verdaccio 6.x (bundled ui-theme@9.0.0-next-9.x).\n * Overridable via `createRegistryConfig({ testIds: { ... } })`.\n */\nexport const DEFAULT_TEST_IDS: TestIds = {\n home: {\n helpCard: 'help-card',\n notFound: '404',\n packageListToolbar: 'package-list-toolbar',\n packageListFilter: 'package-list-filter',\n packageListScope: 'package-list-scope',\n packageListNoMatch: 'package-list-no-match',\n helpSetupTabPrefix: 'help-setup-',\n helpUplinksNote: 'help-uplinks-note',\n helpGuide: 'help-guide',\n },\n header: {\n container: 'header',\n innerNavBar: 'inner-nav-bar',\n right: 'header-right',\n searchContainer: 'search-container',\n defaultLogo: 'default-logo',\n customLogo: 'custom-logo',\n loginButton: 'header--button-login',\n settingsTooltip: 'header--tooltip-settings',\n settingsPublishTab: 'settings-publish-tab',\n settingsPublishPanel: 'settings-publish',\n infoTooltip: 'header--tooltip-info',\n themeSwitchLight: 'header--button--light',\n themeSwitchDark: 'header--button--dark',\n logInDialogIcon: 'logInDialogIcon',\n logOutDialogIcon: 'logOutDialogIcon',\n greetingsLabel: 'greetings-label',\n },\n footer: {\n container: 'footer',\n version: 'version-footer',\n },\n login: {\n dialog: 'login--dialog',\n dialogContent: 'dialogContentLogin',\n error: 'error',\n },\n package: {\n itemList: 'package-item-list',\n title: 'package-title',\n versionLink: 'package-version-link',\n readme: 'readme',\n sidebar: 'sidebar',\n installList: 'installList',\n installNpm: 'installListItem-npm',\n installYarn: 'installListItem-yarn',\n installPnpm: 'installListItem-pnpm',\n installBun: 'installListItem-bun',\n installNpmrc: 'install-npmrc',\n installNpmrcCopy: 'installNpmrc',\n readmeTab: 'readme-tab',\n keywordList: 'keyword-list',\n dependenciesTab: 'dependencies-tab',\n dependencies: 'dependencies',\n versionsTab: 'versions-tab',\n tagLatest: 'tag-latest',\n uplinksTab: 'uplinks-tab',\n noUplinks: 'no-uplinks',\n downloadTarballBtn: 'download-tarball-btn',\n downloadTarball: 'download-tarball',\n rawBtn: 'raw-btn',\n rawViewerDialog: 'rawViewer--dialog',\n closeRawViewer: 'close-raw-viewer',\n },\n search: {\n groupPrefix: 'search-group-',\n originPrefix: 'search-origin-',\n },\n errors: {\n genericError: 'generic-error',\n },\n};\n\n/**\n * Defaults for non-testid CSS selectors. Overridable via\n * `createRegistryConfig({ selectors: { ... } })`.\n */\nexport const DEFAULT_SELECTORS: Selectors = {\n markdownBody: '.markdown-body',\n loginDialog: {\n usernameInput: '#login--dialog-username',\n passwordInput: '#login--dialog-password',\n submitButton: '#login--dialog-button-submit',\n },\n signup: {\n usernameInput: '#login--dialog-username',\n passwordInput: '#login--dialog-password',\n emailInput: 'input[type=\"email\"]',\n submitButton: 'button[type=\"submit\"]',\n },\n};\n\n/** Deep-partial helper — every field of a nested object becomes optional. */\nexport type DeepPartial<T> = {\n [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];\n};\n\n/**\n * Merge user overrides into the default testIds map. Merging is\n * per-section (one level deep): `overrides.header` replaces individual\n * fields under `defaults.header` without touching `defaults.footer`.\n * The shape is fixed and small, so we enumerate sections by hand\n * rather than relying on a recursive generic merger.\n */\nexport function mergeTestIds(defaults: TestIds, overrides?: DeepPartial<TestIds>): TestIds {\n if (!overrides) return defaults;\n return {\n home: { ...defaults.home, ...overrides.home },\n header: { ...defaults.header, ...overrides.header },\n footer: { ...defaults.footer, ...overrides.footer },\n login: { ...defaults.login, ...overrides.login },\n package: { ...defaults.package, ...overrides.package },\n search: { ...defaults.search, ...overrides.search },\n errors: { ...defaults.errors, ...overrides.errors },\n };\n}\n\n/** Same idea as `mergeTestIds`, for the CSS-selector block. */\nexport function mergeSelectors(defaults: Selectors, overrides?: DeepPartial<Selectors>): Selectors {\n if (!overrides) return defaults;\n return {\n markdownBody: overrides.markdownBody ?? defaults.markdownBody,\n loginDialog: { ...defaults.loginDialog, ...overrides.loginDialog },\n signup: { ...defaults.signup, ...overrides.signup },\n };\n}\n"],"mappings":";;;;;AAmMA,IAAa,mBAA4B;CACvC,MAAM;EACJ,UAAU;EACV,UAAU;EACV,oBAAoB;EACpB,mBAAmB;EACnB,kBAAkB;EAClB,oBAAoB;EACpB,oBAAoB;EACpB,iBAAiB;EACjB,WAAW;CACb;CACA,QAAQ;EACN,WAAW;EACX,aAAa;EACb,OAAO;EACP,iBAAiB;EACjB,aAAa;EACb,YAAY;EACZ,aAAa;EACb,iBAAiB;EACjB,oBAAoB;EACpB,sBAAsB;EACtB,aAAa;EACb,kBAAkB;EAClB,iBAAiB;EACjB,iBAAiB;EACjB,kBAAkB;EAClB,gBAAgB;CAClB;CACA,QAAQ;EACN,WAAW;EACX,SAAS;CACX;CACA,OAAO;EACL,QAAQ;EACR,eAAe;EACf,OAAO;CACT;CACA,SAAS;EACP,UAAU;EACV,OAAO;EACP,aAAa;EACb,QAAQ;EACR,SAAS;EACT,aAAa;EACb,YAAY;EACZ,aAAa;EACb,aAAa;EACb,YAAY;EACZ,cAAc;EACd,kBAAkB;EAClB,WAAW;EACX,aAAa;EACb,iBAAiB;EACjB,cAAc;EACd,aAAa;EACb,WAAW;EACX,YAAY;EACZ,WAAW;EACX,oBAAoB;EACpB,iBAAiB;EACjB,QAAQ;EACR,iBAAiB;EACjB,gBAAgB;CAClB;CACA,QAAQ;EACN,aAAa;EACb,cAAc;CAChB;CACA,QAAQ,EACN,cAAc,gBAChB;AACF;;;;;AAMA,IAAa,oBAA+B;CAC1C,cAAc;CACd,aAAa;EACX,eAAe;EACf,eAAe;EACf,cAAc;CAChB;CACA,QAAQ;EACN,eAAe;EACf,eAAe;EACf,YAAY;EACZ,cAAc;CAChB;AACF;;;;;;;;AAcA,SAAgB,aAAa,UAAmB,WAA2C;CACzF,IAAI,CAAC,WAAW,OAAO;CACvB,OAAO;EACL,MAAM;GAAE,GAAG,SAAS;GAAM,GAAG,UAAU;EAAK;EAC5C,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;EAAO;EAClD,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;EAAO;EAClD,OAAO;GAAE,GAAG,SAAS;GAAO,GAAG,UAAU;EAAM;EAC/C,SAAS;GAAE,GAAG,SAAS;GAAS,GAAG,UAAU;EAAQ;EACrD,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;EAAO;EAClD,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;EAAO;CACpD;AACF;;AAGA,SAAgB,eAAe,UAAqB,WAA+C;CACjG,IAAI,CAAC,WAAW,OAAO;CACvB,OAAO;EACL,cAAc,UAAU,gBAAgB,SAAS;EACjD,aAAa;GAAE,GAAG,SAAS;GAAa,GAAG,UAAU;EAAY;EACjE,QAAQ;GAAE,GAAG,SAAS;GAAQ,GAAG,UAAU;EAAO;CACpD;AACF"}
|
|
@@ -31,7 +31,13 @@ function changePasswordTests(config) {
|
|
|
31
31
|
const { header, login } = config.testIds;
|
|
32
32
|
const { loginDialog } = config.selectors;
|
|
33
33
|
const { features } = config;
|
|
34
|
-
const
|
|
34
|
+
const typeUsernameIfPresent = (user) => {
|
|
35
|
+
cy.get("input[name=\"oldPassword\"]").should("exist");
|
|
36
|
+
cy.get("form").then(($form) => {
|
|
37
|
+
const $username = $form.find("input[name=\"username\"]");
|
|
38
|
+
if ($username.length) cy.wrap($username).type(user);
|
|
39
|
+
});
|
|
40
|
+
};
|
|
35
41
|
describe("change password", () => {
|
|
36
42
|
const CHANGE_PASSWORD_PATH = "/-/web/change-password";
|
|
37
43
|
const { user, password } = config.credentials;
|
|
@@ -85,7 +91,7 @@ function changePasswordTests(config) {
|
|
|
85
91
|
});
|
|
86
92
|
cy.wait("@signChangePwdRestore").its("response.statusCode").should("eq", 200);
|
|
87
93
|
cy.visit(CHANGE_PASSWORD_PATH);
|
|
88
|
-
|
|
94
|
+
typeUsernameIfPresent(user);
|
|
89
95
|
cy.get("input[name=\"oldPassword\"]").type(currentPassword);
|
|
90
96
|
cy.get("input[name=\"newPassword\"]").type(password);
|
|
91
97
|
cy.get("input[name=\"confirmPassword\"]").type(password);
|
|
@@ -96,27 +102,34 @@ function changePasswordTests(config) {
|
|
|
96
102
|
cy.get("form button[type=\"submit\"]").should("be.disabled");
|
|
97
103
|
});
|
|
98
104
|
maybeIt(features.changePassword.validation)("should keep submit disabled when new and confirm passwords mismatch", () => {
|
|
99
|
-
|
|
105
|
+
typeUsernameIfPresent(user);
|
|
100
106
|
cy.get("input[name=\"oldPassword\"]").type(currentPassword);
|
|
101
107
|
cy.get("input[name=\"newPassword\"]").type("newSecretPass123");
|
|
102
108
|
cy.get("input[name=\"confirmPassword\"]").type("different-value");
|
|
103
109
|
cy.get("form button[type=\"submit\"]").should("be.disabled");
|
|
104
110
|
});
|
|
105
|
-
maybeIt(features.changePassword.wrongOldPassword)("should show an error
|
|
111
|
+
maybeIt(features.changePassword.wrongOldPassword)("should show an error when the old password is wrong", () => {
|
|
106
112
|
cy.intercept("PUT", "/-/verdaccio/sec/reset_password").as("reset");
|
|
107
|
-
|
|
113
|
+
typeUsernameIfPresent(user);
|
|
108
114
|
cy.get("input[name=\"oldPassword\"]").type("definitely-wrong-xyz");
|
|
109
115
|
cy.get("input[name=\"newPassword\"]").type("newSecretPass123");
|
|
110
116
|
cy.get("input[name=\"confirmPassword\"]").type("newSecretPass123");
|
|
111
117
|
cy.get("form button[type=\"submit\"]").should("not.be.disabled").click();
|
|
112
118
|
cy.wait("@reset").its("response.statusCode").should("not.eq", 200);
|
|
113
|
-
cy.
|
|
119
|
+
cy.get("form", { timeout: 5e3 }).should(($form) => {
|
|
120
|
+
const $banner = $form.find(`[data-testid="${login.error}"]`);
|
|
121
|
+
const $invalidField = $form.find("input[name=\"oldPassword\"][aria-invalid=\"true\"]");
|
|
122
|
+
expect($banner.length + $invalidField.length, "error shown").to.be.greaterThan(0);
|
|
123
|
+
const message = $banner.length ? $banner.text() : $invalidField.closest(".MuiFormControl-root").find(".MuiFormHelperText-root").text();
|
|
124
|
+
expect(message.trim()).to.not.be.empty;
|
|
125
|
+
expect(message).to.not.contain("Cannot PUT");
|
|
126
|
+
});
|
|
114
127
|
cy.location("pathname").should("include", CHANGE_PASSWORD_PATH);
|
|
115
128
|
});
|
|
116
129
|
maybeIt(features.changePassword.happyPath)("should change the password and navigate to the success page", () => {
|
|
117
130
|
const newPassword = `${currentPassword}-rotated`;
|
|
118
131
|
cy.intercept("PUT", "/-/verdaccio/sec/reset_password").as("reset");
|
|
119
|
-
|
|
132
|
+
typeUsernameIfPresent(user);
|
|
120
133
|
cy.get("input[name=\"oldPassword\"]").type(currentPassword);
|
|
121
134
|
cy.get("input[name=\"newPassword\"]").type(newPassword);
|
|
122
135
|
cy.get("input[name=\"confirmPassword\"]").type(newPassword);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-password.js","names":[],"sources":["../../../src/tests/change-password.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Tests for the Change Password page at /-/web/change-password.\n *\n * The page renders only when the server is started with\n * `flags.changePassword: true` (otherwise the React component\n * redirects to `/` on mount). Each test logs in first, navigates\n * directly to the page, and drives the form.\n *\n * Selector strategy: the ChangePassword form does not ship stable\n * `id`/testid attributes on its inputs, but every field is registered\n * via react-hook-form's `register('<name>')`, which sets a stable\n * `name` attribute on the underlying `<input>`. The labels themselves\n * are `t('security.changePassword.*')` calls — when the i18n bundle\n * hasn't finished loading (or isn't loaded at all on this route), MUI\n * renders the literal i18n key as the label, so any selector that\n * matches on visible label text silently misses every field and the\n * form stays empty (which would also make the mismatch test \"pass\"\n * for the wrong reason: the submit button is disabled because the\n * form is empty, not because yup rejected the mismatch).\n *\n * The stable, i18n-independent contract from ChangePassword.tsx is:\n * register('username') → input[name=\"username\"]\n * register('oldPassword') → input[name=\"oldPassword\"]\n * register('newPassword') → input[name=\"newPassword\"]\n * register('confirmPassword') → input[name=\"confirmPassword\"]\n * submit button → form button[type=\"submit\"]\n */\nexport function changePasswordTests(config: RegistryConfig) {\n const { header, login } = config.testIds;\n const { loginDialog } = config.selectors;\n const { features } = config;\n\n // The onSubmit catch block in ChangePassword.tsx sets a hardcoded\n // English string — if the upstream component ever localizes this,\n // this constant and the wrongOldPassword test will need updating.\n const GENERIC_FAILURE_TEXT = 'Failed to change password';\n\n describe('change password', () => {\n const CHANGE_PASSWORD_PATH = '/-/web/change-password';\n const { user, password } = config.credentials;\n\n /**\n * Tests mutate the user's password. We track the \"current\" value\n * across tests so the `after()` hook can restore the original,\n * leaving the registry in the same state other suites assume.\n */\n let currentPassword = password;\n\n /**\n * Capability check. The ChangePassword page's `useEffect` redirects\n * to `/` whenever `configuration.flags.changePassword` is not truthy\n * — which is the case on any registry that either (a) didn't set\n * `flags.changePassword: true` in its config, or (b) runs a\n * verdaccio build whose middleware doesn't yet propagate the flag\n * into `__VERDACCIO_BASENAME_UI_OPTIONS` (older `verdaccio:6`\n * tagged images fall in this bucket).\n *\n * In either case the suite has nothing to exercise, so skip the\n * whole describe with a clear reason rather than burning five\n * seconds per test on cy.contains timeouts.\n */\n before(function () {\n cy.visit(config.registryUrl);\n cy.window().then((win) => {\n const opts = (win as any).__VERDACCIO_BASENAME_UI_OPTIONS;\n const enabled = !!opts?.flags?.changePassword;\n if (!enabled) {\n // eslint-disable-next-line no-console\n console.warn(\n '[change-password] server did not advertise flags.changePassword=true ' +\n '— skipping suite. ui-options.flags: ' +\n JSON.stringify(opts?.flags ?? {})\n );\n this.skip();\n }\n });\n });\n\n beforeEach(() => {\n // Intercept the login POST and wait on it explicitly instead of\n // leaning on a visual sentinel — mirrors the pattern used by\n // signinTests and avoids a race between cy.login's fire-and-forget\n // submit and the next cy.visit.\n cy.intercept('POST', '/-/verdaccio/sec/login').as('signChangePwd');\n cy.visit(config.registryUrl);\n cy.login(user, currentPassword, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@signChangePwd').its('response.statusCode').should('eq', 200);\n\n cy.visit(CHANGE_PASSWORD_PATH);\n // If flags.changePassword is off server-side, the component's\n // useEffect redirects to `/` and this assertion times out — which\n // is the correct signal that the registry is misconfigured.\n // Use the stable `type=\"submit\"` selector so the assertion is\n // independent of whether the i18n bundle has resolved by now.\n cy.get('form button[type=\"submit\"]', { timeout: 5000 }).should('be.visible');\n });\n\n after(() => {\n // Restore the original password so subsequent spec files\n // (and retries) can still log in with `config.credentials`.\n if (currentPassword === password) return;\n cy.intercept('POST', '/-/verdaccio/sec/login').as('signChangePwdRestore');\n cy.visit(config.registryUrl);\n cy.login(user, currentPassword, {\n loginButton: header.loginButton,\n ...loginDialog,\n });\n cy.wait('@signChangePwdRestore').its('response.statusCode').should('eq', 200);\n cy.visit(CHANGE_PASSWORD_PATH);\n cy.get('input[name=\"username\"]').type(user);\n cy.get('input[name=\"oldPassword\"]').type(currentPassword);\n cy.get('input[name=\"newPassword\"]').type(password);\n cy.get('input[name=\"confirmPassword\"]').type(password);\n cy.get('form button[type=\"submit\"]').click();\n currentPassword = password;\n });\n\n // ── Validation (client-side yup) ─────────────────────────────\n\n maybeIt(features.changePassword.validation)(\n 'should disable the submit button while the form is empty',\n () => {\n cy.get('form button[type=\"submit\"]').should('be.disabled');\n }\n );\n\n maybeIt(features.changePassword.validation)(\n 'should keep submit disabled when new and confirm passwords mismatch',\n () => {\n cy.get('input[name=\"username\"]').type(user);\n cy.get('input[name=\"oldPassword\"]').type(currentPassword);\n cy.get('input[name=\"newPassword\"]').type('newSecretPass123');\n cy.get('input[name=\"confirmPassword\"]').type('different-value');\n // yup schema rejects mismatch → isValid stays false → button disabled.\n cy.get('form button[type=\"submit\"]').should('be.disabled');\n }\n );\n\n // ── Server error path ────────────────────────────────────────\n\n maybeIt(features.changePassword.wrongOldPassword)(\n 'should show an error banner when the old password is wrong',\n () => {\n cy.intercept('PUT', '/-/verdaccio/sec/reset_password').as('reset');\n cy.get('input[name=\"username\"]').type(user);\n cy.get('input[name=\"oldPassword\"]').type('definitely-wrong-xyz');\n cy.get('input[name=\"newPassword\"]').type('newSecretPass123');\n cy.get('input[name=\"confirmPassword\"]').type('newSecretPass123');\n cy.get('form button[type=\"submit\"]').should('not.be.disabled').click();\n // Server rejects (htpasswd → plain Error → handler returns 4xx).\n cy.wait('@reset').its('response.statusCode').should('not.eq', 200);\n // onSubmit's catch sets errors.root → rendered via LoginDialogFormError.\n cy.getByTestId(login.error, { timeout: 5000 })\n .should('be.visible')\n .and('contain.text', GENERIC_FAILURE_TEXT);\n // Still on the change-password page so the user can retry.\n cy.location('pathname').should('include', CHANGE_PASSWORD_PATH);\n }\n );\n\n // ── Happy path (mutates state; keeps `currentPassword` in sync) ─\n\n maybeIt(features.changePassword.happyPath)(\n 'should change the password and navigate to the success page',\n () => {\n const newPassword = `${currentPassword}-rotated`;\n cy.intercept('PUT', '/-/verdaccio/sec/reset_password').as('reset');\n\n cy.get('input[name=\"username\"]').type(user);\n cy.get('input[name=\"oldPassword\"]').type(currentPassword);\n cy.get('input[name=\"newPassword\"]').type(newPassword);\n cy.get('input[name=\"confirmPassword\"]').type(newPassword);\n cy.get('form button[type=\"submit\"]').should('not.be.disabled').click();\n\n cy.wait('@reset').its('response.statusCode').should('eq', 200);\n // Post-submit the component navigates to Route.SUCCESS with a\n // messageType query param. Assert the pathname; the message text\n // is i18n-driven and out of scope for this selector layer.\n cy.location('pathname', { timeout: 5000 }).should('include', '/-/web/success');\n\n // Track the rotation so `after()` can restore it.\n currentPassword = newPassword;\n }\n );\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"change-password.js","names":[],"sources":["../../../src/tests/change-password.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Tests for the Change Password page at /-/web/change-password.\n *\n * The page renders only when the server is started with\n * `flags.changePassword: true` (otherwise the React component\n * redirects to `/` on mount). Each test logs in first, navigates\n * directly to the page, and drives the form.\n *\n * Selector strategy: the ChangePassword form does not ship stable\n * `id`/testid attributes on its inputs, but every field is registered\n * via react-hook-form's `register('<name>')`, which sets a stable\n * `name` attribute on the underlying `<input>`. The labels themselves\n * are `t('security.changePassword.*')` calls — when the i18n bundle\n * hasn't finished loading (or isn't loaded at all on this route), MUI\n * renders the literal i18n key as the label, so any selector that\n * matches on visible label text silently misses every field and the\n * form stays empty (which would also make the mismatch test \"pass\"\n * for the wrong reason: the submit button is disabled because the\n * form is empty, not because yup rejected the mismatch).\n *\n * The stable, i18n-independent contract from ChangePassword.tsx is:\n * register('username') → input[name=\"username\"]\n * register('oldPassword') → input[name=\"oldPassword\"]\n * register('newPassword') → input[name=\"newPassword\"]\n * register('confirmPassword') → input[name=\"confirmPassword\"]\n * submit button → form button[type=\"submit\"]\n */\nexport function changePasswordTests(config: RegistryConfig) {\n const { header, login } = config.testIds;\n const { loginDialog } = config.selectors;\n const { features } = config;\n\n // The wrongOldPassword banner text differs across verdaccio lines: older\n // published ui-theme shows a hardcoded \"Failed to change password\", while\n // current master surfaces the server message (\"internal server error\") or\n // the i18n fallback (\"Unable to change password\") via authErrorMessage. The\n // contract this test pins is \"a non-empty error banner appears, not\n // silence\", so it asserts visibility + non-empty text rather than an exact\n // string. The Express \"Cannot PUT\" 404 body is the one text that must never\n // appear — that means the reset_password route was not registered (the\n // server was started without flags.changePassword).\n\n // the form asks for the username only on released UIs; 9.x takes it from the session\n const typeUsernameIfPresent = (user: string) => {\n cy.get('input[name=\"oldPassword\"]').should('exist');\n cy.get('form').then(($form) => {\n const $username = $form.find('input[name=\"username\"]');\n if ($username.length) cy.wrap($username).type(user);\n });\n };\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 typeUsernameIfPresent(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 typeUsernameIfPresent(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 when the old password is wrong',\n () => {\n cy.intercept('PUT', '/-/verdaccio/sec/reset_password').as('reset');\n typeUsernameIfPresent(user);\n cy.get('input[name=\"oldPassword\"]').type('definitely-wrong-xyz');\n cy.get('input[name=\"newPassword\"]').type('newSecretPass123');\n cy.get('input[name=\"confirmPassword\"]').type('newSecretPass123');\n cy.get('form button[type=\"submit\"]').should('not.be.disabled').click();\n // Server rejects the wrong password (the route exists → not a 404).\n cy.wait('@reset').its('response.statusCode').should('not.eq', 200);\n // onSubmit's catch sets errors.root → rendered via LoginDialogFormError.\n // Assert a non-empty banner (not silence), text-agnostic across lines,\n // but never the raw \"Cannot PUT\" 404 (route missing).\n // released UIs show a banner; 9.x flags the old password field instead\n cy.get('form', { timeout: 5000 }).should(($form) => {\n const $banner = $form.find(`[data-testid=\"${login.error}\"]`);\n const $invalidField = $form.find('input[name=\"oldPassword\"][aria-invalid=\"true\"]');\n expect($banner.length + $invalidField.length, 'error shown').to.be.greaterThan(0);\n const message = $banner.length\n ? $banner.text()\n : $invalidField.closest('.MuiFormControl-root').find('.MuiFormHelperText-root').text();\n expect(message.trim()).to.not.be.empty;\n expect(message).to.not.contain('Cannot PUT');\n });\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 typeUsernameIfPresent(user);\n cy.get('input[name=\"oldPassword\"]').type(currentPassword);\n cy.get('input[name=\"newPassword\"]').type(newPassword);\n cy.get('input[name=\"confirmPassword\"]').type(newPassword);\n cy.get('form button[type=\"submit\"]').should('not.be.disabled').click();\n\n cy.wait('@reset').its('response.statusCode').should('eq', 200);\n // Post-submit the component navigates to Route.SUCCESS with a\n // messageType query param. Assert the pathname; the message text\n // is i18n-driven and out of scope for this selector layer.\n cy.location('pathname', { timeout: 5000 }).should('include', '/-/web/success');\n\n // Track the rotation so `after()` can restore it.\n currentPassword = newPassword;\n }\n );\n });\n}\n"],"mappings":"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"}
|
|
@@ -56,6 +56,39 @@ function detailTests(config) {
|
|
|
56
56
|
cy.getByTestId(pkg.sidebar).should("be.visible");
|
|
57
57
|
cy.getByTestId(pkg.sidebar).contains(version);
|
|
58
58
|
});
|
|
59
|
+
maybeIt(features.detail.staleVersionsNavigation)("the versions tab must show the CURRENT package after SPA navigation", () => {
|
|
60
|
+
const otherPkg = "@verdaccio/stale-nav-fixture";
|
|
61
|
+
let otherTemp = null;
|
|
62
|
+
cy.task("publishPackage", {
|
|
63
|
+
pkgName: otherPkg,
|
|
64
|
+
version: "2.0.0",
|
|
65
|
+
unique: true
|
|
66
|
+
}).then((result) => {
|
|
67
|
+
otherTemp = result?.tempFolder ?? null;
|
|
68
|
+
});
|
|
69
|
+
cy.task("publishPackage", {
|
|
70
|
+
pkgName,
|
|
71
|
+
version: "1.1.0",
|
|
72
|
+
unique: true
|
|
73
|
+
}).then((r) => {
|
|
74
|
+
if (r?.tempFolder) cy.task("cleanupPublished", r.tempFolder);
|
|
75
|
+
});
|
|
76
|
+
cy.visit(config.registryUrl);
|
|
77
|
+
cy.contains(`[data-testid="${pkg.title}"]`, pkgName).click();
|
|
78
|
+
cy.getByTestId(pkg.versionsTab, { timeout: 1e4 }).click();
|
|
79
|
+
cy.get("[data-testid=\"version-list-text\"]").should("have.length", 2);
|
|
80
|
+
cy.getByTestId(config.testIds.header.defaultLogo).first().click();
|
|
81
|
+
cy.contains(`[data-testid="${pkg.title}"]`, otherPkg).click();
|
|
82
|
+
cy.getByTestId(pkg.versionsTab, { timeout: 1e4 }).click();
|
|
83
|
+
cy.get("[data-testid=\"version-list-text\"]").should("have.length", 1);
|
|
84
|
+
cy.task("unpublishPackage", {
|
|
85
|
+
pkgName: otherPkg,
|
|
86
|
+
tempFolder: otherTemp ?? void 0
|
|
87
|
+
});
|
|
88
|
+
cy.then(() => {
|
|
89
|
+
if (otherTemp) cy.task("cleanupPublished", otherTemp);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
59
92
|
maybeIt(features.detail.versionNotFound)("should render Not Found for a version that does not exist", () => {
|
|
60
93
|
cy.intercept("GET", `/-/verdaccio/data/sidebar/${pkgName}*`).as("sidebar");
|
|
61
94
|
cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}/v/9.9.9`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detail.js","names":[],"sources":["../../../src/tests/detail.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Package detail page tests: version-pinned URLs and the wire format of\n * the UI's data requests.\n *\n * The wire format is an observable contract, not an implementation\n * detail: proxies, intercepts and every npm client expect scoped names\n * as literal `/@scope/name`. A ui-components refactor that\n * percent-encoded the `@` broke exactly these suites\n * (verdaccio/verdaccio#6210), which is why it is pinned here on purpose.\n */\nexport function detailTests(config: RegistryConfig) {\n const { features } = config;\n const { package: pkg, home } = config.testIds;\n\n describe('package detail', () => {\n describe('with a published package', () => {\n const pkgName = '@verdaccio/detail-fixture';\n let tempFolder: string | null = null;\n // `unique: true` uniquifies the VERSION (1.0.0-t<ts>), not the name\n let version = '1.0.0';\n\n beforeEach(() => {\n cy.task('publishPackage', {\n pkgName,\n version: '1.0.0',\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n version = result?.version ?? '1.0.0';\n });\n });\n\n afterEach(() => {\n cy.task('unpublishPackage', {\n pkgName,\n tempFolder: tempFolder ?? undefined,\n });\n if (tempFolder) {\n cy.task('cleanupPublished', tempFolder);\n }\n tempFolder = null;\n });\n\n maybeIt(features.detail.scopedWireFormat)(\n 'should request sidebar and readme with the scope literal (@, not %40)',\n () => {\n // Literal-path intercepts: if the UI ever encodes `@` or `/`,\n // these never match and the waits below time out.\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}*`).as('sidebar');\n cy.intercept('GET', `/-/verdaccio/data/package/readme/${pkgName}*`).as('readme');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);\n\n cy.wait('@sidebar', { timeout: 10000 }).then((interception: any) => {\n expect(interception.request.url).to.contain(`/sidebar/${pkgName}`);\n expect(interception.request.url).to.not.contain('%40');\n });\n cy.wait('@readme', { timeout: 10000 });\n cy.getByTestId(pkg.sidebar).should('be.visible');\n }\n );\n\n maybeIt(features.detail.versionPinned)(\n 'should pin the requested version via the ?v= query on /v/<version> urls',\n () => {\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}*`).as('sidebar');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}/v/${version}`);\n\n cy.wait('@sidebar', { timeout: 10000 }).then((interception: any) => {\n expect(interception.request.url).to.contain(`v=${version}`);\n });\n cy.getByTestId(pkg.sidebar).should('be.visible');\n cy.getByTestId(pkg.sidebar).contains(version);\n }\n );\n\n maybeIt(features.detail.versionNotFound)(\n 'should render Not Found for a version that does not exist',\n () => {\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}*`).as('sidebar');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}/v/9.9.9`);\n\n // The server answers 404 for unknown versions instead of\n // silently serving `latest` under the requested title.\n cy.wait('@sidebar', { timeout: 10000 }).then((interception: any) => {\n expect(interception.response?.statusCode).to.eq(404);\n });\n cy.getByTestId(home.notFound, { timeout: 10000 }).should('be.visible');\n }\n );\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;AAcA,SAAgB,YAAY,QAAwB;CAClD,MAAM,EAAE,aAAa;CACrB,MAAM,EAAE,SAAS,KAAK,SAAS,OAAO;CAEtC,SAAS,wBAAwB;EAC/B,SAAS,kCAAkC;GACzC,MAAM,UAAU;GAChB,IAAI,aAA4B;GAEhC,IAAI,UAAU;GAEd,iBAAiB;IACf,GAAG,KAAK,kBAAkB;KACxB;KACA,SAAS;KACT,QAAQ;IACV,CAAC,CAAC,CAAC,MAAM,WAAW;KAClB,aAAa,QAAQ,cAAc;KACnC,UAAU,QAAQ,WAAW;IAC/B,CAAC;GACH,CAAC;GAED,gBAAgB;IACd,GAAG,KAAK,oBAAoB;KAC1B;KACA,YAAY,cAAc,KAAA;IAC5B,CAAC;IACD,IAAI,YACF,GAAG,KAAK,oBAAoB,UAAU;IAExC,aAAa;GACf,CAAC;GAED,QAAQ,SAAS,OAAO,gBAAgB,CAAC,CACvC,+EACM;IAGJ,GAAG,UAAU,OAAO,6BAA6B,QAAQ,EAAE,CAAC,CAAC,GAAG,SAAS;IACzE,GAAG,UAAU,OAAO,oCAAoC,QAAQ,EAAE,CAAC,CAAC,GAAG,QAAQ;IAE/E,GAAG,MAAM,GAAG,OAAO,YAAY,gBAAgB,SAAS;IAExD,GAAG,KAAK,YAAY,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,MAAM,iBAAsB;KAClE,OAAO,aAAa,QAAQ,GAAG,CAAC,CAAC,GAAG,QAAQ,YAAY,SAAS;KACjE,OAAO,aAAa,QAAQ,GAAG,CAAC,CAAC,GAAG,IAAI,QAAQ,KAAK;IACvD,CAAC;IACD,GAAG,KAAK,WAAW,EAAE,SAAS,IAAM,CAAC;IACrC,GAAG,YAAY,IAAI,OAAO,CAAC,CAAC,OAAO,YAAY;GACjD,CACF;GAEA,QAAQ,SAAS,OAAO,aAAa,CAAC,CACpC,iFACM;IACJ,GAAG,UAAU,OAAO,6BAA6B,QAAQ,EAAE,CAAC,CAAC,GAAG,SAAS;IAEzE,GAAG,MAAM,GAAG,OAAO,YAAY,gBAAgB,QAAQ,KAAK,SAAS;IAErE,GAAG,KAAK,YAAY,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,MAAM,iBAAsB;KAClE,OAAO,aAAa,QAAQ,GAAG,CAAC,CAAC,GAAG,QAAQ,KAAK,SAAS;IAC5D,CAAC;IACD,GAAG,YAAY,IAAI,OAAO,CAAC,CAAC,OAAO,YAAY;IAC/C,GAAG,YAAY,IAAI,OAAO,CAAC,CAAC,SAAS,OAAO;GAC9C,CACF;GAEA,QAAQ,SAAS,OAAO,eAAe,CAAC,CACtC,mEACM;IACJ,GAAG,UAAU,OAAO,6BAA6B,QAAQ,EAAE,CAAC,CAAC,GAAG,SAAS;IAEzE,GAAG,MAAM,GAAG,OAAO,YAAY,gBAAgB,QAAQ,SAAS;IAIhE,GAAG,KAAK,YAAY,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,MAAM,iBAAsB;KAClE,OAAO,aAAa,UAAU,UAAU,CAAC,CAAC,GAAG,GAAG,GAAG;IACrD,CAAC;IACD,GAAG,YAAY,KAAK,UAAU,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,OAAO,YAAY;GACvE,CACF;EACF,CAAC;CACH,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"detail.js","names":[],"sources":["../../../src/tests/detail.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Package detail page tests: version-pinned URLs and the wire format of\n * the UI's data requests.\n *\n * The wire format is an observable contract, not an implementation\n * detail: proxies, intercepts and every npm client expect scoped names\n * as literal `/@scope/name`. A ui-components refactor that\n * percent-encoded the `@` broke exactly these suites\n * (verdaccio/verdaccio#6210), which is why it is pinned here on purpose.\n */\nexport function detailTests(config: RegistryConfig) {\n const { features } = config;\n const { package: pkg, home } = config.testIds;\n\n describe('package detail', () => {\n describe('with a published package', () => {\n const pkgName = '@verdaccio/detail-fixture';\n let tempFolder: string | null = null;\n // `unique: true` uniquifies the VERSION (1.0.0-t<ts>), not the name\n let version = '1.0.0';\n\n beforeEach(() => {\n cy.task('publishPackage', {\n pkgName,\n version: '1.0.0',\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n version = result?.version ?? '1.0.0';\n });\n });\n\n afterEach(() => {\n cy.task('unpublishPackage', {\n pkgName,\n tempFolder: tempFolder ?? undefined,\n });\n if (tempFolder) {\n cy.task('cleanupPublished', tempFolder);\n }\n tempFolder = null;\n });\n\n maybeIt(features.detail.scopedWireFormat)(\n 'should request sidebar and readme with the scope literal (@, not %40)',\n () => {\n // Literal-path intercepts: if the UI ever encodes `@` or `/`,\n // these never match and the waits below time out.\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}*`).as('sidebar');\n cy.intercept('GET', `/-/verdaccio/data/package/readme/${pkgName}*`).as('readme');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);\n\n cy.wait('@sidebar', { timeout: 10000 }).then((interception: any) => {\n expect(interception.request.url).to.contain(`/sidebar/${pkgName}`);\n expect(interception.request.url).to.not.contain('%40');\n });\n cy.wait('@readme', { timeout: 10000 });\n cy.getByTestId(pkg.sidebar).should('be.visible');\n }\n );\n\n maybeIt(features.detail.versionPinned)(\n 'should pin the requested version via the ?v= query on /v/<version> urls',\n () => {\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}*`).as('sidebar');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}/v/${version}`);\n\n cy.wait('@sidebar', { timeout: 10000 }).then((interception: any) => {\n expect(interception.request.url).to.contain(`v=${version}`);\n });\n cy.getByTestId(pkg.sidebar).should('be.visible');\n cy.getByTestId(pkg.sidebar).contains(version);\n }\n );\n\n maybeIt(features.detail.staleVersionsNavigation)(\n 'the versions tab must show the CURRENT package after SPA navigation',\n () => {\n // second fixture with a different version count; the detail\n // routes reuse one mounted component, so navigating A -> B\n // must not leak A's cached versions into B's tab\n const otherPkg = '@verdaccio/stale-nav-fixture';\n let otherTemp: string | null = null;\n cy.task('publishPackage', { pkgName: otherPkg, version: '2.0.0', unique: true }).then(\n (result) => {\n otherTemp = result?.tempFolder ?? null;\n }\n );\n // give the first fixture a second version so the counts differ\n cy.task('publishPackage', { pkgName, version: '1.1.0', unique: true }).then((r) => {\n if (r?.tempFolder) cy.task('cleanupPublished', r.tempFolder);\n });\n\n cy.visit(config.registryUrl);\n cy.contains(`[data-testid=\"${pkg.title}\"]`, pkgName).click();\n cy.getByTestId(pkg.versionsTab, { timeout: 10000 }).click();\n cy.get('[data-testid=\"version-list-text\"]').should('have.length', 2);\n\n // SPA navigation back home via the logo (no full reload);\n // the logo can render more than once (desktop + mobile nav)\n cy.getByTestId(config.testIds.header.defaultLogo).first().click();\n cy.contains(`[data-testid=\"${pkg.title}\"]`, otherPkg).click();\n cy.getByTestId(pkg.versionsTab, { timeout: 10000 }).click();\n // stale-state bug: B showed A's list (length 2) here\n cy.get('[data-testid=\"version-list-text\"]').should('have.length', 1);\n\n cy.task('unpublishPackage', { pkgName: otherPkg, tempFolder: otherTemp ?? undefined });\n cy.then(() => {\n if (otherTemp) cy.task('cleanupPublished', otherTemp);\n });\n }\n );\n\n maybeIt(features.detail.versionNotFound)(\n 'should render Not Found for a version that does not exist',\n () => {\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}*`).as('sidebar');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}/v/9.9.9`);\n\n // The server answers 404 for unknown versions instead of\n // silently serving `latest` under the requested title.\n cy.wait('@sidebar', { timeout: 10000 }).then((interception: any) => {\n expect(interception.response?.statusCode).to.eq(404);\n });\n cy.getByTestId(home.notFound, { timeout: 10000 }).should('be.visible');\n }\n );\n });\n });\n}\n"],"mappings":"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"}
|
|
@@ -13,6 +13,17 @@ function failureModeTests(config) {
|
|
|
13
13
|
const { features } = config;
|
|
14
14
|
const { home, header, errors, package: pkg } = config.testIds;
|
|
15
15
|
describe("failure modes", () => {
|
|
16
|
+
maybeIt(features.failureModes.searchError)("a search 5xx must show an error, not \"no results found\"", () => {
|
|
17
|
+
cy.intercept("GET", "**/-/verdaccio/data/search/**", {
|
|
18
|
+
statusCode: 500,
|
|
19
|
+
body: { error: "internal server error" }
|
|
20
|
+
}).as("search");
|
|
21
|
+
cy.visit(config.registryUrl);
|
|
22
|
+
cy.getByTestId(header.searchContainer).find("input").type("anything", { delay: 20 });
|
|
23
|
+
cy.wait("@search", { timeout: 1e4 });
|
|
24
|
+
cy.contains("Something went wrong while searching", { timeout: 1e4 }).should("be.visible");
|
|
25
|
+
cy.contains("No results found").should("not.exist");
|
|
26
|
+
});
|
|
16
27
|
maybeIt(features.failureModes.homeServerError)("a 5xx on the package list must not render the empty-registry onboarding", () => {
|
|
17
28
|
cy.intercept("GET", "/-/verdaccio/data/packages", {
|
|
18
29
|
statusCode: 500,
|
|
@@ -51,6 +62,17 @@ function failureModeTests(config) {
|
|
|
51
62
|
if (tempFolder) cy.task("cleanupPublished", tempFolder);
|
|
52
63
|
tempFolder = null;
|
|
53
64
|
});
|
|
65
|
+
maybeIt(features.failureModes.downloadError)("a failed tarball download must show an error snackbar, not silence", () => {
|
|
66
|
+
cy.intercept("GET", "**/*.tgz*", {
|
|
67
|
+
statusCode: 500,
|
|
68
|
+
body: "boom"
|
|
69
|
+
}).as("tarball");
|
|
70
|
+
cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);
|
|
71
|
+
cy.getByTestId(pkg.sidebar, { timeout: 1e4 }).should("be.visible");
|
|
72
|
+
cy.getByTestId(pkg.downloadTarballBtn).click();
|
|
73
|
+
cy.wait("@tarball", { timeout: 1e4 });
|
|
74
|
+
cy.contains("The tarball could not be downloaded", { timeout: 1e4 }).should("be.visible");
|
|
75
|
+
});
|
|
54
76
|
maybeIt(features.failureModes.detailErrorState)("a 5xx on the detail data must render an error state, not a blank page", () => {
|
|
55
77
|
cy.intercept("GET", `/-/verdaccio/data/sidebar/${pkgName}*`, {
|
|
56
78
|
statusCode: 500,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"failure-modes.js","names":[],"sources":["../../../src/tests/failure-modes.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Failure-mode tests: what the UI shows when the backend misbehaves.\n *\n * A registry that is down must never look like a healthy empty registry\n * (that invites the user to publish into the void), and a failed detail\n * request must render an error state instead of a blank page. These are\n * the failure classes fixed by verdaccio/verdaccio#6210 — the strict\n * assertions are feature-gated probes until that ships.\n */\nexport function failureModeTests(config: RegistryConfig) {\n const { features } = config;\n const { home, header, errors, package: pkg } = config.testIds;\n\n describe('failure modes', () => {\n maybeIt(features.failureModes.homeServerError)(\n 'a 5xx on the package list must not render the empty-registry onboarding',\n () => {\n cy.intercept('GET', '/-/verdaccio/data/packages', {\n statusCode: 500,\n body: { error: 'internal server error' },\n }).as('pkgs');\n\n cy.visit(config.registryUrl);\n cy.wait('@pkgs', { timeout: 10000 });\n\n // The header must survive and the onboarding card must not\n // appear — a dead backend is not an empty registry.\n cy.getByTestId(header.container).should('be.visible');\n cy.getByTestId(home.helpCard).should('not.exist');\n }\n );\n\n maybeIt(features.failureModes.homeNetworkError)(\n 'an unreachable backend must not render the empty-registry onboarding',\n () => {\n cy.intercept('GET', '/-/verdaccio/data/packages', { forceNetworkError: true }).as('pkgs');\n\n cy.visit(config.registryUrl);\n cy.wait('@pkgs', { timeout: 10000 });\n\n cy.getByTestId(header.container).should('be.visible');\n cy.getByTestId(home.helpCard).should('not.exist');\n cy.getByTestId(errors.genericError, { timeout: 10000 }).should('be.visible');\n }\n );\n\n describe('with a published package', () => {\n const pkgName = '@verdaccio/failure-fixture';\n let tempFolder: string | null = null;\n\n beforeEach(() => {\n cy.task('publishPackage', {\n pkgName,\n version: '1.0.0',\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n });\n });\n\n afterEach(() => {\n cy.task('unpublishPackage', {\n pkgName,\n tempFolder: tempFolder ?? undefined,\n });\n if (tempFolder) {\n cy.task('cleanupPublished', tempFolder);\n }\n tempFolder = null;\n });\n\n maybeIt(features.failureModes.detailErrorState)(\n 'a 5xx on the detail data must render an error state, not a blank page',\n () => {\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}*`, {\n statusCode: 500,\n body: { error: 'internal server error' },\n }).as('sidebar');\n cy.intercept('GET', `/-/verdaccio/data/package/readme/${pkgName}*`, {\n statusCode: 500,\n body: { error: 'internal server error' },\n }).as('readme');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);\n cy.wait('@sidebar', { timeout: 10000 });\n\n // Error page instead of a blank layout whose tabs crash the\n // whole app; the header must survive.\n cy.getByTestId(errors.genericError, { timeout: 10000 }).should('be.visible');\n cy.getByTestId(header.container).should('be.visible');\n cy.getByTestId(pkg.sidebar).should('not.exist');\n }\n );\n });\n });\n}\n"],"mappings":";;;;;;;;;;;AAaA,SAAgB,iBAAiB,QAAwB;CACvD,MAAM,EAAE,aAAa;CACrB,MAAM,EAAE,MAAM,QAAQ,QAAQ,SAAS,QAAQ,OAAO;CAEtD,SAAS,uBAAuB;EAC9B,QAAQ,SAAS,aAAa,eAAe,CAAC,CAC5C,iFACM;GACJ,GAAG,UAAU,OAAO,8BAA8B;IAChD,YAAY;IACZ,MAAM,EAAE,OAAO,wBAAwB;GACzC,CAAC,CAAC,CAAC,GAAG,MAAM;GAEZ,GAAG,MAAM,OAAO,WAAW;GAC3B,GAAG,KAAK,SAAS,EAAE,SAAS,IAAM,CAAC;GAInC,GAAG,YAAY,OAAO,SAAS,CAAC,CAAC,OAAO,YAAY;GACpD,GAAG,YAAY,KAAK,QAAQ,CAAC,CAAC,OAAO,WAAW;EAClD,CACF;EAEA,QAAQ,SAAS,aAAa,gBAAgB,CAAC,CAC7C,8EACM;GACJ,GAAG,UAAU,OAAO,8BAA8B,EAAE,mBAAmB,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM;GAExF,GAAG,MAAM,OAAO,WAAW;GAC3B,GAAG,KAAK,SAAS,EAAE,SAAS,IAAM,CAAC;GAEnC,GAAG,YAAY,OAAO,SAAS,CAAC,CAAC,OAAO,YAAY;GACpD,GAAG,YAAY,KAAK,QAAQ,CAAC,CAAC,OAAO,WAAW;GAChD,GAAG,YAAY,OAAO,cAAc,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,OAAO,YAAY;EAC7E,CACF;EAEA,SAAS,kCAAkC;GACzC,MAAM,UAAU;GAChB,IAAI,aAA4B;GAEhC,iBAAiB;IACf,GAAG,KAAK,kBAAkB;KACxB;KACA,SAAS;KACT,QAAQ;IACV,CAAC,CAAC,CAAC,MAAM,WAAW;KAClB,aAAa,QAAQ,cAAc;IACrC,CAAC;GACH,CAAC;GAED,gBAAgB;IACd,GAAG,KAAK,oBAAoB;KAC1B;KACA,YAAY,cAAc,KAAA;IAC5B,CAAC;IACD,IAAI,YACF,GAAG,KAAK,oBAAoB,UAAU;IAExC,aAAa;GACf,CAAC;GAED,QAAQ,SAAS,aAAa,gBAAgB,CAAC,CAC7C,+EACM;IACJ,GAAG,UAAU,OAAO,6BAA6B,QAAQ,IAAI;KAC3D,YAAY;KACZ,MAAM,EAAE,OAAO,wBAAwB;IACzC,CAAC,CAAC,CAAC,GAAG,SAAS;IACf,GAAG,UAAU,OAAO,oCAAoC,QAAQ,IAAI;KAClE,YAAY;KACZ,MAAM,EAAE,OAAO,wBAAwB;IACzC,CAAC,CAAC,CAAC,GAAG,QAAQ;IAEd,GAAG,MAAM,GAAG,OAAO,YAAY,gBAAgB,SAAS;IACxD,GAAG,KAAK,YAAY,EAAE,SAAS,IAAM,CAAC;IAItC,GAAG,YAAY,OAAO,cAAc,EAAE,SAAS,IAAM,CAAC,CAAC,CAAC,OAAO,YAAY;IAC3E,GAAG,YAAY,OAAO,SAAS,CAAC,CAAC,OAAO,YAAY;IACpD,GAAG,YAAY,IAAI,OAAO,CAAC,CAAC,OAAO,WAAW;GAChD,CACF;EACF,CAAC;CACH,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"failure-modes.js","names":[],"sources":["../../../src/tests/failure-modes.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Failure-mode tests: what the UI shows when the backend misbehaves.\n *\n * A registry that is down must never look like a healthy empty registry\n * (that invites the user to publish into the void), and a failed detail\n * request must render an error state instead of a blank page. These are\n * the failure classes fixed by verdaccio/verdaccio#6210 — the strict\n * assertions are feature-gated probes until that ships.\n */\nexport function failureModeTests(config: RegistryConfig) {\n const { features } = config;\n const { home, header, errors, package: pkg } = config.testIds;\n\n describe('failure modes', () => {\n maybeIt(features.failureModes.searchError)(\n 'a search 5xx must show an error, not \"no results found\"',\n () => {\n cy.intercept('GET', '**/-/verdaccio/data/search/**', {\n statusCode: 500,\n body: { error: 'internal server error' },\n }).as('search');\n\n cy.visit(config.registryUrl);\n cy.getByTestId(header.searchContainer).find('input').type('anything', { delay: 20 });\n cy.wait('@search', { timeout: 10000 });\n\n // en-US bundle: autoComplete.error\n cy.contains('Something went wrong while searching', { timeout: 10000 }).should(\n 'be.visible'\n );\n cy.contains('No results found').should('not.exist');\n }\n );\n\n maybeIt(features.failureModes.homeServerError)(\n 'a 5xx on the package list must not render the empty-registry onboarding',\n () => {\n cy.intercept('GET', '/-/verdaccio/data/packages', {\n statusCode: 500,\n body: { error: 'internal server error' },\n }).as('pkgs');\n\n cy.visit(config.registryUrl);\n cy.wait('@pkgs', { timeout: 10000 });\n\n // The header must survive and the onboarding card must not\n // appear — a dead backend is not an empty registry.\n cy.getByTestId(header.container).should('be.visible');\n cy.getByTestId(home.helpCard).should('not.exist');\n }\n );\n\n maybeIt(features.failureModes.homeNetworkError)(\n 'an unreachable backend must not render the empty-registry onboarding',\n () => {\n cy.intercept('GET', '/-/verdaccio/data/packages', { forceNetworkError: true }).as('pkgs');\n\n cy.visit(config.registryUrl);\n cy.wait('@pkgs', { timeout: 10000 });\n\n cy.getByTestId(header.container).should('be.visible');\n cy.getByTestId(home.helpCard).should('not.exist');\n cy.getByTestId(errors.genericError, { timeout: 10000 }).should('be.visible');\n }\n );\n\n describe('with a published package', () => {\n const pkgName = '@verdaccio/failure-fixture';\n let tempFolder: string | null = null;\n\n beforeEach(() => {\n cy.task('publishPackage', {\n pkgName,\n version: '1.0.0',\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n });\n });\n\n afterEach(() => {\n cy.task('unpublishPackage', {\n pkgName,\n tempFolder: tempFolder ?? undefined,\n });\n if (tempFolder) {\n cy.task('cleanupPublished', tempFolder);\n }\n tempFolder = null;\n });\n\n maybeIt(features.failureModes.downloadError)(\n 'a failed tarball download must show an error snackbar, not silence',\n () => {\n cy.intercept('GET', '**/*.tgz*', {\n statusCode: 500,\n body: 'boom',\n }).as('tarball');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);\n cy.getByTestId(pkg.sidebar, { timeout: 10000 }).should('be.visible');\n cy.getByTestId(pkg.downloadTarballBtn).click();\n cy.wait('@tarball', { timeout: 10000 });\n\n // en-US bundle: error.download-tarball\n cy.contains('The tarball could not be downloaded', { timeout: 10000 }).should(\n 'be.visible'\n );\n }\n );\n\n maybeIt(features.failureModes.detailErrorState)(\n 'a 5xx on the detail data must render an error state, not a blank page',\n () => {\n cy.intercept('GET', `/-/verdaccio/data/sidebar/${pkgName}*`, {\n statusCode: 500,\n body: { error: 'internal server error' },\n }).as('sidebar');\n cy.intercept('GET', `/-/verdaccio/data/package/readme/${pkgName}*`, {\n statusCode: 500,\n body: { error: 'internal server error' },\n }).as('readme');\n\n cy.visit(`${config.registryUrl}/-/web/detail/${pkgName}`);\n cy.wait('@sidebar', { timeout: 10000 });\n\n // Error page instead of a blank layout whose tabs crash the\n // whole app; the header must survive.\n cy.getByTestId(errors.genericError, { timeout: 10000 }).should('be.visible');\n cy.getByTestId(header.container).should('be.visible');\n cy.getByTestId(pkg.sidebar).should('not.exist');\n }\n );\n });\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { maybeIt } from "../features.js";
|
|
2
|
+
//#region src/tests/home-detail-ux.ts
|
|
3
|
+
/**
|
|
4
|
+
* Home and package page UX that ships with the 9.x web UI: the package
|
|
5
|
+
* list toolbar, the tab in the address, the welcome guide, the install
|
|
6
|
+
* box (bun and `.npmrc`), copy feedback and search grouped by origin.
|
|
7
|
+
*
|
|
8
|
+
* Every test is a probe (flag defaults to `false`): released lines do
|
|
9
|
+
* not have these features yet.
|
|
10
|
+
*/
|
|
11
|
+
function homeDetailUxTests(config) {
|
|
12
|
+
const { features } = config;
|
|
13
|
+
const flags = features.homeDetailUx;
|
|
14
|
+
const { header, home, package: pkg, search } = config.testIds;
|
|
15
|
+
describe("home and detail ux", () => {
|
|
16
|
+
describe("with an empty registry", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
cy.intercept("GET", "/-/verdaccio/data/packages").as("pkgs");
|
|
19
|
+
cy.visit(config.registryUrl);
|
|
20
|
+
cy.wait("@pkgs");
|
|
21
|
+
});
|
|
22
|
+
maybeIt(flags.welcomeGuide)("should guide the first publish with a setup tab per package manager", () => {
|
|
23
|
+
cy.getByTestId(home.helpCard, { timeout: 1e4 }).should("be.visible");
|
|
24
|
+
cy.getByTestId(home.helpCard).contains("No Package Published Yet.");
|
|
25
|
+
cy.getByTestId(home.helpCard).contains(`npm adduser --registry ${config.registryUrl}`);
|
|
26
|
+
cy.getByTestId(home.helpCard).contains(`npm publish --registry ${config.registryUrl}`);
|
|
27
|
+
cy.getByTestId(`${home.helpSetupTabPrefix}npm`).should("be.visible");
|
|
28
|
+
cy.getByTestId(home.helpCard).contains(`npm config set registry ${config.registryUrl}`);
|
|
29
|
+
cy.get(`[data-testid="${home.helpSetupTabPrefix}.npmrc"]`).click();
|
|
30
|
+
cy.getByTestId(home.helpCard).contains(`registry=${config.registryUrl}`);
|
|
31
|
+
cy.getByTestId(home.helpUplinksNote).should("be.visible");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe("with packages from two scopes", () => {
|
|
35
|
+
const scopedPkg = "@verdaccio/ux-toolbar-fixture";
|
|
36
|
+
const unscopedPkg = "ux-toolbar-unscoped-fixture";
|
|
37
|
+
const tempFolders = [];
|
|
38
|
+
before(() => {
|
|
39
|
+
if (!flags.homeToolbar) return;
|
|
40
|
+
[scopedPkg, unscopedPkg].forEach((pkgName) => {
|
|
41
|
+
cy.task("publishPackage", {
|
|
42
|
+
pkgName,
|
|
43
|
+
version: "1.0.0",
|
|
44
|
+
unique: true
|
|
45
|
+
}).then((r) => {
|
|
46
|
+
if (r?.tempFolder) tempFolders.push(r.tempFolder);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
after(() => {
|
|
51
|
+
if (!flags.homeToolbar) return;
|
|
52
|
+
[scopedPkg, unscopedPkg].forEach((pkgName) => cy.task("unpublishPackage", pkgName));
|
|
53
|
+
cy.then(() => tempFolders.forEach((folder) => cy.task("cleanupPublished", folder)));
|
|
54
|
+
});
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
cy.intercept("GET", "/-/verdaccio/data/packages").as("pkgs");
|
|
57
|
+
});
|
|
58
|
+
maybeIt(flags.homeToolbar)("should filter the list and keep the filter in ?q=", () => {
|
|
59
|
+
cy.visit(config.registryUrl);
|
|
60
|
+
cy.wait("@pkgs");
|
|
61
|
+
cy.getByTestId(home.packageListToolbar, { timeout: 1e4 }).should("be.visible");
|
|
62
|
+
cy.getByTestId(pkg.title).should("have.length", 2);
|
|
63
|
+
cy.getByTestId(home.packageListFilter).type("unscoped", { delay: 20 });
|
|
64
|
+
cy.getByTestId(pkg.title).should("have.length", 1).and("contain", unscopedPkg);
|
|
65
|
+
cy.location("search").should("eq", "?q=unscoped");
|
|
66
|
+
cy.reload();
|
|
67
|
+
cy.getByTestId(home.packageListFilter, { timeout: 1e4 }).should("have.value", "unscoped");
|
|
68
|
+
cy.getByTestId(pkg.title).should("have.length", 1);
|
|
69
|
+
});
|
|
70
|
+
maybeIt(flags.homeToolbar)("should restore the scope from ?scope=", () => {
|
|
71
|
+
cy.visit(`${config.registryUrl}/?scope=@verdaccio`);
|
|
72
|
+
cy.wait("@pkgs");
|
|
73
|
+
cy.getByTestId(home.packageListScope, { timeout: 1e4 }).should("have.value", "@verdaccio");
|
|
74
|
+
cy.getByTestId(pkg.title).should("have.length", 1).and("contain", scopedPkg);
|
|
75
|
+
});
|
|
76
|
+
maybeIt(flags.homeToolbar)("should say nothing matches instead of showing the onboarding card", () => {
|
|
77
|
+
cy.visit(`${config.registryUrl}/?q=no-package-matches-this`);
|
|
78
|
+
cy.wait("@pkgs");
|
|
79
|
+
cy.getByTestId(home.packageListNoMatch, { timeout: 1e4 }).should("be.visible");
|
|
80
|
+
cy.getByTestId(home.helpCard).should("not.exist");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe("with a published package", () => {
|
|
84
|
+
const pkgName = "@verdaccio/ux-detail-fixture";
|
|
85
|
+
const detailUrl = `${config.registryUrl}/-/web/detail/${pkgName}`;
|
|
86
|
+
const needsPackage = flags.detailTabInUrl || flags.installBunAndNpmrc || flags.copyFeedback || flags.searchGrouping || flags.versionLink || flags.settingsPublishTab;
|
|
87
|
+
let tempFolder = null;
|
|
88
|
+
let version = "1.0.0";
|
|
89
|
+
before(() => {
|
|
90
|
+
if (!needsPackage) return;
|
|
91
|
+
cy.task("publishPackage", {
|
|
92
|
+
pkgName,
|
|
93
|
+
version: "1.0.0",
|
|
94
|
+
dependencies: { debug: "4.0.0" },
|
|
95
|
+
unique: true
|
|
96
|
+
}).then((result) => {
|
|
97
|
+
tempFolder = result?.tempFolder ?? null;
|
|
98
|
+
version = result?.version ?? version;
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
after(() => {
|
|
102
|
+
if (!needsPackage) return;
|
|
103
|
+
cy.task("unpublishPackage", {
|
|
104
|
+
pkgName,
|
|
105
|
+
tempFolder: tempFolder ?? void 0
|
|
106
|
+
});
|
|
107
|
+
cy.then(() => {
|
|
108
|
+
if (tempFolder) cy.task("cleanupPublished", tempFolder);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
maybeIt(flags.settingsPublishTab)("should keep the publishing guide in the settings once packages exist", () => {
|
|
112
|
+
cy.visit(config.registryUrl);
|
|
113
|
+
cy.getByTestId(pkg.itemList, { timeout: 1e4 }).should("exist");
|
|
114
|
+
cy.getByTestId(header.settingsTooltip).click();
|
|
115
|
+
cy.getByTestId(header.settingsPublishTab).click();
|
|
116
|
+
cy.getByTestId(header.settingsPublishPanel).within(() => {
|
|
117
|
+
cy.getByTestId(home.helpGuide).should("be.visible");
|
|
118
|
+
cy.contains(`npm publish --registry ${config.registryUrl}`).scrollIntoView().should("be.visible");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
maybeIt(flags.versionLink)("should open the version page from the home list", () => {
|
|
122
|
+
cy.visit(config.registryUrl);
|
|
123
|
+
cy.contains(`[data-testid="${pkg.itemList}"]`, pkgName, { timeout: 1e4 }).find(`[data-testid="${pkg.versionLink}"]`).click();
|
|
124
|
+
cy.location("pathname").should("eq", `/-/web/detail/${pkgName}/v/${version}`);
|
|
125
|
+
cy.getByTestId(pkg.sidebar, { timeout: 1e4 }).contains(version);
|
|
126
|
+
});
|
|
127
|
+
maybeIt(flags.detailTabInUrl)("should keep the selected tab in ?tab=", () => {
|
|
128
|
+
cy.visit(detailUrl);
|
|
129
|
+
cy.getByTestId(pkg.versionsTab, { timeout: 1e4 }).click();
|
|
130
|
+
cy.location("search").should("contain", "tab=versions");
|
|
131
|
+
cy.getByTestId(pkg.tagLatest).should("be.visible");
|
|
132
|
+
cy.getByTestId(pkg.readmeTab).click();
|
|
133
|
+
cy.location("search").should("not.contain", "tab=");
|
|
134
|
+
});
|
|
135
|
+
maybeIt(flags.detailTabInUrl)("should open the tab a link points to", () => {
|
|
136
|
+
cy.visit(`${detailUrl}?tab=dependencies`);
|
|
137
|
+
cy.getByTestId(pkg.dependenciesTab, { timeout: 1e4 }).should("have.attr", "aria-selected", "true");
|
|
138
|
+
cy.getByTestId(pkg.dependencies).contains("debug");
|
|
139
|
+
});
|
|
140
|
+
maybeIt(flags.installBunAndNpmrc)("should offer bun and the scoped .npmrc line in the install box", () => {
|
|
141
|
+
cy.visit(detailUrl);
|
|
142
|
+
cy.getByTestId(pkg.installBun, { timeout: 1e4 }).contains(`bun add ${pkgName}`);
|
|
143
|
+
cy.getByTestId(pkg.installNpmrc).contains(`@verdaccio:registry=${config.registryUrl}`);
|
|
144
|
+
});
|
|
145
|
+
maybeIt(flags.copyFeedback)("should confirm a copy on the copy button", () => {
|
|
146
|
+
cy.visit(detailUrl);
|
|
147
|
+
cy.getByTestId(pkg.installNpmrcCopy, { timeout: 1e4 }).click();
|
|
148
|
+
cy.getByTestId(pkg.installNpmrcCopy).should("have.attr", "aria-label", "Copied");
|
|
149
|
+
});
|
|
150
|
+
maybeIt(flags.searchGrouping)("should list a package published here under its own search group", () => {
|
|
151
|
+
cy.intercept("GET", "**/-/verdaccio/data/search/**").as("webSearch");
|
|
152
|
+
cy.visit(config.registryUrl);
|
|
153
|
+
cy.get("input[placeholder*=\"earch\"]", { timeout: 1e4 }).type("ux-detail-fixture", { delay: 30 });
|
|
154
|
+
cy.wait("@webSearch", { timeout: 1e4 });
|
|
155
|
+
cy.get("[role=\"listbox\"]", { timeout: 5e3 }).within(() => {
|
|
156
|
+
cy.getByTestId(`${search.groupPrefix}private`).should("be.visible");
|
|
157
|
+
cy.contains("[role=\"option\"]", pkgName).find(`[data-testid="${search.originPrefix}private"]`).should("exist");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
export { homeDetailUxTests };
|
|
165
|
+
|
|
166
|
+
//# sourceMappingURL=home-detail-ux.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"home-detail-ux.js","names":[],"sources":["../../../src/tests/home-detail-ux.ts"],"sourcesContent":["/// <reference types=\"cypress\" />\nimport { maybeIt } from '../features';\nimport { RegistryConfig } from '../types';\n\n/**\n * Home and package page UX that ships with the 9.x web UI: the package\n * list toolbar, the tab in the address, the welcome guide, the install\n * box (bun and `.npmrc`), copy feedback and search grouped by origin.\n *\n * Every test is a probe (flag defaults to `false`): released lines do\n * not have these features yet.\n */\nexport function homeDetailUxTests(config: RegistryConfig) {\n const { features } = config;\n const flags = features.homeDetailUx;\n const { header, home, package: pkg, search } = config.testIds;\n\n describe('home and detail ux', () => {\n describe('with an empty registry', () => {\n beforeEach(() => {\n cy.intercept('GET', '/-/verdaccio/data/packages').as('pkgs');\n cy.visit(config.registryUrl);\n cy.wait('@pkgs');\n });\n\n maybeIt(flags.welcomeGuide)(\n 'should guide the first publish with a setup tab per package manager',\n () => {\n cy.getByTestId(home.helpCard, { timeout: 10000 }).should('be.visible');\n // the e2e contract of the old card still holds\n cy.getByTestId(home.helpCard).contains('No Package Published Yet.');\n cy.getByTestId(home.helpCard).contains(`npm adduser --registry ${config.registryUrl}`);\n cy.getByTestId(home.helpCard).contains(`npm publish --registry ${config.registryUrl}`);\n\n cy.getByTestId(`${home.helpSetupTabPrefix}npm`).should('be.visible');\n cy.getByTestId(home.helpCard).contains(`npm config set registry ${config.registryUrl}`);\n // quoted: getByTestId does not escape the dot in `.npmrc`\n cy.get(`[data-testid=\"${home.helpSetupTabPrefix}.npmrc\"]`).click();\n cy.getByTestId(home.helpCard).contains(`registry=${config.registryUrl}`);\n cy.getByTestId(home.helpUplinksNote).should('be.visible');\n }\n );\n });\n\n describe('with packages from two scopes', () => {\n const scopedPkg = '@verdaccio/ux-toolbar-fixture';\n const unscopedPkg = 'ux-toolbar-unscoped-fixture';\n const tempFolders: string[] = [];\n\n before(() => {\n if (!flags.homeToolbar) return;\n [scopedPkg, unscopedPkg].forEach((pkgName) => {\n cy.task('publishPackage', { pkgName, version: '1.0.0', unique: true }).then((r) => {\n if (r?.tempFolder) tempFolders.push(r.tempFolder);\n });\n });\n });\n\n after(() => {\n if (!flags.homeToolbar) return;\n [scopedPkg, unscopedPkg].forEach((pkgName) => cy.task('unpublishPackage', pkgName));\n cy.then(() => tempFolders.forEach((folder) => cy.task('cleanupPublished', folder)));\n });\n\n beforeEach(() => {\n cy.intercept('GET', '/-/verdaccio/data/packages').as('pkgs');\n });\n\n maybeIt(flags.homeToolbar)('should filter the list and keep the filter in ?q=', () => {\n cy.visit(config.registryUrl);\n cy.wait('@pkgs');\n cy.getByTestId(home.packageListToolbar, { timeout: 10000 }).should('be.visible');\n cy.getByTestId(pkg.title).should('have.length', 2);\n\n cy.getByTestId(home.packageListFilter).type('unscoped', { delay: 20 });\n cy.getByTestId(pkg.title).should('have.length', 1).and('contain', unscopedPkg);\n cy.location('search').should('eq', '?q=unscoped');\n\n // a reload keeps the filter: it lives in the address\n cy.reload();\n cy.getByTestId(home.packageListFilter, { timeout: 10000 }).should('have.value', 'unscoped');\n cy.getByTestId(pkg.title).should('have.length', 1);\n });\n\n maybeIt(flags.homeToolbar)('should restore the scope from ?scope=', () => {\n cy.visit(`${config.registryUrl}/?scope=@verdaccio`);\n cy.wait('@pkgs');\n cy.getByTestId(home.packageListScope, { timeout: 10000 }).should(\n 'have.value',\n '@verdaccio'\n );\n cy.getByTestId(pkg.title).should('have.length', 1).and('contain', scopedPkg);\n });\n\n maybeIt(flags.homeToolbar)(\n 'should say nothing matches instead of showing the onboarding card',\n () => {\n cy.visit(`${config.registryUrl}/?q=no-package-matches-this`);\n cy.wait('@pkgs');\n cy.getByTestId(home.packageListNoMatch, { timeout: 10000 }).should('be.visible');\n cy.getByTestId(home.helpCard).should('not.exist');\n }\n );\n });\n\n describe('with a published package', () => {\n const pkgName = '@verdaccio/ux-detail-fixture';\n const detailUrl = `${config.registryUrl}/-/web/detail/${pkgName}`;\n const needsPackage =\n flags.detailTabInUrl ||\n flags.installBunAndNpmrc ||\n flags.copyFeedback ||\n flags.searchGrouping ||\n flags.versionLink ||\n flags.settingsPublishTab;\n let tempFolder: string | null = null;\n let version = '1.0.0';\n\n before(() => {\n if (!needsPackage) return;\n cy.task('publishPackage', {\n pkgName,\n version: '1.0.0',\n dependencies: { debug: '4.0.0' },\n unique: true,\n }).then((result) => {\n tempFolder = result?.tempFolder ?? null;\n // `unique: true` makes the version 1.0.0-t<timestamp>\n version = result?.version ?? version;\n });\n });\n\n after(() => {\n if (!needsPackage) return;\n cy.task('unpublishPackage', { pkgName, tempFolder: tempFolder ?? undefined });\n cy.then(() => {\n if (tempFolder) cy.task('cleanupPublished', tempFolder);\n });\n });\n\n maybeIt(flags.settingsPublishTab)(\n 'should keep the publishing guide in the settings once packages exist',\n () => {\n cy.visit(config.registryUrl);\n cy.getByTestId(pkg.itemList, { timeout: 10000 }).should('exist');\n cy.getByTestId(header.settingsTooltip).click();\n cy.getByTestId(header.settingsPublishTab).click();\n cy.getByTestId(header.settingsPublishPanel).within(() => {\n cy.getByTestId(home.helpGuide).should('be.visible');\n // the dialog scrolls; at the default viewport the command sits below the fold\n cy.contains(`npm publish --registry ${config.registryUrl}`)\n .scrollIntoView()\n .should('be.visible');\n });\n }\n );\n\n maybeIt(flags.versionLink)('should open the version page from the home list', () => {\n cy.visit(config.registryUrl);\n cy.contains(`[data-testid=\"${pkg.itemList}\"]`, pkgName, { timeout: 10000 })\n .find(`[data-testid=\"${pkg.versionLink}\"]`)\n .click();\n // scoped names keep the literal slash in the path\n cy.location('pathname').should('eq', `/-/web/detail/${pkgName}/v/${version}`);\n cy.getByTestId(pkg.sidebar, { timeout: 10000 }).contains(version);\n });\n\n maybeIt(flags.detailTabInUrl)('should keep the selected tab in ?tab=', () => {\n cy.visit(detailUrl);\n cy.getByTestId(pkg.versionsTab, { timeout: 10000 }).click();\n cy.location('search').should('contain', 'tab=versions');\n cy.getByTestId(pkg.tagLatest).should('be.visible');\n\n cy.getByTestId(pkg.readmeTab).click();\n cy.location('search').should('not.contain', 'tab=');\n });\n\n maybeIt(flags.detailTabInUrl)('should open the tab a link points to', () => {\n cy.visit(`${detailUrl}?tab=dependencies`);\n cy.getByTestId(pkg.dependenciesTab, { timeout: 10000 }).should(\n 'have.attr',\n 'aria-selected',\n 'true'\n );\n cy.getByTestId(pkg.dependencies).contains('debug');\n });\n\n maybeIt(flags.installBunAndNpmrc)(\n 'should offer bun and the scoped .npmrc line in the install box',\n () => {\n cy.visit(detailUrl);\n cy.getByTestId(pkg.installBun, { timeout: 10000 }).contains(`bun add ${pkgName}`);\n cy.getByTestId(pkg.installNpmrc).contains(`@verdaccio:registry=${config.registryUrl}`);\n }\n );\n\n maybeIt(flags.copyFeedback)('should confirm a copy on the copy button', () => {\n cy.visit(detailUrl);\n cy.getByTestId(pkg.installNpmrcCopy, { timeout: 10000 }).click();\n cy.getByTestId(pkg.installNpmrcCopy).should('have.attr', 'aria-label', 'Copied');\n });\n\n maybeIt(flags.searchGrouping)(\n 'should list a package published here under its own search group',\n () => {\n cy.intercept('GET', '**/-/verdaccio/data/search/**').as('webSearch');\n cy.visit(config.registryUrl);\n cy.get('input[placeholder*=\"earch\"]', { timeout: 10000 }).type('ux-detail-fixture', {\n delay: 30,\n });\n cy.wait('@webSearch', { timeout: 10000 });\n cy.get('[role=\"listbox\"]', { timeout: 5000 }).within(() => {\n cy.getByTestId(`${search.groupPrefix}private`).should('be.visible');\n cy.contains('[role=\"option\"]', pkgName)\n .find(`[data-testid=\"${search.originPrefix}private\"]`)\n .should('exist');\n });\n }\n );\n });\n });\n}\n"],"mappings":"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"}
|