@hexonet/semantic-release-whmcs 5.0.69 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/HISTORY.md +35 -0
  2. package/extensions/I-Still-Dont-Care-About-Cookies/LICENSE +14 -0
  3. package/extensions/I-Still-Dont-Care-About-Cookies/_locales/en/messages.json +160 -0
  4. package/extensions/I-Still-Dont-Care-About-Cookies/_metadata/generated_indexed_rulesets/_ruleset1 +0 -0
  5. package/extensions/I-Still-Dont-Care-About-Cookies/data/background.html +2 -0
  6. package/extensions/I-Still-Dont-Care-About-Cookies/data/background.js +681 -0
  7. package/extensions/I-Still-Dont-Care-About-Cookies/data/css/common.css +14306 -0
  8. package/extensions/I-Still-Dont-Care-About-Cookies/data/hotreload.js +43 -0
  9. package/extensions/I-Still-Dont-Care-About-Cookies/data/js/0_defaultClickHandler.js +590 -0
  10. package/extensions/I-Still-Dont-Care-About-Cookies/data/js/2_sessionStorageHandler.js +30 -0
  11. package/extensions/I-Still-Dont-Care-About-Cookies/data/js/3_localStorageHandler.js +243 -0
  12. package/extensions/I-Still-Dont-Care-About-Cookies/data/js/5_clickHandler.js +8523 -0
  13. package/extensions/I-Still-Dont-Care-About-Cookies/data/js/6_cookieHandler.js +764 -0
  14. package/extensions/I-Still-Dont-Care-About-Cookies/data/js/8_googleHandler.js +105 -0
  15. package/extensions/I-Still-Dont-Care-About-Cookies/data/js/embedsHandler.js +107 -0
  16. package/extensions/I-Still-Dont-Care-About-Cookies/data/menu/index.html +79 -0
  17. package/extensions/I-Still-Dont-Care-About-Cookies/data/menu/script.js +161 -0
  18. package/extensions/I-Still-Dont-Care-About-Cookies/data/menu/spinner.svg +12 -0
  19. package/extensions/I-Still-Dont-Care-About-Cookies/data/menu/style.css +68 -0
  20. package/extensions/I-Still-Dont-Care-About-Cookies/data/options.html +42 -0
  21. package/extensions/I-Still-Dont-Care-About-Cookies/data/options.js +63 -0
  22. package/extensions/I-Still-Dont-Care-About-Cookies/data/rules.js +20684 -0
  23. package/extensions/I-Still-Dont-Care-About-Cookies/icons/128.png +0 -0
  24. package/extensions/I-Still-Dont-Care-About-Cookies/icons/16.png +0 -0
  25. package/extensions/I-Still-Dont-Care-About-Cookies/icons/32.png +0 -0
  26. package/extensions/I-Still-Dont-Care-About-Cookies/icons/48.png +0 -0
  27. package/extensions/I-Still-Dont-Care-About-Cookies/manifest.json +47 -0
  28. package/extensions/I-Still-Dont-Care-About-Cookies/manifest_v2.json +46 -0
  29. package/extensions/I-Still-Dont-Care-About-Cookies/rules.json +19539 -0
  30. package/lib/delete-marketplace-version.js +82 -31
  31. package/lib/publish.js +52 -43
  32. package/lib/puppet-utils.js +145 -0
  33. package/lib/puppet.js +68 -41
  34. package/lib/resolve-config.js +1 -0
  35. package/lib/set-compatible-versions.js +26 -10
  36. package/package.json +6 -6
  37. package/whmcs.js +7 -5
@@ -1,6 +1,7 @@
1
1
  import puppet from "./puppet.js";
2
2
  import debugConfig from "debug";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { wait, waitForSubmitResult, safeClose, loginAndNavigate, clickAndWaitForResult } from "./puppet-utils.js";
4
5
  const debug = debugConfig("semantic-release:whmcs");
5
6
  const __filename = fileURLToPath(import.meta.url);
6
7
 
@@ -19,59 +20,109 @@ export default async (pluginConfig, context) => {
19
20
 
20
21
  debug(`${out}Delete Version: ${version}`);
21
22
 
22
- const püppi = await puppet(context);
23
- const result = await püppi.login();
24
- if (!result) {
25
- return result;
26
- }
27
-
28
- const { page, navOpts, gotoOpts, selectorOpts, productid, urlbase } = püppi.config;
29
-
23
+ let page, püppi;
24
+ const { productid, urlbase, gotoOpts, navOpts, selectorOpts } = (context && context.config) || {};
30
25
  try {
31
- // Navigate to product administration
26
+ // Login and navigate to product administration
27
+ ({ page, püppi } = await loginAndNavigate(
28
+ puppet,
29
+ context,
30
+ (p) => `${p.config.urlbase}/product/${p.config.productid}/edit#versions`,
31
+ undefined
32
+ ));
33
+ const { urlbase, productid, gotoOpts, navOpts, selectorOpts } = püppi.config;
32
34
  const url = `${urlbase}/product/${productid}/edit#versions`;
33
- await page.goto(url, gotoOpts);
34
35
  debug("Product page loaded at %s", url);
35
36
 
37
+ // Wait for the table to appear
38
+ debug("Waiting for table to appear...");
39
+ await page.waitForSelector("table", selectorOpts);
40
+ debug("Table found, fetching rows...");
41
+
36
42
  // Fetch all version rows
37
43
  const rows = await page.$$("tr"); // Get all rows in the table
44
+ debug(`Found ${rows.length} rows in the table.`);
38
45
 
46
+ // Find the correct row and delete button for the specified version
39
47
  let deleteButton = null;
40
-
41
- // Loop through rows to find the one containing the specified version
42
48
  for (const row of rows) {
43
49
  const textContent = await row.evaluate((el) => el.textContent);
44
- if (textContent.includes(`Version ${version}`)) {
45
- // Found the correct row, now look for the delete button
46
- deleteButton = await row.$("a.btn-styled-red");
47
- break; // Exit the loop once found
50
+ if (!textContent.includes(`Version ${version}`)) continue;
51
+ const rightCell = await row.$("td.text-right");
52
+ if (!rightCell) continue;
53
+ const candidates = await rightCell.$$("a.btn-styled-red");
54
+ for (const btn of candidates) {
55
+ const btnText = (await btn.evaluate((el) => el.textContent)).trim().toLowerCase();
56
+ if (btnText === "delete") {
57
+ deleteButton = btn;
58
+ break;
59
+ }
48
60
  }
61
+ if (deleteButton) break;
49
62
  }
50
-
51
63
  if (!deleteButton) {
52
64
  debug(`Delete button for version ${version} not found.`);
65
+ await safeClose(page);
53
66
  return false;
54
67
  }
55
68
 
56
- // Click the delete button and wait for navigation
57
- debug("Clicking the delete button.");
58
- await Promise.all([deleteButton.click(), page.waitForNavigation(navOpts)]);
59
-
60
- // Confirm deletion by clicking the confirmation button
61
- const confirmSelector = "button.btn-styled-red";
62
- await page.waitForSelector(confirmSelector, selectorOpts);
63
- debug("Deletion confirmation button available.");
64
-
65
- await Promise.all([page.click(confirmSelector), page.waitForNavigation(navOpts)]);
69
+ // Click the delete button and wait for navigation/alert
70
+ const box = await deleteButton.boundingBox();
71
+ if (!box) {
72
+ debug("Delete button is not visible in the viewport.");
73
+ await safeClose(page);
74
+ return false;
75
+ }
76
+ await deleteButton.evaluate((el) => el.scrollIntoView({ block: "center" }));
77
+ await wait(200);
78
+ let navigated = false;
79
+ try {
80
+ await Promise.all([
81
+ page.waitForNavigation({ waitUntil: "networkidle0", timeout: navOpts.timeout }),
82
+ deleteButton.click(),
83
+ ]);
84
+ navigated = true;
85
+ debug("Navigation after delete button click complete.");
86
+ } catch (err) {
87
+ debug("Navigation after delete button click timed out, will check for alert anyway.");
88
+ }
66
89
 
67
- debug("Clicked confirmation button. WHMCS Marketplace product version deleted successfully.");
90
+ // Click the confirmation button and wait for navigation/alert
91
+ await clickAndWaitForResult(page, 'button[type="submit"].btn-styled-red', { navOpts });
92
+ await wait(200);
93
+ debug("Checking for alert-success after confirmation...");
94
+ const result = await waitForSubmitResult(page, { timeout: navOpts.timeout });
95
+ if (result === "error") {
96
+ debug("Delete failed: error alert shown.");
97
+ await safeClose(page);
98
+ return false;
99
+ }
100
+ if (result === "success") {
101
+ debug("Delete succeeded.");
102
+ // Success, return as normal
103
+ } else {
104
+ // Fallback: check if the version row is gone
105
+ debug("No success or error alert appeared after delete. Checking if version row is gone...");
106
+ await page.waitForSelector("table", { timeout: 5000 });
107
+ const rowsAfter = await page.$$("tr");
108
+ for (const row of rowsAfter) {
109
+ const textContent = await row.evaluate((el) => el.textContent);
110
+ if (textContent.includes(`Version ${version}`)) {
111
+ debug("Delete failed: version row still present.");
112
+ await safeClose(page);
113
+ return false;
114
+ }
115
+ }
116
+ debug("Delete succeeded (row is gone).");
117
+ }
68
118
  } catch (error) {
69
- debug("Deleting product version failed.", error.message);
70
- await page.browser().close();
119
+ debug("Deleting product version failed.", error && error.message);
120
+ // await page.screenshot({ path: `delete-version-error.png` });
121
+ await safeClose(page);
71
122
  return false;
72
123
  }
73
124
 
74
- await page.browser().close();
125
+ await safeClose(page);
75
126
  return {
76
127
  name: "WHMCS Marketplace Product Version",
77
128
  url: `${urlbase}/product/${productid}`,
package/lib/publish.js CHANGED
@@ -2,6 +2,7 @@ import puppet from "./puppet.js";
2
2
  import setCompatibleVersions from "./set-compatible-versions.js";
3
3
  import debugConfig from "debug";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { robustType, safeClose, waitForSubmitResult, loginAndNavigate, clickAndWaitForResult } from "./puppet-utils.js";
5
6
  const debug = debugConfig("semantic-release:whmcs");
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
 
@@ -21,69 +22,77 @@ export default async (pluginConfig, context) => {
21
22
  // strip markdown links from notes as not allowed to keep (taken from remove-markdown and cleaned up)
22
23
  const cleanedNotes = notes.replace(/\[([^[\]]*)\]\([^()]*\)/gm, "$1");
23
24
 
24
- const püppi = await puppet(context);
25
- const result = await püppi.login();
26
- if (!result) {
27
- return result;
28
- }
29
- const { page, gotoOpts, selectorOpts, productid, urlbase } = püppi.config;
25
+ let page, püppi, urlbase, productid, gotoOpts, selectorOpts;
30
26
 
31
27
  debug(`Release Version: ${version}`);
32
28
  debug(`Notes: ${notes}`);
33
29
 
34
30
  try {
35
- // add new version
31
+ // Login and navigate using a urlBuilder function
32
+ ({ page, püppi } = await loginAndNavigate(
33
+ puppet,
34
+ context,
35
+ (p) => `${p.config.urlbase}/product/${p.config.productid}/versions/new`,
36
+ undefined
37
+ ));
38
+ ({ urlbase, productid, gotoOpts, selectorOpts } = püppi.config);
36
39
  const url = `${urlbase}/product/${productid}/versions/new`;
37
- await page.goto(url, gotoOpts);
38
40
  debug("product page loaded at %s", url);
39
- const selector = 'div.listing-edit-container form button[type="submit"]';
40
- await page.waitForSelector(selector, selectorOpts);
41
+ const submitSelector = 'div.listing-edit-container form button[type="submit"]';
42
+ await page.waitForSelector(submitSelector, selectorOpts);
41
43
  debug("product page submit button selector found");
42
- /* istanbul ignore next */
43
- await page.$eval(
44
- "#version",
45
- (el, value) => {
46
- el.value = value;
47
- },
48
- version
49
- );
44
+ // Fill version
45
+ await robustType(page, "#version", version);
50
46
  debug("form input for version finished.");
51
47
 
52
- // fill input type date with localized string
53
- // https://www.mattzeunert.com/2020/04/01/filling-out-a-date-input-with-puppeteer.html
54
- const date = releaseDate ? new Date(releaseDate) : new Date();
55
- /* istanbul ignore next */
56
- const dateString = await page.evaluate(
57
- (d) =>
58
- new Date(d).toLocaleDateString(navigator.language, {
59
- day: "2-digit",
60
- month: "2-digit",
61
- year: "numeric",
62
- }),
63
- date.toISOString()
64
- );
65
- await page.enterAndType("#released_at", dateString);
66
-
67
- debug("form input for released_at finished.");
68
- /* istanbul ignore next */
69
- await page.$eval(
70
- "#description",
71
- (el, value) => {
72
- el.value = value;
48
+ // Fill release date (input type="date" expects yyyy-mm-dd)
49
+ // Simplified date logic: support dd/mm/yyyy and ISO, always output yyyy-mm-dd
50
+ // Always use the current date for the release date input
51
+ const now = new Date();
52
+ const dateString = now.toISOString().slice(0, 10);
53
+ // Set the value directly for input[type=date] (robust for Puppeteer)
54
+ await page.evaluate(
55
+ (selector, value) => {
56
+ const el = document.querySelector(selector);
57
+ if (el) {
58
+ el.value = value;
59
+ el.dispatchEvent(new Event("input", { bubbles: true }));
60
+ el.dispatchEvent(new Event("change", { bubbles: true }));
61
+ }
73
62
  },
74
- cleanedNotes
63
+ "#released_at",
64
+ dateString
75
65
  );
66
+ debug("form input for released_at finished.");
67
+
68
+ // Fill description
69
+ await robustType(page, "#description", cleanedNotes);
76
70
  debug("form input for description finished.");
77
71
 
78
- await page.clickAndNavigate('div.listing-edit-container form button[type="submit"]');
72
+ // Wait for submit button to be enabled
73
+ await page.waitForFunction((sel) => !document.querySelector(sel).disabled, {}, submitSelector);
74
+
75
+ // Click submit and wait for navigation/alert using shared util
76
+ await clickAndWaitForResult(page, submitSelector, { navOpts: selectorOpts });
77
+ const result = await waitForSubmitResult(page, { timeout: selectorOpts.timeout || 15000 });
78
+ if (result === "error") {
79
+ debug("Publish failed: error alert shown.");
80
+ await safeClose(page);
81
+ return false;
82
+ } else if (result === "success") {
83
+ debug("Publish succeeded.");
84
+ } else {
85
+ debug("No success or error alert appeared after submit.");
86
+ }
87
+
79
88
  await setCompatibleVersions(pluginConfig, context);
80
89
  } catch (error) {
81
90
  debug("Publishing new product version failed.", error.message);
82
- await page.browser().close();
91
+ await safeClose(page);
83
92
  return false;
84
93
  }
85
94
  debug("Publishing new product version succeeded.");
86
- await page.browser().close();
95
+ await safeClose(page);
87
96
 
88
97
  return {
89
98
  name: "WHMCS Marketplace Product Version",
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Safely close a Puppeteer page and its browser if defined.
3
+ */
4
+ export async function safeClose(page) {
5
+ if (page) {
6
+ try {
7
+ if (typeof page.close === "function") await page.close();
8
+ if (typeof page.browser === "function") {
9
+ const browser = page.browser();
10
+ if (browser && typeof browser.close === "function") await browser.close();
11
+ }
12
+ } catch (e) {
13
+ // ignore errors on close
14
+ }
15
+ }
16
+ }
17
+ /**
18
+ * Login, navigate to a URL, and return the page object if successful.
19
+ * Throws on login failure or navigation failure.
20
+ */
21
+ /**
22
+ * Log in, then build the URL using the logged-in config, then navigate.
23
+ * urlBuilder: function that receives püppi and returns the URL string.
24
+ */
25
+ export async function loginAndNavigate(puppet, context, urlBuilder, gotoOpts) {
26
+ const debug = context && context.debug ? context.debug : console.debug;
27
+ const püppi = await puppet(context);
28
+ const loginResult = await püppi.login();
29
+ if (!loginResult) {
30
+ debug && debug("Login failed or returned falsy result.");
31
+ throw new Error("Login failed");
32
+ }
33
+ const { page } = püppi.config;
34
+ const url = typeof urlBuilder === "function" ? urlBuilder(püppi) : urlBuilder;
35
+ const navOpts = gotoOpts || { waitUntil: ["load", "domcontentloaded"], timeout: 10000 };
36
+ debug && debug(`Navigating to: ${url} with options: ${JSON.stringify(navOpts)}`);
37
+ await page.goto(url, navOpts);
38
+ debug && debug(`Navigation to ${url} complete.`);
39
+ return { page, püppi };
40
+ }
41
+
42
+ /**
43
+ * Click a selector, wait for navigation or alert, and return the result.
44
+ * Handles both navigation and in-place alert scenarios.
45
+ */
46
+ export async function clickAndWaitForResult(page, selector, { navOpts, resultTimeout = 10000, waitAfter = 200 } = {}) {
47
+ await page.waitForSelector(selector, { visible: true, timeout: navOpts?.timeout || 10000 });
48
+ const btn = await page.$(selector);
49
+ if (!btn) throw new Error(`Button not found: ${selector}`);
50
+ const isDisabled = await btn.evaluate((el) => el.disabled);
51
+ if (isDisabled) throw new Error(`Button is disabled: ${selector}`);
52
+ await btn.evaluate((el) => el.scrollIntoView({ block: "center" }));
53
+ await new Promise((res) => setTimeout(res, waitAfter));
54
+ let navigated = false;
55
+ try {
56
+ await Promise.all([
57
+ page.waitForNavigation({ waitUntil: "networkidle0", timeout: navOpts?.timeout || 10000 }),
58
+ btn.evaluate((el) => el.click()),
59
+ ]);
60
+ navigated = true;
61
+ } catch (err) {
62
+ // Navigation may not always happen
63
+ }
64
+ await new Promise((res) => setTimeout(res, waitAfter));
65
+ return navigated;
66
+ }
67
+ /**
68
+ * Wait for a success or error alert after a form submit, and return 'success', 'error', or 'none'.
69
+ */
70
+ export async function waitForSubmitResult(page, { timeout = 10000 } = {}) {
71
+ const successSelector = ".alert-success, .alert.alert-success";
72
+ const errorSelector = ".alert-danger, .alert.alert-danger";
73
+ try {
74
+ await page.waitForSelector(`${successSelector},${errorSelector}`, { timeout });
75
+ if (await page.$(successSelector)) return "success";
76
+ if (await page.$(errorSelector)) return "error";
77
+ } catch (e) {
78
+ // No alert appeared
79
+ }
80
+ return "none";
81
+ }
82
+ // Shared Puppeteer utility functions for robust automation
83
+
84
+ /**
85
+ * Wait for either a navigation (URL change) or a selector to appear, polling for up to timeout ms.
86
+ * Returns { redirected: boolean, selectorFound: boolean, url: string }
87
+ */
88
+ export async function waitForNavigationOrSelector(
89
+ page,
90
+ { urlPart = "", selector = "", timeout = 15000, pollInterval = 300 } = {}
91
+ ) {
92
+ const oldUrl = page.url();
93
+ let redirected = false;
94
+ let selectorFound = false;
95
+ let urlAfter = "";
96
+ const start = Date.now();
97
+ while (Date.now() - start < timeout) {
98
+ urlAfter = page.url();
99
+ if (urlPart && urlAfter.includes(urlPart)) {
100
+ redirected = true;
101
+ break;
102
+ }
103
+ if (selector) {
104
+ try {
105
+ await page.waitForSelector(selector, { timeout: pollInterval });
106
+ selectorFound = true;
107
+ urlAfter = page.url();
108
+ break;
109
+ } catch (e) {
110
+ // ignore, keep polling
111
+ }
112
+ }
113
+ await new Promise((res) => setTimeout(res, pollInterval));
114
+ }
115
+ return { redirected, selectorFound, url: urlAfter };
116
+ }
117
+
118
+ /**
119
+ * Robustly fill an input field: focus, clear, type, and fallback to evaluate if needed.
120
+ */
121
+ export async function robustType(page, selector, value, delay = 30) {
122
+ await page.focus(selector);
123
+ await page.click(selector, { clickCount: 3 });
124
+ await page.keyboard.press("Backspace");
125
+ await page.type(selector, value, { delay });
126
+ let actual = await page.$eval(selector, (el) => el.value);
127
+ if (actual !== value) {
128
+ await page.evaluate(
129
+ (sel, val) => {
130
+ document.querySelector(sel).value = val;
131
+ },
132
+ selector,
133
+ value
134
+ );
135
+ actual = await page.$eval(selector, (el) => el.value);
136
+ }
137
+ return actual;
138
+ }
139
+
140
+ /**
141
+ * Wait for a short time (ms)
142
+ */
143
+ export function wait(ms) {
144
+ return new Promise((res) => setTimeout(res, ms));
145
+ }
package/lib/puppet.js CHANGED
@@ -1,7 +1,13 @@
1
1
  import puppeteer from "puppeteer";
2
2
  import resolveConfig from "./resolve-config.js";
3
3
  import debugConfig from "debug";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { waitForNavigationOrSelector, robustType, wait } from "./puppet-utils.js";
4
7
  const debug = debugConfig("semantic-release:whmcs");
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
5
11
  export default async (context) => {
6
12
  let config;
7
13
  const cfg = {
@@ -10,32 +16,44 @@ export default async (context) => {
10
16
  // logger: logger,
11
17
  gotoOpts: {
12
18
  waitUntil: ["load", "domcontentloaded"],
13
- timeout: 60 * 1000 * 10,
19
+ timeout: 20 * 1000,
14
20
  },
15
21
  navOpts: {
16
22
  waitUntil: ["networkidle0"],
17
- timeout: 60 * 1000 * 5,
23
+ timeout: 20 * 1000,
18
24
  },
19
25
  selectorOpts: {
20
- timeout: 60 * 1000 * 5,
26
+ timeout: 20 * 1000,
21
27
  },
22
28
  logger: context.logger,
23
29
  };
24
30
 
31
+ // Configure browser launch arguments
32
+ const baseArgs = [
33
+ "--disable-gpu",
34
+ "--start-maximized",
35
+ "--no-sandbox",
36
+ "--disable-setuid-sandbox",
37
+ "--disable-infobars",
38
+ "--ignore-certifcate-errors",
39
+ "--ignore-certifcate-errors-spki-list",
40
+ "--ignoreHTTPSErrors=true",
41
+ ];
42
+
43
+ const launchArgs = [...baseArgs];
44
+ // Add extension if not using incognito mode and if extension is available
45
+ if (cfg.useCookieExtension && !cfg.incognito) {
46
+ const extensionPath = path.join(__dirname, "../extensions/I-Still-Dont-Care-About-Cookies");
47
+ launchArgs.push(`--disable-extensions-except=${extensionPath}`, `--load-extension=${extensionPath}`);
48
+ debug("Loading I Still Don't Care About Cookies extension");
49
+ } else if (cfg.incognito) {
50
+ launchArgs.push("--incognito");
51
+ }
52
+
25
53
  const browser = await puppeteer.launch({
26
- headless: cfg.headless === "1" ? "new" : false,
54
+ headless: cfg.headless === "1" ? "true" : false,
27
55
  defaultViewport: null, // automatically full-sized
28
- args: [
29
- "--disable-gpu",
30
- "--incognito",
31
- "--start-maximized",
32
- "--no-sandbox",
33
- "--disable-setuid-sandbox",
34
- "--disable-infobars",
35
- "--ignore-certifcate-errors",
36
- "--ignore-certifcate-errors-spki-list",
37
- "--ignoreHTTPSErrors=true",
38
- ],
56
+ args: launchArgs,
39
57
  });
40
58
  const { logger } = cfg;
41
59
  const [page] = await browser.pages();
@@ -55,40 +73,51 @@ export default async (context) => {
55
73
  });
56
74
  }
57
75
 
58
- page.clickAndNavigate = async (selector) => {
59
- const { page, navOpts } = config;
60
- const nav = page.waitForNavigation(navOpts);
61
- await page.hover(selector);
62
- await page.click(selector);
63
- await nav;
64
- };
65
-
66
- page.enterAndType = async (selector, value) => {
67
- const { page, selectorOpts } = config;
68
- await page.waitForSelector(selector, selectorOpts);
69
- await page.type(selector, value);
70
- };
71
-
72
76
  async function login() {
73
- const { page, login, password, productid, gotoOpts, urlbase } = config;
74
- const selector = 'div.login-leftcol form button[type="submit"]';
75
- // do login
77
+ const { page, login, password, productid, gotoOpts, urlbase, selectorOpts } = config;
78
+ const submitSelector = 'div.login-leftcol form button[type="submit"]';
76
79
  try {
77
80
  await page.goto(`${urlbase}/user/login`, gotoOpts);
78
81
  debug("login form loaded at %s", `${urlbase}/user/login`);
79
- await page.enterAndType("#email", login);
80
- await page.enterAndType("#password", password);
82
+ // Wait for login form
83
+ await page.waitForSelector("#email", selectorOpts);
84
+ await page.waitForSelector("#password", selectorOpts);
85
+ await page.waitForSelector(submitSelector, selectorOpts);
86
+ // Enter credentials
87
+ await robustType(page, "#email", login);
88
+ await robustType(page, "#password", password);
81
89
  debug("WHMCS Marketplace credentials entered");
82
- await page.clickAndNavigate(selector);
83
- debug("WHMCS Marketplace login form submitted.");
90
+ await page.click(submitSelector);
91
+ // Wait for either navigation or post-login selector
92
+ const { redirected, selectorFound, url } = await waitForNavigationOrSelector(page, {
93
+ urlPart: "/account",
94
+ selector: ".account-navbar",
95
+ timeout: 5000,
96
+ });
97
+ if (!redirected && !selectorFound) {
98
+ // Check for alert-danger (login error) on the same page
99
+ const errorAlert = await page.$(".alert-danger, .alert.alert-danger");
100
+ if (errorAlert) {
101
+ debug("Login failed: error alert shown on login page.");
102
+ } else {
103
+ debug("Login failed: no navigation, no selector, and no error alert after submit");
104
+ }
105
+ // await page.screenshot({ path: `login-failed-no-redirect-or-selector.png` });
106
+ await page.browser().close();
107
+ return false;
108
+ }
109
+ debug("WHMCS Marketplace login succeeded (redirected: %s, selectorFound: %s)", redirected, selectorFound);
84
110
  } catch (error) {
85
- debug("WHMCS Marketplace login failed or Product ID missing", error.message);
111
+ debug("WHMCS Marketplace login failed", error.message);
112
+ try {
113
+ // await page.screenshot({ path: `login-error.png` });
114
+ } catch (e) {
115
+ debug("Screenshot failed", e.message);
116
+ }
86
117
  await page.browser().close();
87
118
  return false;
88
119
  }
89
120
 
90
- debug("WHMCS Marketplace login succeeded.");
91
-
92
121
  // access MP Product ID
93
122
  let tmp = productid;
94
123
  if (!tmp || !/^[0-9]+$/.test(productid) || !parseInt(productid, 10)) {
@@ -96,10 +125,8 @@ export default async (context) => {
96
125
  await page.browser().close();
97
126
  return false;
98
127
  }
99
-
100
128
  tmp = tmp.replace(/(.)/g, "$&\u200E");
101
129
  debug(`WHMCS Marketplace Product ID: ${tmp}`);
102
-
103
130
  return true;
104
131
  }
105
132
 
@@ -7,4 +7,5 @@ export default ({ env }) => ({
7
7
  ghrepo: env.GH_REPO || env.GITHUB_REPO || false,
8
8
  headless: env.PUPPETEER_HEADLESS || "1",
9
9
  debug: (env.DEBUG && /^semantic-release:(\*|whmcs)$/.test(env.DEBUG)) || false,
10
+ useCookieExtension: env.USE_COOKIE_EXTENSION || true,
10
11
  });
@@ -1,6 +1,7 @@
1
1
  import puppet from "./puppet.js";
2
2
  import debugConfig from "debug";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { robustType, wait, waitForSubmitResult, loginAndNavigate, clickAndWaitForResult } from "./puppet-utils.js";
4
5
  const debug = debugConfig("semantic-release:whmcs");
5
6
  const __filename = fileURLToPath(import.meta.url);
6
7
 
@@ -11,18 +12,20 @@ export default async (pluginConfig, context) => {
11
12
  const sep = "+++++++++++++++++++++++++++++++++++++++++++++++++++";
12
13
  const out = `\n${sep}\n${__filename}\n${sep}\n`;
13
14
 
14
- const püppi = await puppet(context);
15
- const result = await püppi.login();
16
- if (!result) {
17
- return result;
18
- }
19
- const { page, gotoOpts, selectorOpts, productid, urlbase, minversion } = püppi.config;
15
+ let page, püppi;
16
+ const { productid, urlbase, gotoOpts, selectorOpts, minversion } = (context && context.config) || {};
20
17
 
21
18
  debug(out);
22
19
  try {
23
- // scrap versions from WHMCS marketplace
20
+ // Login and navigate to compatibility page
21
+ ({ page, püppi } = await loginAndNavigate(
22
+ puppet,
23
+ context,
24
+ (p) => `${p.config.urlbase}/product/${p.config.productid}/edit#compatibility`,
25
+ undefined
26
+ ));
27
+ const { urlbase, productid, gotoOpts, selectorOpts, minversion } = püppi.config;
24
28
  const url = `${urlbase}/product/${productid}/edit#compatibility`;
25
- await page.goto(url, gotoOpts);
26
29
  debug("product page loaded at %s", url);
27
30
 
28
31
  const selector = 'input[name="versionIds[]"]';
@@ -36,7 +39,7 @@ export default async (pluginConfig, context) => {
36
39
  tmp = tmp.replace(/(.)/g, "$&\u200E");
37
40
  }
38
41
  debug(`Minimum required WHMCS version: ${tmp}`);
39
- /* istanbul ignore next */
42
+ // No robustType needed for checkboxes, but keep logic DRY
40
43
  await page.$$eval(
41
44
  selector,
42
45
  (checkboxes, minversion) =>
@@ -59,7 +62,20 @@ export default async (pluginConfig, context) => {
59
62
  }),
60
63
  minversion
61
64
  );
62
- await page.clickAndNavigate(submitSelector);
65
+ // Wait for submit button to be enabled
66
+ await page.waitForFunction((sel) => !document.querySelector(sel).disabled, {}, submitSelector);
67
+ // Click submit and wait for navigation/alert using shared util
68
+ await clickAndWaitForResult(page, submitSelector, { navOpts: gotoOpts });
69
+ const result = await waitForSubmitResult(page, { timeout: gotoOpts.timeout || 10000 });
70
+ if (result === "error") {
71
+ debug("Compatibility update failed: error alert shown.");
72
+ await page.browser().close();
73
+ return false;
74
+ } else if (result === "success") {
75
+ debug("Compatibility update succeeded.");
76
+ } else {
77
+ debug("No success or error alert appeared after submit.");
78
+ }
63
79
  } catch (error) {
64
80
  debug("Updating whmcs compatibility list failed.", error.message);
65
81
  await page.browser().close();