wikiploy 1.0.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/.eslintrc.cjs ADDED
@@ -0,0 +1,41 @@
1
+ module.exports = {
2
+ "env": {
3
+ "node": true,
4
+ "browser": true,
5
+ "es2021": true,
6
+ },
7
+ "globals": {
8
+ "$": true,
9
+ "mw": true,
10
+ },
11
+ "extends": "eslint:recommended",
12
+ "overrides": [{
13
+ "env": {
14
+ "node": true,
15
+ "browser": true,
16
+ },
17
+ "files": [
18
+ ".eslintrc.{js,cjs}"
19
+ ],
20
+ "parserOptions": {
21
+ "sourceType": "script"
22
+ }
23
+ }],
24
+ "parserOptions": {
25
+ "ecmaVersion": "latest",
26
+ "sourceType": "module"
27
+ },
28
+ "rules": {
29
+ "no-prototype-builtins": "off",
30
+ "indent": [
31
+ "error",
32
+ "tab",
33
+ {
34
+ "SwitchCase": 1,
35
+ },
36
+ ],
37
+ //"array-bracket-newline": ["error", { "multiline": true, "minItems": 3 }],
38
+ //"array-element-newline": ["error", { "multiline": true }]
39
+ "array-element-newline": ["error", "consistent"]
40
+ }
41
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ // Use IntelliSense to learn about possible attributes.
3
+ // Hover to view descriptions of existing attributes.
4
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
+ "version": "0.2.0",
6
+ "configurations": [
7
+ {
8
+ "type": "node",
9
+ "request": "launch",
10
+ "name": "Run current JS",
11
+ "skipFiles": [
12
+ "<node_internals>/**"
13
+ ],
14
+ "program": "${workspaceFolder}\\${file}"
15
+ },
16
+ {
17
+ "type": "node",
18
+ "request": "launch",
19
+ "name": "Deploy test.js",
20
+ "skipFiles": [
21
+ "<node_internals>/**"
22
+ ],
23
+ "program": "${workspaceFolder}\\Wiki_bot_test.js"
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Deployment metadata.
3
+ */
4
+ export default class DeployConfig {
5
+ /**
6
+ * @param {DeployConfig} page Edit page (tab).
7
+ */
8
+ constructor(options) {
9
+ this.src = options?.src;
10
+ this.dst = options?.dst;
11
+ if (!this.dst) {
12
+ this.dst = `~/${this.src}`;
13
+ }
14
+ }
15
+
16
+ /** info */
17
+ info() {
18
+ return `deploy "${this.src}" to "${this.dst}"`;
19
+ }
20
+
21
+ }
package/PageCache.js ADDED
@@ -0,0 +1,134 @@
1
+
2
+ /**
3
+ * Aggressive resource caching.
4
+ *
5
+ * Multiple pages should be able to share this cache.
6
+ */
7
+ export default class PageCache {
8
+ constructor() {
9
+ this.cache = {};
10
+ this.stats = {
11
+ fromCache: 0,
12
+ direct: 0,
13
+ saved: 0,
14
+ };
15
+ /** Fake max age [s] */
16
+ this.maxAge = 10 * 3600;
17
+ }
18
+
19
+ /** Usage info. */
20
+ info() {
21
+ console.log('[PageCache]'
22
+ , 'stats: ' + JSON.stringify(this.stats)
23
+ , 'urls: ' + Object.keys(this.cache).length
24
+ );
25
+ }
26
+
27
+ /**
28
+ * Prepare page (tab) for caching.
29
+ *
30
+ * Note! This should be done before opening a URL (`page.goto`).
31
+ *
32
+ * @param {Page} page
33
+ */
34
+ async enable(page) {
35
+ const cache = this.cache;
36
+
37
+ // enable requests hijacking (~PWA)
38
+ await page.setRequestInterception(true);
39
+
40
+ // serve from cache
41
+ page.on('request', async (request) => {
42
+ const url = request.url();
43
+ try {
44
+ if (url in cache) {
45
+ const cached = cache[url];
46
+ const method = request.method();
47
+ // if (cached.expires > Date.now()) {
48
+ if (method === 'GET') {
49
+ this.stats.fromCache++;
50
+ await request.respond(cached);
51
+ return;
52
+ }
53
+ else {
54
+ console.warn('[PageCache]', 'skipped', JSON.stringify({method, url}));
55
+ }
56
+ }
57
+ } catch (error) {
58
+ console.warn('[PageCache]', 'serving cache failed', url, '\n', error);
59
+ }
60
+ this.stats.direct++;
61
+ request.continue();
62
+ });
63
+
64
+ // save to cache
65
+ page.on('response', async (response) => {
66
+ const headers = response.headers();
67
+ const url = response.url();
68
+ let cacheit = this.shouldCache(url, headers);
69
+ if (cacheit) {
70
+ if (url in cache) {
71
+ return;
72
+ }
73
+
74
+ let buffer;
75
+ try {
76
+ buffer = await response.buffer();
77
+ } catch (error) {
78
+ return;
79
+ }
80
+
81
+ // cleanup headers
82
+ delete headers['nel'];
83
+ delete headers['report-to'];
84
+ delete headers['age'];
85
+ delete headers['cache-control'];
86
+ delete headers['set-cookie']; // this brakes puppeteer
87
+ delete headers['server-timing'];
88
+ delete headers['x-cache'];
89
+ delete headers['x-cache-status'];
90
+
91
+ // save
92
+ this.stats.saved++;
93
+ cache[url] = {
94
+ status: response.status(),
95
+ headers,
96
+ body: buffer,
97
+ expires: Date.now() + (this.maxAge * 1000),
98
+ };
99
+ }
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Check if we should cache the response.
105
+ */
106
+ shouldCache(url, headers) {
107
+ const contentType = headers['content-type'] || '';
108
+ if (contentType.startsWith('text/html')) {
109
+ return false;
110
+ }
111
+ if (contentType.search(/^text\/(javascript|css)/i) >= 0) {
112
+ return true;
113
+ }
114
+
115
+ const cacheControl = headers['cache-control'] || '';
116
+ if (cacheControl.search(/max-age=[1-9]/) >= 0) {
117
+ return true;
118
+ }
119
+
120
+ // user scripts
121
+ if (url.search(/&ctype=text\/(javascript|css)/) >= 0) {
122
+ return true;
123
+ }
124
+ // MediaWiki modules
125
+ if (url.search(/load.+&modules/) >= 0) {
126
+ return true;
127
+ }
128
+
129
+ // console.log(JSON.stringify(headers, null, '\t'));
130
+ // console.log(url);
131
+
132
+ return null;
133
+ }
134
+ }
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ Wikiploy
2
+ ==========================
3
+
4
+ User scripts and gadgets deployment for wikis (Wikipedia or more generally MediaWiki based wiki).
5
+ Rollout your JS, CSS etc from your git repository to as many MW wikis as you need.
6
+
7
+ This is using [Puppeteer](https://pptr.dev/) to control [Chrome Canary](https://www.google.com/chrome/canary/). You just open Chrome and run a script. The idea is that you are logged in in Chrome and so all edits are still your edits. You can keep the Canary running in the background when you are changing and deploying more stuff.
package/WikiOps.js ADDED
@@ -0,0 +1,139 @@
1
+ import {
2
+ scrollIntoViewIfNeeded,
3
+ waitForSelector,
4
+ waitForSelectors,
5
+ } from './chromeBase.js'
6
+
7
+ import PageCache from './PageCache.js';
8
+
9
+ /**
10
+ * Helper class for edits and navigation.
11
+ *
12
+ * Note! Due to Chrome's large memory consupmtion on new tabs it is best to re-use tabs.
13
+ * So it is best to open tab before loop:
14
+ // open new tab
15
+ const page = await wikiBot.openTab(browser);
16
+ // some loop
17
+ for () {
18
+ await page.goto(url);
19
+ ...do stuff...
20
+ }
21
+ page.close();
22
+ */
23
+ export default class WikiOps {
24
+ /**
25
+ * Init.
26
+ * @param {PageCache} globalCache Cache for page resources.
27
+ */
28
+ constructor(globalCache) {
29
+ this.cache = globalCache ? globalCache : new PageCache();
30
+ }
31
+
32
+ /**
33
+ * Open new tab.
34
+ * @returns {Browser} browser.
35
+ */
36
+ async openTab(browser) {
37
+ const page = await browser.newPage();
38
+ await this.initViewport(page);
39
+ await this.cache.enable(page);
40
+ await this.disarmUnloadWarning(page);
41
+ const timeout = 5000;
42
+ page.setDefaultTimeout(timeout);
43
+ return page;
44
+ }
45
+
46
+ /**
47
+ * Avoid leave-page warning.
48
+ * @param {Page} page
49
+ * @private
50
+ */
51
+ async disarmUnloadWarning(page) {
52
+ // force to accept the warning
53
+ page.on("dialog", (dialog) => {
54
+ if (dialog.type() === "beforeunload") {
55
+ dialog.accept();
56
+ }
57
+ });
58
+ await page.evaluate(() => {
59
+ window.onbeforeunload = null;
60
+ window.addEventListener("load", () => {
61
+ $(window).off('beforeunload');
62
+ });
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Init view.
68
+ * @param {Page} targetPage
69
+ * @private
70
+ */
71
+ async initViewport(targetPage) {
72
+ await targetPage.setViewport({
73
+ width: 1200,
74
+ height: 900
75
+ })
76
+ }
77
+
78
+ /** Save. */
79
+ async saveEdit(targetPage) {
80
+ const timeout = 200;
81
+
82
+ await scrollIntoViewIfNeeded([
83
+ '#wpSave'
84
+ ], targetPage, timeout);
85
+ const element = await waitForSelectors([
86
+ '#wpSave'
87
+ ], targetPage, {
88
+ timeout,
89
+ visible: true
90
+ });
91
+ let nav = targetPage.waitForNavigation(); // init wait
92
+ await element.click({
93
+ offset: {
94
+ x: 10,
95
+ y: 4,
96
+ },
97
+ });
98
+ await nav; // wait for form submit
99
+ }
100
+
101
+ /** Go to url (and wait for it). */
102
+ async goto(page, url) {
103
+ let nav = page.waitForNavigation(); // init wait
104
+ await page.goto(url);
105
+ await nav; // wait for url
106
+ await this.disarmUnloadWarning(page);
107
+ }
108
+
109
+ /** Change intput's value. */
110
+ async fillEdit(page, value) {
111
+ const timeout = 5000;
112
+ await waitForSelector('#editform', page, {
113
+ timeout,
114
+ });
115
+ // await changeElementValue(element, value);
116
+ await page.evaluate((value) => {
117
+ // remove editors (plain, WYSIWYG)
118
+ document.querySelectorAll('#editform textarea, .ace_editor').forEach(el=>el.remove());
119
+ // add plain textarea
120
+ document.querySelector('#editform').insertAdjacentHTML('afterbegin', `
121
+ <textarea id="wpTextbox1" name="wpTextbox1"
122
+ ></textarea>
123
+ `);
124
+ // insert value
125
+ let input = document.querySelector('#wpTextbox1');
126
+ input.value = value;
127
+ }, value);
128
+ }
129
+
130
+ /** Insert summary. */
131
+ async fillSummary(page, summary) {
132
+ await page.evaluate((summary) => {
133
+ let wpSummary = document.querySelector('#wpSummary');
134
+ wpSummary.value = summary;
135
+ }, summary);
136
+ }
137
+
138
+
139
+ }
@@ -0,0 +1,23 @@
1
+ import DeployConfig from './DeployConfig.js';
2
+ import Wikiploy from './Wikiploy.js';
3
+
4
+ const ployBot = new Wikiploy();
5
+ // mock
6
+ // ployBot.mock = true;
7
+ // ployBot.mockSleep = 5_000;
8
+
9
+ (async () => {
10
+ const configs = [];
11
+ configs.push(new DeployConfig({
12
+ src: 'test.js',
13
+ dst: 'User:Nux/test-jsbot--test.js',
14
+ }));
15
+ configs.push(new DeployConfig({
16
+ src: 'test.css',
17
+ dst: 'User:Nux/test-jsbot--test.css',
18
+ }));
19
+ await ployBot.deploy(configs);
20
+ })().catch(err => {
21
+ console.error(err);
22
+ process.exit(1);
23
+ });
package/Wikiploy.js ADDED
@@ -0,0 +1,133 @@
1
+ import puppeteer, { Browser } from 'puppeteer'; // v13+
2
+
3
+ import WikiOps from './WikiOps.js';
4
+ import PageCache from './PageCache.js';
5
+
6
+ import { promises as fs } from "fs"; // node v11+
7
+
8
+ import {
9
+ wsBrowserPort,
10
+ } from './chrome.config.js'
11
+
12
+ // eslint-disable-next-line no-unused-vars
13
+ function sleep(sleepMs) {
14
+ return new Promise((resolve)=>{setTimeout(()=>resolve(), sleepMs)});
15
+ }
16
+
17
+ /**
18
+ * MediaWiki deployment automation.
19
+ *
20
+ * @property _browser {Browser} The user's email
21
+ */
22
+ export default class Wikiploy {
23
+ constructor() {
24
+ this.cache = new PageCache();
25
+ /** Disable save. */
26
+ this.mock = false;
27
+ /** Wait before close [ms] (or you can set a breakpoint to check stuff). */
28
+ this.mockSleep = 0;
29
+
30
+ /** Browser connection. */
31
+ this._browser = false;
32
+
33
+ /** Bot helper. */
34
+ this._bot = new WikiOps(this.cache);
35
+ }
36
+
37
+ /**
38
+ * Deploy scripts.
39
+ * @param {DeployConfig[]} configs
40
+ */
41
+ async deploy(configs) {
42
+ const bot = this._bot;
43
+ const browser = await this.init();
44
+ const page = await bot.openTab(browser);
45
+ console.log(JSON.stringify(configs));
46
+ // main loop
47
+ for (const config of configs) {
48
+ await this.save(config, page);
49
+ }
50
+ page.close();
51
+ console.log(`done`);
52
+ process.exit(0);
53
+ }
54
+
55
+ /**
56
+ * Deploy script.
57
+ * @param {DeployConfig} config Config.
58
+ * @param {Page} page
59
+ */
60
+ async save(config, page) {
61
+ console.log('[Wikiploy]', config.info());
62
+ const bot = this._bot;
63
+ // navigate
64
+ let url = this.editUrl(config.dst);
65
+ await bot.goto(page, url);
66
+ // insert the content of the file into the edit field
67
+ const contents = await fs.readFile(config.src, 'utf8');
68
+ await bot.fillEdit(page, contents);
69
+ // edit description
70
+ const summary = this.preapreSummary(config);
71
+ await bot.fillSummary(page, summary);
72
+
73
+ // save
74
+ if (!this.mock) {
75
+ await bot.saveEdit(page);
76
+ console.log('saved');
77
+ } else {
78
+ await sleep(this.mockSleep);
79
+ }
80
+ }
81
+
82
+
83
+ /**
84
+ * Prepare edit summary.
85
+ *
86
+ * @param {String} pageTitle Title with namespace.
87
+ * @returns {String} Full edit URL.
88
+ * @private
89
+ */
90
+ preapreSummary(config) {
91
+ return '[Wikiploy]' + ` ${config.src}`;
92
+ }
93
+
94
+ /**
95
+ * Prepare edit URL.
96
+ *
97
+ * @param {String} pageTitle Title with namespace.
98
+ * @returns {String} Full edit URL.
99
+ * @private
100
+ */
101
+ editUrl(pageTitle) {
102
+ const baseUrl = `https://pl.wikipedia.org/w/index.php`; // TODO: config/options
103
+
104
+ // common params
105
+ // note that submit action is not affected by new wikicode editor
106
+ let params = `
107
+ &useskin=monobook
108
+ &action=submit
109
+ `.replace(/\s+/g, '');
110
+
111
+ return baseUrl + '?title=' + encodeURIComponent(pageTitle) + params;
112
+ }
113
+
114
+ /**
115
+ * Init browser connection.
116
+ *
117
+ * @returns {Browser} the browser API.
118
+ */
119
+ async init() {
120
+ if (this._browser instanceof Browser) {
121
+ return this._browser;
122
+ }
123
+
124
+ // connect to current (open) Chrome window
125
+ const browserURL = `http://127.0.0.1:${wsBrowserPort}`;
126
+ const browser = await puppeteer.connect({
127
+ // browserWSEndpoint: wsUrl,
128
+ browserURL,
129
+ });
130
+
131
+ return browser;
132
+ }
133
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Chrome configuration.
3
+ *
4
+ * You should have a Canary edition of Chrome. It seems easiest to setup doesn't involve your main Chrome.
5
+ *
6
+ * Then just run Chrome Canary with param: "...\chrome.exe" --remote-debugging-port=9222
7
+ */
8
+
9
+ /** Remote debugging port. */
10
+ export const wsBrowserPort = '9222';
package/chromeBase.js ADDED
@@ -0,0 +1,252 @@
1
+ /* eslint-disable no-undef */
2
+ /* eslint-disable indent */
3
+
4
+ /**
5
+ * Functions provided by Chrome Recorder.
6
+ */
7
+
8
+ export async function waitForSelectors(selectors, frame, options) {
9
+ for (const selector of selectors) {
10
+ try {
11
+ return await waitForSelector(selector, frame, options);
12
+ } catch (err) {
13
+ console.error(err);
14
+ }
15
+ }
16
+ throw new Error('Could not find element for selectors: ' + JSON.stringify(selectors));
17
+ }
18
+
19
+ export async function scrollIntoViewIfNeeded(selectors, frame, timeout) {
20
+ const element = await waitForSelectors(selectors, frame, {
21
+ visible: false,
22
+ timeout
23
+ });
24
+ if (!element) {
25
+ throw new Error(
26
+ 'The element could not be found.'
27
+ );
28
+ }
29
+ await waitForConnected(element, timeout);
30
+ const isInViewport = await element.isIntersectingViewport({
31
+ threshold: 0
32
+ });
33
+ if (isInViewport) {
34
+ return;
35
+ }
36
+ await element.evaluate(element => {
37
+ element.scrollIntoView({
38
+ block: 'center',
39
+ inline: 'center',
40
+ behavior: 'auto',
41
+ });
42
+ });
43
+ await waitForInViewport(element, timeout);
44
+ }
45
+
46
+ export async function waitForConnected(element, timeout) {
47
+ await waitForFunction(async () => {
48
+ return await element.getProperty('isConnected');
49
+ }, timeout);
50
+ }
51
+
52
+ export async function waitForInViewport(element, timeout) {
53
+ await waitForFunction(async () => {
54
+ return await element.isIntersectingViewport({
55
+ threshold: 0
56
+ });
57
+ }, timeout);
58
+ }
59
+
60
+ export async function waitForSelector(selector, frame, options) {
61
+ if (!Array.isArray(selector)) {
62
+ selector = [selector];
63
+ }
64
+ if (!selector.length) {
65
+ throw new Error('Empty selector provided to waitForSelector');
66
+ }
67
+ let element = null;
68
+ for (let i = 0; i < selector.length; i++) {
69
+ const part = selector[i];
70
+ if (element) {
71
+ element = await element.waitForSelector(part, options);
72
+ } else {
73
+ element = await frame.waitForSelector(part, options);
74
+ }
75
+ if (!element) {
76
+ throw new Error('Could not find element: ' + selector.join('>>'));
77
+ }
78
+ if (i < selector.length - 1) {
79
+ element = (await element.evaluateHandle(el => el.shadowRoot ? el.shadowRoot : el)).asElement();
80
+ }
81
+ }
82
+ if (!element) {
83
+ throw new Error('Could not find element: ' + selector.join('|'));
84
+ }
85
+ return element;
86
+ }
87
+
88
+ export async function waitForElement(step, frame, timeout) {
89
+ const {
90
+ count = 1,
91
+ operator = '>=',
92
+ visible = true,
93
+ properties,
94
+ attributes,
95
+ } = step;
96
+ const compFn = {
97
+ '==': (a, b) => a === b,
98
+ '>=': (a, b) => a >= b,
99
+ '<=': (a, b) => a <= b,
100
+ } [operator];
101
+ await waitForFunction(async () => {
102
+ const elements = await querySelectorsAll(step.selectors, frame);
103
+ let result = compFn(elements.length, count);
104
+ const elementsHandle = await frame.evaluateHandle((...elements) => {
105
+ return elements;
106
+ }, ...elements);
107
+ await Promise.all(elements.map((element) => element.dispose()));
108
+ if (result && (properties || attributes)) {
109
+ result = await elementsHandle.evaluate(
110
+ (elements, properties, attributes) => {
111
+ for (const element of elements) {
112
+ if (attributes) {
113
+ for (const [name, value] of Object.entries(attributes)) {
114
+ if (element.getAttribute(name) !== value) {
115
+ return false;
116
+ }
117
+ }
118
+ }
119
+ if (properties) {
120
+ if (!isDeepMatch(properties, element)) {
121
+ return false;
122
+ }
123
+ }
124
+ }
125
+ return true;
126
+
127
+ function isDeepMatch(a, b) {
128
+ if (a === b) {
129
+ return true;
130
+ }
131
+ if ((a && !b) || (!a && b)) {
132
+ return false;
133
+ }
134
+ if (!(a instanceof Object) || !(b instanceof Object)) {
135
+ return false;
136
+ }
137
+ for (const [key, value] of Object.entries(a)) {
138
+ if (!isDeepMatch(value, b[key])) {
139
+ return false;
140
+ }
141
+ }
142
+ return true;
143
+ }
144
+ },
145
+ properties,
146
+ attributes
147
+ );
148
+ }
149
+ await elementsHandle.dispose();
150
+ return result === visible;
151
+ }, timeout);
152
+ }
153
+
154
+ export async function querySelectorsAll(selectors, frame) {
155
+ for (const selector of selectors) {
156
+ const result = await querySelectorAll(selector, frame);
157
+ if (result.length) {
158
+ return result;
159
+ }
160
+ }
161
+ return [];
162
+ }
163
+
164
+ export async function querySelectorAll(selector, frame) {
165
+ if (!Array.isArray(selector)) {
166
+ selector = [selector];
167
+ }
168
+ if (!selector.length) {
169
+ throw new Error('Empty selector provided to querySelectorAll');
170
+ }
171
+ let elements = [];
172
+ for (let i = 0; i < selector.length; i++) {
173
+ const part = selector[i];
174
+ if (i === 0) {
175
+ elements = await frame.$$(part);
176
+ } else {
177
+ const tmpElements = elements;
178
+ elements = [];
179
+ for (const el of tmpElements) {
180
+ elements.push(...(await el.$$(part)));
181
+ }
182
+ }
183
+ if (elements.length === 0) {
184
+ return [];
185
+ }
186
+ if (i < selector.length - 1) {
187
+ const tmpElements = [];
188
+ for (const el of elements) {
189
+ const newEl = (await el.evaluateHandle(el => el.shadowRoot ? el.shadowRoot : el)).asElement();
190
+ if (newEl) {
191
+ tmpElements.push(newEl);
192
+ }
193
+ }
194
+ elements = tmpElements;
195
+ }
196
+ }
197
+ return elements;
198
+ }
199
+
200
+ export async function waitForFunction(fn, timeout) {
201
+ let isActive = true;
202
+ const timeoutId = setTimeout(() => {
203
+ isActive = false;
204
+ }, timeout);
205
+ while (isActive) {
206
+ const result = await fn();
207
+ if (result) {
208
+ clearTimeout(timeoutId);
209
+ return;
210
+ }
211
+ await new Promise(resolve => setTimeout(resolve, 100));
212
+ }
213
+ throw new Error('Timed out');
214
+ }
215
+
216
+ export async function changeSelectElement(element, value) {
217
+ await element.select(value);
218
+ await element.evaluateHandle((e) => {
219
+ e.blur();
220
+ e.focus();
221
+ });
222
+ }
223
+
224
+ export async function changeElementValue(element, value) {
225
+ await element.focus();
226
+ await element.evaluate((input, value) => {
227
+ input.value = value;
228
+ input.dispatchEvent(new Event('input', {
229
+ bubbles: true
230
+ }));
231
+ input.dispatchEvent(new Event('change', {
232
+ bubbles: true
233
+ }));
234
+ }, value);
235
+ }
236
+
237
+ export async function typeIntoElement(element, value) {
238
+ const textToType = await element.evaluate((input, newValue) => {
239
+ if (
240
+ newValue.length <= input.value.length ||
241
+ !newValue.startsWith(input.value)
242
+ ) {
243
+ input.value = '';
244
+ return newValue;
245
+ }
246
+ const originalValue = input.value;
247
+ input.value = '';
248
+ input.value = originalValue;
249
+ return newValue.substring(originalValue.length);
250
+ }, value);
251
+ await element.type(textToType);
252
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "wikiploy",
3
+ "version": "1.0.0",
4
+ "description": "User scripts and gadgets deployment for MediaWiki (Wikipedia).",
5
+ "main": "Wiki_bot_test.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/Eccenux/Wikiploy.git"
13
+ },
14
+ "keywords": [
15
+ "Mediawiki",
16
+ "Wikipedia",
17
+ "deploy",
18
+ "rollout",
19
+ "gadgets"
20
+ ],
21
+ "author": "Maciej Nux Jaros",
22
+ "license": "MIT",
23
+ "bugs": {
24
+ "url": "https://github.com/Eccenux/Wikiploy/issues"
25
+ },
26
+ "homepage": "https://github.com/Eccenux/Wikiploy#readme",
27
+ "dependencies": {
28
+ "puppeteer": "^20.7.2"
29
+ },
30
+ "devDependencies": {
31
+ "chai": "^4.3.7",
32
+ "mocha": "^10.2.0",
33
+ "eslint": "^8.43.0"
34
+ }
35
+ }
@@ -0,0 +1,31 @@
1
+ /* global describe, it */
2
+ import { assert } from 'chai';
3
+ import DeployConfig from '../DeployConfig.js';
4
+
5
+ describe('DeployConfig', function () {
6
+
7
+ describe('init', function () {
8
+ it('should set src, dst', function () {
9
+ let config = {
10
+ src: 'test.src.js',
11
+ dst: 'User:Nux/test.dst.js',
12
+ };
13
+ let result = new DeployConfig(config);
14
+ console.log(result);
15
+ assert.equal(result.src, config.src);
16
+ assert.equal(result.dst, config.dst);
17
+ // console.log(new DeployConfig({
18
+ // src: 'test.js',
19
+ // }));
20
+ });
21
+ it('should default to home', function () {
22
+ let config = {
23
+ src: 'test.js',
24
+ };
25
+ let expected = '~/test.js';
26
+ let result = new DeployConfig(config);
27
+ console.log(result);
28
+ assert.equal(result.dst, expected);
29
+ });
30
+ });
31
+ });
package/test.css ADDED
@@ -0,0 +1,5 @@
1
+ /* test.css */
2
+ .yadda.yadda.yadda {
3
+ content: 'bla, bla, bla';
4
+ }
5
+ .something {}
package/test.js ADDED
@@ -0,0 +1,3 @@
1
+ // test.js
2
+ console.log('test');
3
+ // edit