@hexonet/semantic-release-whmcs 5.0.69 → 5.1.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/HISTORY.md +28 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/LICENSE +14 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/_locales/en/messages.json +160 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/_metadata/generated_indexed_rulesets/_ruleset1 +0 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/background.html +2 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/background.js +681 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/css/common.css +14306 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/hotreload.js +43 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/js/0_defaultClickHandler.js +590 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/js/2_sessionStorageHandler.js +30 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/js/3_localStorageHandler.js +243 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/js/5_clickHandler.js +8523 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/js/6_cookieHandler.js +764 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/js/8_googleHandler.js +105 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/js/embedsHandler.js +107 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/menu/index.html +79 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/menu/script.js +161 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/menu/spinner.svg +12 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/menu/style.css +68 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/options.html +42 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/options.js +63 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/data/rules.js +20684 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/icons/128.png +0 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/icons/16.png +0 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/icons/32.png +0 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/icons/48.png +0 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/manifest.json +47 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/manifest_v2.json +46 -0
- package/extensions/I-Still-Dont-Care-About-Cookies/rules.json +19539 -0
- package/lib/delete-marketplace-version.js +82 -31
- package/lib/publish.js +52 -43
- package/lib/puppet-utils.js +145 -0
- package/lib/puppet.js +68 -41
- package/lib/resolve-config.js +1 -0
- package/lib/set-compatible-versions.js +26 -10
- package/package.json +6 -6
- 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
|
-
|
23
|
-
const
|
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
|
-
//
|
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
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
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.
|
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
|
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
|
-
|
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
|
-
//
|
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
|
40
|
-
await page.waitForSelector(
|
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
|
-
|
43
|
-
await page
|
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
|
-
//
|
53
|
-
//
|
54
|
-
|
55
|
-
|
56
|
-
const dateString =
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
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
|
-
|
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
|
91
|
+
await safeClose(page);
|
83
92
|
return false;
|
84
93
|
}
|
85
94
|
debug("Publishing new product version succeeded.");
|
86
|
-
await page
|
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:
|
19
|
+
timeout: 20 * 1000,
|
14
20
|
},
|
15
21
|
navOpts: {
|
16
22
|
waitUntil: ["networkidle0"],
|
17
|
-
timeout:
|
23
|
+
timeout: 20 * 1000,
|
18
24
|
},
|
19
25
|
selectorOpts: {
|
20
|
-
timeout:
|
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" ? "
|
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
|
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
|
-
|
80
|
-
await page.
|
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.
|
83
|
-
|
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
|
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
|
|
package/lib/resolve-config.js
CHANGED
@@ -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
|
-
|
15
|
-
const
|
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
|
-
//
|
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
|
-
|
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
|
-
|
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();
|