Add smooth account editor transitions with immediate input release, keep the cursor visible in development and production windows, align host option descriptions, and ensure localized settings search placeholders fit at narrow widths. Bump the application to v2.1.22 and extend hidden Electron regression coverage.
This commit is contained in:
+10
-2
@@ -1,6 +1,10 @@
|
|||||||
function configureStartupRenderer(app, env = process.env, platform = process.platform) {
|
function configureStartupRenderer(app, env = process.env, platform = process.platform, argv = process.argv) {
|
||||||
const sessionName = String(env && env.SESSIONNAME || '');
|
const sessionName = String(env && env.SESSIONNAME || '');
|
||||||
if (platform === 'win32' && /^RDP-/i.test(sessionName)) app.disableHardwareAcceleration();
|
if (platform === 'win32' && /^RDP-/i.test(sessionName)) app.disableHardwareAcceleration();
|
||||||
|
if (Array.isArray(argv) && argv.includes('--dev')) {
|
||||||
|
app.commandLine.appendSwitch('force-prefers-no-reduced-motion');
|
||||||
|
app.commandLine.appendSwitch('user-data-dir', app.getPath('userData'));
|
||||||
|
}
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,7 +520,11 @@ function createStartupRendererHandlers({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createStartupWindow(BrowserWindow, options) {
|
function createStartupWindow(BrowserWindow, options) {
|
||||||
const window = new BrowserWindow({ ...options, show: false });
|
const window = new BrowserWindow({
|
||||||
|
...options,
|
||||||
|
show: false,
|
||||||
|
disableAutoHideCursor: true
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
window,
|
window,
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "2.1.21",
|
"version": "2.1.22",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "2.1.21",
|
"version": "2.1.22",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
"undici": "^7.29.0",
|
"undici": "^7.29.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "2.1.21",
|
"version": "2.1.22",
|
||||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+44
-7
@@ -5987,17 +5987,17 @@ function _buildHosterSettingsHtml(name) {
|
|||||||
<input id="${fieldPrefix}-max-size" type="number" class="hs-input" data-hoster="${name}" data-hs="maxSizeMb" value="${hs.maxSizeMb ?? 0}" min="0">
|
<input id="${fieldPrefix}-max-size" type="number" class="hs-input" data-hoster="${name}" data-hs="maxSizeMb" value="${hs.maxSizeMb ?? 0}" min="0">
|
||||||
<span class="hint">0 = unbegrenzt</span>
|
<span class="hint">0 = unbegrenzt</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-row">
|
<div class="settings-row account-hoster-option-row">
|
||||||
<label for="${fieldPrefix}-log">Links in Log schreiben</label>
|
<label for="${fieldPrefix}-log">Links in Log schreiben</label>
|
||||||
<input id="${fieldPrefix}-log" type="checkbox" class="hs-input" data-hoster="${name}" data-hs="logToFile" ${hs.logToFile !== false ? 'checked' : ''}>
|
<input id="${fieldPrefix}-log" type="checkbox" class="hs-input" data-hoster="${name}" data-hs="logToFile" ${hs.logToFile !== false ? 'checked' : ''}>
|
||||||
<span class="hint">Erfolgreiche Links in fileuploader.log.</span>
|
<span class="hint">Erfolgreiche Links in fileuploader.log.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-row">
|
<div class="settings-row account-hoster-option-row">
|
||||||
<label for="${fieldPrefix}-rotate">Accounts rotieren</label>
|
<label for="${fieldPrefix}-rotate">Accounts rotieren</label>
|
||||||
<input id="${fieldPrefix}-rotate" type="checkbox" class="hs-input" data-hoster="${name}" data-hs="rotateAccounts" ${hs.rotateAccounts === true ? 'checked' : ''}>
|
<input id="${fieldPrefix}-rotate" type="checkbox" class="hs-input" data-hoster="${name}" data-hs="rotateAccounts" ${hs.rotateAccounts === true ? 'checked' : ''}>
|
||||||
<span class="hint">Verteilt die Dateien reihum auf alle aktiven Accounts dieses Hosters (Datei 1 → Account 1, Datei 2 → Account 2 …). Hält z. B. byse-Accounts aktiv. Nur ein Account = kein Effekt.</span>
|
<span class="hint">Verteilt Dateien abwechselnd auf alle aktiven Accounts dieses Hosters. Bei nur einem Account hat die Option keinen Effekt.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-row">
|
<div class="settings-row account-hoster-option-row">
|
||||||
<label for="${fieldPrefix}-size-memo">Größen-Limit merken</label>
|
<label for="${fieldPrefix}-size-memo">Größen-Limit merken</label>
|
||||||
<input id="${fieldPrefix}-size-memo" type="checkbox" class="hs-input" data-hoster="${name}" data-hs="sizeMemoEnabled" ${hs.sizeMemoEnabled !== false ? 'checked' : ''}>
|
<input id="${fieldPrefix}-size-memo" type="checkbox" class="hs-input" data-hoster="${name}" data-hs="sizeMemoEnabled" ${hs.sizeMemoEnabled !== false ? 'checked' : ''}>
|
||||||
<span class="hint">Überspringt nach zwei verdächtigen Ablehnungen auf einem Account größere Dateien dort vorab ("Bekanntes Größen-Limit"). Abschalten = jede Datei wird immer wirklich versucht.</span>
|
<span class="hint">Überspringt nach zwei verdächtigen Ablehnungen auf einem Account größere Dateien dort vorab ("Bekanntes Größen-Limit"). Abschalten = jede Datei wird immer wirklich versucht.</span>
|
||||||
@@ -6353,7 +6353,7 @@ function getCredsFieldsHtml(authType, account, hoster) {
|
|||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label for="accField_password">Passwort</label>
|
<label for="accField_password">Passwort</label>
|
||||||
<input type="password" class="key-input" id="accField_password" name="password" autocomplete="current-password" value="${escapeAttr(account.password || '')}" placeholder="Passwort">
|
<input type="password" class="key-input" id="accField_password" name="password" autocomplete="current-password" value="${escapeAttr(account.password || '')}" placeholder="Passwort">
|
||||||
<button class="toggle-vis" type="button" title="Passwort anzeigen" aria-label="Passwort anzeigen" aria-pressed="false">👁</button>
|
<button class="toggle-vis" type="button" title="Passwort anzeigen" aria-label="Passwort anzeigen" aria-pressed="false">👁️</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
// API key
|
// API key
|
||||||
@@ -6361,7 +6361,7 @@ function getCredsFieldsHtml(authType, account, hoster) {
|
|||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label for="accField_apiKey">API-Key</label>
|
<label for="accField_apiKey">API-Key</label>
|
||||||
<input type="password" class="key-input" id="accField_apiKey" name="apiKey" autocomplete="off" spellcheck="false" value="${escapeAttr(account.apiKey || '')}" placeholder="API-Key">
|
<input type="password" class="key-input" id="accField_apiKey" name="apiKey" autocomplete="off" spellcheck="false" value="${escapeAttr(account.apiKey || '')}" placeholder="API-Key">
|
||||||
<button class="toggle-vis" type="button" title="API-Key anzeigen" aria-label="API-Key anzeigen" aria-pressed="false">👁</button>
|
<button class="toggle-vis" type="button" title="API-Key anzeigen" aria-label="API-Key anzeigen" aria-pressed="false">👁️</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6429,13 +6429,50 @@ function openAccountModal(editAccountId) {
|
|||||||
onEscape: closeAccountModal,
|
onEscape: closeAccountModal,
|
||||||
onBackdrop: closeAccountModal
|
onBackdrop: closeAccountModal
|
||||||
});
|
});
|
||||||
|
_openAccountModalMotion(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _accountModalMotionToken = 0;
|
||||||
|
|
||||||
|
function _openAccountModalMotion(modal) {
|
||||||
|
const card = modal.querySelector('.modal-card');
|
||||||
|
const token = ++_accountModalMotionToken;
|
||||||
|
modal.classList.remove('account-modal-opening', 'account-modal-closing');
|
||||||
|
if (!card) return;
|
||||||
|
void card.offsetHeight;
|
||||||
|
modal.classList.add('account-modal-opening');
|
||||||
|
const finish = event => {
|
||||||
|
if (event && event.target !== card) return;
|
||||||
|
card.removeEventListener('animationend', finish);
|
||||||
|
if (Object.is(token, _accountModalMotionToken)) modal.classList.remove('account-modal-opening');
|
||||||
|
};
|
||||||
|
card.addEventListener('animationend', finish);
|
||||||
|
window.setTimeout(() => finish(), 440);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeAccountModal() {
|
function closeAccountModal() {
|
||||||
modalController.close('accountModal', { fallbackFocus: '#addAccountBtn' });
|
const modal = document.getElementById('accountModal');
|
||||||
|
if (!modalController.isOpen(modal) || modal.classList.contains('account-modal-closing')) return;
|
||||||
|
const card = modal.querySelector('.modal-card');
|
||||||
|
const token = ++_accountModalMotionToken;
|
||||||
|
modal.classList.remove('account-modal-opening', 'account-modal-closing');
|
||||||
|
if (card) void card.offsetHeight;
|
||||||
|
modalController.close(modal, { fallbackFocus: '#addAccountBtn' });
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
modal.inert = true;
|
||||||
|
modal.classList.add('account-modal-closing');
|
||||||
_hideOtpField();
|
_hideOtpField();
|
||||||
editingAccountId = null;
|
editingAccountId = null;
|
||||||
_resetAccountModalState();
|
_resetAccountModalState();
|
||||||
|
const finish = event => {
|
||||||
|
if (event && event.target !== card) return;
|
||||||
|
if (card) card.removeEventListener('animationend', finish);
|
||||||
|
if (!Object.is(token, _accountModalMotionToken)) return;
|
||||||
|
modal.classList.remove('account-modal-closing');
|
||||||
|
modal.style.display = 'none';
|
||||||
|
};
|
||||||
|
if (card) card.addEventListener('animationend', finish);
|
||||||
|
window.setTimeout(() => finish(), 380);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDeleteAccountModal(accountId) {
|
function openDeleteAccountModal(accountId) {
|
||||||
|
|||||||
+1
-1
@@ -237,7 +237,7 @@
|
|||||||
['Hoster-Einstellungen', 'Host settings'],
|
['Hoster-Einstellungen', 'Host settings'],
|
||||||
['Upload-Einstellungen', 'Upload settings'],
|
['Upload-Einstellungen', 'Upload settings'],
|
||||||
['Erfolgreiche Links in fileuploader.log.', 'Successful links in fileuploader.log.'],
|
['Erfolgreiche Links in fileuploader.log.', 'Successful links in fileuploader.log.'],
|
||||||
['Verteilt die Dateien reihum auf alle aktiven Accounts dieses Hosters (Datei 1 → Account 1, Datei 2 → Account 2 …). Hält z. B. byse-Accounts aktiv. Nur ein Account = kein Effekt.', 'Distributes files across all active accounts for this host in round-robin order (file 1 → account 1, file 2 → account 2, and so on). Keeps accounts such as byse active. Has no effect with only one account.'],
|
['Verteilt Dateien abwechselnd auf alle aktiven Accounts dieses Hosters. Bei nur einem Account hat die Option keinen Effekt.', 'Distributes files alternately across all active accounts for this host. The option has no effect with only one account.'],
|
||||||
['Größen-Limit merken', 'Remember size limit'],
|
['Größen-Limit merken', 'Remember size limit'],
|
||||||
['Überspringt nach zwei verdächtigen Ablehnungen auf einem Account größere Dateien dort vorab ("Bekanntes Größen-Limit"). Abschalten = jede Datei wird immer wirklich versucht.', 'After two suspicious rejections on an account, larger files are skipped there in advance ("Known size limit"). When disabled, every file is always attempted.'],
|
['Überspringt nach zwei verdächtigen Ablehnungen auf einem Account größere Dateien dort vorab ("Bekanntes Größen-Limit"). Abschalten = jede Datei wird immer wirklich versucht.', 'After two suspicious rejections on an account, larger files are skipped there in advance ("Known size limit"). When disabled, every file is always attempted.'],
|
||||||
['Einstellungen einzelner Hoster', 'Settings for individual hosts'],
|
['Einstellungen einzelner Hoster', 'Settings for individual hosts'],
|
||||||
|
|||||||
+66
-1
@@ -29,6 +29,7 @@
|
|||||||
body {
|
body {
|
||||||
font-family: 'Aptos', 'Segoe UI Variable Text', 'Bahnschrift', sans-serif;
|
font-family: 'Aptos', 'Segoe UI Variable Text', 'Bahnschrift', sans-serif;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
|
cursor: default;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
@@ -1287,6 +1288,29 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
|||||||
.account-hoster-settings-header .panel-arrow { font-size: 10px; color: var(--text-dim); }
|
.account-hoster-settings-header .panel-arrow { font-size: 10px; color: var(--text-dim); }
|
||||||
.account-hoster-settings-body-inner { padding: 4px 12px 10px; }
|
.account-hoster-settings-body-inner { padding: 4px 12px 10px; }
|
||||||
|
|
||||||
|
.account-hoster-settings-body-inner .account-hoster-option-row {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 150px minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-hoster-settings-body-inner .account-hoster-option-row label {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-hoster-settings-body-inner .account-hoster-option-row .hint {
|
||||||
|
grid-column: 2;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-hoster-settings-body-inner .account-hoster-option-row input[type="checkbox"] {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 1;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-hoster-pointer {
|
.settings-hoster-pointer {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
@@ -3456,7 +3480,7 @@ input[type="checkbox"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#settingsSearchInput {
|
#settingsSearchInput {
|
||||||
font-size: 13px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-nav-button {
|
.settings-nav-button {
|
||||||
@@ -3753,6 +3777,47 @@ input[type="checkbox"] {
|
|||||||
box-shadow: 0 24px 64px rgba(0, 0, 0, .48);
|
box-shadow: 0 24px 64px rgba(0, 0, 0, .48);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#accountModal.account-modal-opening {
|
||||||
|
animation: account-modal-dim-in 360ms cubic-bezier(.16, 1, .3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
#accountModal.account-modal-opening > .modal-card {
|
||||||
|
transform-origin: top center;
|
||||||
|
animation: account-modal-unfold 360ms cubic-bezier(.16, 1, .3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
#accountModal.account-modal-closing {
|
||||||
|
pointer-events: none;
|
||||||
|
animation: account-modal-dim-out 300ms cubic-bezier(.4, 0, .2, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
#accountModal.account-modal-closing > .modal-card {
|
||||||
|
transform-origin: top center;
|
||||||
|
animation: account-modal-fold 300ms cubic-bezier(.4, 0, .2, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes account-modal-dim-in {
|
||||||
|
from { background: rgba(0, 0, 0, 0); }
|
||||||
|
to { background: rgba(0, 0, 0, .6); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes account-modal-dim-out {
|
||||||
|
from { background: rgba(0, 0, 0, .6); }
|
||||||
|
to { background: rgba(0, 0, 0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes account-modal-unfold {
|
||||||
|
0% { opacity: 0; clip-path: inset(0 0 18% 0 round 12px); transform: translateY(-16px) scale(.985); }
|
||||||
|
55% { opacity: .96; }
|
||||||
|
100% { opacity: 1; clip-path: inset(0 round 12px); transform: translateY(0) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes account-modal-fold {
|
||||||
|
0% { opacity: 1; clip-path: inset(0 round 12px); transform: translateY(0) scale(1); }
|
||||||
|
45% { opacity: .9; }
|
||||||
|
100% { opacity: 0; clip-path: inset(0 0 18% 0 round 12px); transform: translateY(-16px) scale(.985); }
|
||||||
|
}
|
||||||
|
|
||||||
.modal-header,
|
.modal-header,
|
||||||
.modal-footer {
|
.modal-footer {
|
||||||
padding: 16px 18px;
|
padding: 16px 18px;
|
||||||
|
|||||||
@@ -139,6 +139,32 @@ test('configureStartupRenderer disables hardware acceleration for a Windows Remo
|
|||||||
assert.equal(calls, 1);
|
assert.equal(calls, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('configureStartupRenderer forces full motion only for hot dev', () => {
|
||||||
|
const devSwitches = [];
|
||||||
|
const releaseSwitches = [];
|
||||||
|
const createApp = switches => ({
|
||||||
|
disableHardwareAcceleration() {},
|
||||||
|
getPath(name) {
|
||||||
|
assert.equal(name, 'userData');
|
||||||
|
return 'C:\\ReleaseTest\\user-data';
|
||||||
|
},
|
||||||
|
commandLine: {
|
||||||
|
appendSwitch(name, value) {
|
||||||
|
switches.push({ name, value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
configureStartupRenderer(createApp(devSwitches), { SESSIONNAME: 'Console' }, 'win32', ['electron', '.', '--dev']);
|
||||||
|
configureStartupRenderer(createApp(releaseSwitches), { SESSIONNAME: 'Console' }, 'win32', ['Multi-Hoster-Upload.exe']);
|
||||||
|
|
||||||
|
assert.deepEqual(devSwitches, [
|
||||||
|
{ name: 'force-prefers-no-reduced-motion', value: undefined },
|
||||||
|
{ name: 'user-data-dir', value: 'C:\\ReleaseTest\\user-data' }
|
||||||
|
]);
|
||||||
|
assert.deepEqual(releaseSwitches, []);
|
||||||
|
});
|
||||||
|
|
||||||
test('resolveStartupLanguage accepts only the supported persisted language', () => {
|
test('resolveStartupLanguage accepts only the supported persisted language', () => {
|
||||||
assert.equal(resolveStartupLanguage({ globalSettings: { language: 'de' } }), 'de');
|
assert.equal(resolveStartupLanguage({ globalSettings: { language: 'de' } }), 'de');
|
||||||
assert.equal(resolveStartupLanguage({ globalSettings: { language: 'en' } }), 'en');
|
assert.equal(resolveStartupLanguage({ globalSettings: { language: 'en' } }), 'en');
|
||||||
@@ -147,10 +173,15 @@ test('resolveStartupLanguage accepts only the supported persisted language', ()
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('createStartupWindow forces the main window to start hidden', () => {
|
test('createStartupWindow forces the main window to start hidden', () => {
|
||||||
const startup = createStartupWindow(TestBrowserWindow, { width: 1100, show: true });
|
const startup = createStartupWindow(TestBrowserWindow, {
|
||||||
|
width: 1100,
|
||||||
|
show: true,
|
||||||
|
disableAutoHideCursor: false
|
||||||
|
});
|
||||||
|
|
||||||
assert.equal(startup.window.options.width, 1100);
|
assert.equal(startup.window.options.width, 1100);
|
||||||
assert.equal(startup.window.options.show, false);
|
assert.equal(startup.window.options.show, false);
|
||||||
|
assert.equal(startup.window.options.disableAutoHideCursor, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('main window uses the branded application icon', () => {
|
test('main window uses the branded application icon', () => {
|
||||||
|
|||||||
+113
-3
@@ -167,6 +167,7 @@ setTimeout(async () => {
|
|||||||
if (win.isMaximized()) win.unmaximize();
|
if (win.isMaximized()) win.unmaximize();
|
||||||
const wc = win.webContents;
|
const wc = win.webContents;
|
||||||
if (typeof wc.setFrameRate === 'function') wc.setFrameRate(60);
|
if (typeof wc.setFrameRate === 'function') wc.setFrameRate(60);
|
||||||
|
const initialReducedMotion = await wc.executeJavaScript('matchMedia("(prefers-reduced-motion: reduce)").matches');
|
||||||
if (!wc.debugger.isAttached()) wc.debugger.attach('1.3');
|
if (!wc.debugger.isAttached()) wc.debugger.attach('1.3');
|
||||||
await wc.debugger.sendCommand('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-reduced-motion', value: 'no-preference' }] });
|
await wc.debugger.sendCommand('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-reduced-motion', value: 'no-preference' }] });
|
||||||
const setWindowBounds = async bounds => {
|
const setWindowBounds = async bounds => {
|
||||||
@@ -218,6 +219,7 @@ setTimeout(async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
check('Every UI smoke window stays offscreen and never takes focus', hiddenWindowHarness.areNativeSurfacesSuppressed(hiddenWindowHarness.getWindows()));
|
check('Every UI smoke window stays offscreen and never takes focus', hiddenWindowHarness.areNativeSurfacesSuppressed(hiddenWindowHarness.getWindows()));
|
||||||
|
check('Hot Dev forces full motion despite the Windows reduced-motion preference', initialReducedMotion === false);
|
||||||
const startupUpdateState = await wc.executeJavaScript('(() => { const button = document.getElementById("headerUpdateBtn"); return [_knownUpdateInfo?.remoteVersion, button?.hidden, getComputedStyle(button).display, document.getElementById("updateBanner")?.style.display].join("|"); })()');
|
const startupUpdateState = await wc.executeJavaScript('(() => { const button = document.getElementById("headerUpdateBtn"); return [_knownUpdateInfo?.remoteVersion, button?.hidden, getComputedStyle(button).display, document.getElementById("updateBanner")?.style.display].join("|"); })()');
|
||||||
check('Startup update survives pending renderer initialization', startupUpdateState === '9.9.8|false|flex|flex');
|
check('Startup update survives pending renderer initialization', startupUpdateState === '9.9.8|false|flex|flex');
|
||||||
await wc.executeJavaScript('_knownUpdateInfo = null; closeUpdateDialog(); _syncHeaderUpdateState();');
|
await wc.executeJavaScript('_knownUpdateInfo = null; closeUpdateDialog(); _syncHeaderUpdateState();');
|
||||||
@@ -317,6 +319,9 @@ setTimeout(async () => {
|
|||||||
const appHeaderExists = await wc.executeJavaScript('Boolean(document.querySelector(".app-header"))');
|
const appHeaderExists = await wc.executeJavaScript('Boolean(document.querySelector(".app-header"))');
|
||||||
check('App shell exposes the primary header', appHeaderExists);
|
check('App shell exposes the primary header', appHeaderExists);
|
||||||
|
|
||||||
|
const cursorContract = await wc.executeJavaScript('[getComputedStyle(document.body).cursor, getComputedStyle(document.getElementById("settings-tab")).cursor].join("|")');
|
||||||
|
check('Main surface keeps a visible default cursor and interactive controls keep the pointer cursor', cursorContract === 'default|pointer');
|
||||||
|
|
||||||
const appBrandText = await wc.executeJavaScript('document.querySelector(".app-brand-name")?.textContent?.trim()');
|
const appBrandText = await wc.executeJavaScript('document.querySelector(".app-brand-name")?.textContent?.trim()');
|
||||||
check('App header shows the Multi Hoster Uploader brand', appBrandText === 'MULTI HOSTER UPLOADER');
|
check('App header shows the Multi Hoster Uploader brand', appBrandText === 'MULTI HOSTER UPLOADER');
|
||||||
|
|
||||||
@@ -980,15 +985,96 @@ setTimeout(async () => {
|
|||||||
check('Password visibility action exposes its state', passwordToggleState === 'Passwort verbergen|true');
|
check('Password visibility action exposes its state', passwordToggleState === 'Passwort verbergen|true');
|
||||||
|
|
||||||
await wc.executeJavaScript('document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }))');
|
await wc.executeJavaScript('document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }))');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 340));
|
||||||
const accountModalHidden = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display');
|
const accountModalHidden = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display');
|
||||||
check('Escape closes account modal', accountModalHidden === 'none');
|
check('Escape closes account modal', accountModalHidden === 'none');
|
||||||
|
|
||||||
const restoredAccountFocus = await wc.executeJavaScript('document.activeElement?.hasAttribute("data-account-empty-add") || document.activeElement?.id === "addAccountBtn"');
|
const restoredAccountFocus = await wc.executeJavaScript('document.activeElement?.hasAttribute("data-account-empty-add") || document.activeElement?.id === "addAccountBtn"');
|
||||||
check('Account modal restores trigger focus', restoredAccountFocus === true);
|
check('Account modal restores trigger focus', restoredAccountFocus === true);
|
||||||
|
|
||||||
const fallbackAccountFocus = await wc.executeJavaScript('(() => { const trigger = document.querySelector("[data-account-empty-add]") || document.getElementById("addAccountBtn"); trigger.focus(); trigger.click(); document.querySelector("[data-account-empty-add]")?.remove(); document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); return document.activeElement?.id; })()');
|
await wc.executeJavaScript('(() => { const trigger = document.querySelector("[data-account-empty-add]") || document.getElementById("addAccountBtn"); trigger.focus(); trigger.click(); document.querySelector("[data-account-empty-add]")?.remove(); document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); })()');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 340));
|
||||||
|
const fallbackAccountFocus = await wc.executeJavaScript('document.activeElement?.id');
|
||||||
check('Account modal restores stable focus after list rerender', fallbackAccountFocus === 'addAccountBtn');
|
check('Account modal restores stable focus after list rerender', fallbackAccountFocus === 'addAccountBtn');
|
||||||
|
|
||||||
|
ipcMain.removeHandler('validate-credentials');
|
||||||
|
ipcMain.handle('validate-credentials', () => ({ ok: true, status: 'ok', message: 'Credentials verified', checkedAt: '2033-01-02T03:04:05.000Z' }));
|
||||||
|
ipcMain.removeHandler('save-config');
|
||||||
|
ipcMain.handle('save-config', () => true);
|
||||||
|
await wc.executeJavaScript(\`(() => {
|
||||||
|
HOSTERS.forEach(name => { config.hosters[name] = []; });
|
||||||
|
config.hosters['byse.sx'] = [{ id: 'ui-animated-account', enabled: true, authType: 'api', apiKey: 'animated-key' }];
|
||||||
|
accountStatuses = { 'ui-animated-account': { status: 'ok', message: 'Ready' } };
|
||||||
|
renderAccounts();
|
||||||
|
document.querySelector('[data-account-edit="ui-animated-account"]')?.click();
|
||||||
|
})()\`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 60));
|
||||||
|
const accountEditOpeningFrame = await wc.executeJavaScript(\`(() => {
|
||||||
|
const modal = document.getElementById('accountModal');
|
||||||
|
const card = modal?.querySelector('.modal-card');
|
||||||
|
const style = card ? getComputedStyle(card) : null;
|
||||||
|
return {
|
||||||
|
title: document.getElementById('accountModalTitle')?.textContent,
|
||||||
|
display: modal?.style.display,
|
||||||
|
animation: style?.animationName || 'none',
|
||||||
|
duration: parseFloat(style?.animationDuration || '0'),
|
||||||
|
transform: style?.transform || 'none',
|
||||||
|
clipPath: style?.clipPath || 'none',
|
||||||
|
opacity: Number(style?.opacity || 1),
|
||||||
|
overlayAnimation: getComputedStyle(modal).animationName,
|
||||||
|
overlayAlpha: Number((getComputedStyle(modal).backgroundColor.match(/[0-9.]+/g) || [0, 0, 0, 0])[3] || 0)
|
||||||
|
};
|
||||||
|
})()\`);
|
||||||
|
check('Editing an account unfolds the editor through a real opening frame', accountEditOpeningFrame.title === 'Account bearbeiten' && accountEditOpeningFrame.display === 'flex' && accountEditOpeningFrame.animation !== 'none' && accountEditOpeningFrame.duration >= .34 && (accountEditOpeningFrame.transform !== 'none' || accountEditOpeningFrame.clipPath !== 'none' || accountEditOpeningFrame.opacity < 1));
|
||||||
|
check('Opening the account editor softly fades in the surrounding dimming', accountEditOpeningFrame.overlayAnimation !== 'none' && accountEditOpeningFrame.overlayAlpha > 0 && accountEditOpeningFrame.overlayAlpha < .6);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 340));
|
||||||
|
const apiEyeEmoji = await wc.executeJavaScript('document.querySelector("#accountCredsFields .toggle-vis")?.textContent');
|
||||||
|
check('API key visibility uses the normal eye emoji', apiEyeEmoji === '👁️');
|
||||||
|
|
||||||
|
await wc.executeJavaScript('document.getElementById("closeAccountModalBtn")?.click()');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 60));
|
||||||
|
const accountEditClosingFrame = await wc.executeJavaScript(\`(() => {
|
||||||
|
const modal = document.getElementById('accountModal');
|
||||||
|
const card = modal?.querySelector('.modal-card');
|
||||||
|
const style = card ? getComputedStyle(card) : null;
|
||||||
|
return {
|
||||||
|
display: modal?.style.display,
|
||||||
|
ariaHidden: modal?.getAttribute('aria-hidden'),
|
||||||
|
modalInert: modal?.inert,
|
||||||
|
headerInert: document.querySelector('.app-header')?.inert,
|
||||||
|
activeViewInert: document.querySelector('.view.active')?.inert,
|
||||||
|
animation: style?.animationName || 'none',
|
||||||
|
duration: parseFloat(style?.animationDuration || '0'),
|
||||||
|
transform: style?.transform || 'none',
|
||||||
|
clipPath: style?.clipPath || 'none',
|
||||||
|
opacity: Number(style?.opacity || 1),
|
||||||
|
overlayAnimation: getComputedStyle(modal).animationName,
|
||||||
|
overlayAlpha: Number((getComputedStyle(modal).backgroundColor.match(/[0-9.]+/g) || [0, 0, 0, 0])[3] || 0)
|
||||||
|
};
|
||||||
|
})()\`);
|
||||||
|
check('The close button folds the account editor upward without blocking the application', accountEditClosingFrame.display === 'flex' && accountEditClosingFrame.ariaHidden === 'true' && accountEditClosingFrame.modalInert === true && accountEditClosingFrame.headerInert === false && accountEditClosingFrame.activeViewInert === false && accountEditClosingFrame.animation !== 'none' && accountEditClosingFrame.duration >= .28 && (accountEditClosingFrame.transform !== 'none' || accountEditClosingFrame.clipPath !== 'none' || accountEditClosingFrame.opacity < 1));
|
||||||
|
check('Closing the account editor softly fades out the surrounding dimming', accountEditClosingFrame.overlayAnimation !== 'none' && accountEditClosingFrame.overlayAlpha > 0 && accountEditClosingFrame.overlayAlpha < .6);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 340));
|
||||||
|
const accountEditClosed = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display === "none"');
|
||||||
|
check('The account editor hides after its closing animation', accountEditClosed === true);
|
||||||
|
|
||||||
|
await wc.executeJavaScript('document.querySelector("[data-account-edit=ui-animated-account]")?.click()');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 400));
|
||||||
|
await wc.executeJavaScript('document.getElementById("saveAccountBtn")?.click()');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 680));
|
||||||
|
const accountSaveClosingFrame = await wc.executeJavaScript(\`(() => {
|
||||||
|
const modal = document.getElementById('accountModal');
|
||||||
|
const card = modal?.querySelector('.modal-card');
|
||||||
|
const style = card ? getComputedStyle(card) : null;
|
||||||
|
return { display: modal?.style.display, animation: style?.animationName || 'none' };
|
||||||
|
})()\`);
|
||||||
|
check('A verified saved account uses the same closing animation', accountSaveClosingFrame.display === 'flex' && accountSaveClosingFrame.animation !== 'none');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 340));
|
||||||
|
const accountSaveClosed = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display === "none"');
|
||||||
|
check('A verified saved account hides after the closing animation', accountSaveClosed === true);
|
||||||
|
restoreInitialIpcHandler('validate-credentials');
|
||||||
|
restoreInitialIpcHandler('save-config');
|
||||||
|
|
||||||
await setWindowBounds({ ...win.getBounds(), width: 1280, height: 720 });
|
await setWindowBounds({ ...win.getBounds(), width: 1280, height: 720 });
|
||||||
|
|
||||||
const emptyAccountsGeometry = await wc.executeJavaScript(\`(() => {
|
const emptyAccountsGeometry = await wc.executeJavaScript(\`(() => {
|
||||||
@@ -1037,6 +1123,28 @@ setTimeout(async () => {
|
|||||||
check('Hoster groups visibly animate while opening and closing', hasSmoothAccountCollapse(accountCollapseMotion.hosterMotion));
|
check('Hoster groups visibly animate while opening and closing', hasSmoothAccountCollapse(accountCollapseMotion.hosterMotion));
|
||||||
check('Hoster upload settings visibly animate while opening and closing', hasSmoothAccountCollapse(accountCollapseMotion.settingsMotion));
|
check('Hoster upload settings visibly animate while opening and closing', hasSmoothAccountCollapse(accountCollapseMotion.settingsMotion));
|
||||||
|
|
||||||
|
const accountSettingsDescriptionLayout = await wc.executeJavaScript(\`(async () => {
|
||||||
|
const hoster = HOSTERS[0];
|
||||||
|
const settings = document.querySelector('[data-hoster-settings-toggle="' + hoster + '"]');
|
||||||
|
if (settings && settings.getAttribute('aria-expanded') !== 'true') settings.click();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 240));
|
||||||
|
const container = document.querySelector('.account-hoster-settings-body-inner');
|
||||||
|
const rows = [...document.querySelectorAll('.account-hoster-option-row')];
|
||||||
|
const containerRect = container?.getBoundingClientRect();
|
||||||
|
return rows.map(row => {
|
||||||
|
const rowRect = row.getBoundingClientRect();
|
||||||
|
const hintRect = row.querySelector('.hint')?.getBoundingClientRect();
|
||||||
|
const inputRect = row.querySelector('input')?.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
widthRatio: containerRect ? rowRect.width / containerRect.width : 0,
|
||||||
|
descriptionBeforeToggle: Boolean(hintRect && inputRect && hintRect.right <= inputRect.left)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})()\`);
|
||||||
|
const accountSettingsDescriptionLayoutClean = accountSettingsDescriptionLayout.length === 3 && accountSettingsDescriptionLayout.every(row => row.widthRatio >= .97 && row.descriptionBeforeToggle);
|
||||||
|
if (!accountSettingsDescriptionLayoutClean) console.log('Account settings description layout:', JSON.stringify(accountSettingsDescriptionLayout));
|
||||||
|
check('Account host options give descriptions a full clean row with toggles aligned right', accountSettingsDescriptionLayoutClean);
|
||||||
|
|
||||||
const accountsFooterGeometry = await wc.executeJavaScript(\`(() => {
|
const accountsFooterGeometry = await wc.executeJavaScript(\`(() => {
|
||||||
const main = document.querySelector('#accounts-view .accounts-main')?.getBoundingClientRect();
|
const main = document.querySelector('#accounts-view .accounts-main')?.getBoundingClientRect();
|
||||||
const list = document.getElementById('accountsList')?.getBoundingClientRect();
|
const list = document.getElementById('accountsList')?.getBoundingClientRect();
|
||||||
@@ -1394,6 +1502,8 @@ setTimeout(async () => {
|
|||||||
check('Settings search icon aligns to the input text line', settingsSearchIconAlignment === 'flex|center|true');
|
check('Settings search icon aligns to the input text line', settingsSearchIconAlignment === 'flex|center|true');
|
||||||
const settingsSearchControlGeometry = await wc.executeJavaScript('(() => { const control = document.querySelector(".settings-search-control"); const input = document.getElementById("settingsSearchInput"); const icon = document.querySelector(".settings-search-icon"); const svg = icon?.querySelector("svg"); if (!control || !input || !icon || !svg) return "missing"; const controlRect = control.getBoundingClientRect(); const inputRect = input.getBoundingClientRect(); const iconRect = icon.getBoundingClientRect(); const inputStyle = getComputedStyle(input); const iconStyle = getComputedStyle(icon); return [Math.round(controlRect.height), Math.round(inputRect.height), Math.round(Math.abs((inputRect.top + inputRect.height / 2) - (iconRect.top + iconRect.height / 2))), svg.getAttribute("viewBox"), inputStyle.lineHeight, inputStyle.paddingTop, inputStyle.paddingBottom, iconStyle.display, iconStyle.alignItems].join("|"); })()');
|
const settingsSearchControlGeometry = await wc.executeJavaScript('(() => { const control = document.querySelector(".settings-search-control"); const input = document.getElementById("settingsSearchInput"); const icon = document.querySelector(".settings-search-icon"); const svg = icon?.querySelector("svg"); if (!control || !input || !icon || !svg) return "missing"; const controlRect = control.getBoundingClientRect(); const inputRect = input.getBoundingClientRect(); const iconRect = icon.getBoundingClientRect(); const inputStyle = getComputedStyle(input); const iconStyle = getComputedStyle(icon); return [Math.round(controlRect.height), Math.round(inputRect.height), Math.round(Math.abs((inputRect.top + inputRect.height / 2) - (iconRect.top + iconRect.height / 2))), svg.getAttribute("viewBox"), inputStyle.lineHeight, inputStyle.paddingTop, inputStyle.paddingBottom, iconStyle.display, iconStyle.alignItems].join("|"); })()');
|
||||||
check('Settings search control keeps icon and text on one shared center line', settingsSearchControlGeometry === '44|44|0|0 0 24 24|18px|0px|0px|flex|center');
|
check('Settings search control keeps icon and text on one shared center line', settingsSearchControlGeometry === '44|44|0|0 0 24 24|18px|0px|0px|flex|center');
|
||||||
|
const settingsSearchPlaceholderFit = await wc.executeJavaScript('(() => { const input = document.getElementById("settingsSearchInput"); if (!input) return false; const style = getComputedStyle(input); const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); context.font = style.font; const available = input.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight); return ["Einstellungen durchsuchen", "Search settings"].every(text => context.measureText(text).width <= available - 12); })()');
|
||||||
|
check('Settings search placeholders retain visible right-side breathing room', settingsSearchPlaceholderFit === true);
|
||||||
|
|
||||||
await wc.executeJavaScript('document.querySelector("[data-settings-page=\\'logs\\']")?.click()');
|
await wc.executeJavaScript('document.querySelector("[data-settings-page=\\'logs\\']")?.click()');
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
@@ -1454,7 +1564,7 @@ setTimeout(async () => {
|
|||||||
const plaintextCredentialOverride = await wc.executeJavaScript('(() => ({ control: document.getElementById("allowPlaintextCredentialStorageInput"), copy: document.body.textContent.includes("Unsichere Klartext-Speicherung"), bridge: typeof window.api.getSecretStoreStatus }))()');
|
const plaintextCredentialOverride = await wc.executeJavaScript('(() => ({ control: document.getElementById("allowPlaintextCredentialStorageInput"), copy: document.body.textContent.includes("Unsichere Klartext-Speicherung"), bridge: typeof window.api.getSecretStoreStatus }))()');
|
||||||
check('Settings expose no plaintext credential storage override', plaintextCredentialOverride.control === null && plaintextCredentialOverride.copy === false && plaintextCredentialOverride.bridge === 'undefined');
|
check('Settings expose no plaintext credential storage override', plaintextCredentialOverride.control === null && plaintextCredentialOverride.copy === false && plaintextCredentialOverride.bridge === 'undefined');
|
||||||
const settingsTypography = await wc.executeJavaScript('(() => { const size = selector => parseFloat(getComputedStyle(document.querySelector(selector)).fontSize); return { heading: size(".settings-subpage.active .settings-page-header h3"), intro: size(".settings-subpage.active .settings-page-header p"), section: size(".settings-subpage.active .settings-section-label"), rowLabel: size(".settings-subpage.active .settings-row > label"), hint: size(".settings-subpage.active .hint"), optionLabel: size(".settings-subpage.active .settings-option-copy label"), optionDescription: size(".settings-subpage.active .settings-option-description"), navigation: size(".settings-nav-button"), search: size("#settingsSearchInput") }; })()');
|
const settingsTypography = await wc.executeJavaScript('(() => { const size = selector => parseFloat(getComputedStyle(document.querySelector(selector)).fontSize); return { heading: size(".settings-subpage.active .settings-page-header h3"), intro: size(".settings-subpage.active .settings-page-header p"), section: size(".settings-subpage.active .settings-section-label"), rowLabel: size(".settings-subpage.active .settings-row > label"), hint: size(".settings-subpage.active .hint"), optionLabel: size(".settings-subpage.active .settings-option-copy label"), optionDescription: size(".settings-subpage.active .settings-option-description"), navigation: size(".settings-nav-button"), search: size("#settingsSearchInput") }; })()');
|
||||||
check('Settings use the enlarged readable typography scale', settingsTypography.heading >= 22 && settingsTypography.intro >= 14 && settingsTypography.section >= 12 && settingsTypography.rowLabel >= 14 && settingsTypography.hint >= 12 && settingsTypography.optionLabel >= 14 && settingsTypography.optionDescription >= 12 && settingsTypography.navigation >= 13 && settingsTypography.search >= 13);
|
check('Settings use the enlarged readable typography scale', settingsTypography.heading >= 22 && settingsTypography.intro >= 14 && settingsTypography.section >= 12 && settingsTypography.rowLabel >= 14 && settingsTypography.hint >= 12 && settingsTypography.optionLabel >= 14 && settingsTypography.optionDescription >= 12 && settingsTypography.navigation >= 13 && settingsTypography.search >= 11);
|
||||||
const settingsSelection = await wc.executeJavaScript('(() => ({ heading: getComputedStyle(document.querySelector(".settings-subpage.active .settings-page-header h3")).userSelect, hint: getComputedStyle(document.querySelector(".settings-subpage.active .hint")).userSelect, input: getComputedStyle(document.getElementById("globalMaxSpeedMbsInput")).userSelect }))()');
|
const settingsSelection = await wc.executeJavaScript('(() => ({ heading: getComputedStyle(document.querySelector(".settings-subpage.active .settings-page-header h3")).userSelect, hint: getComputedStyle(document.querySelector(".settings-subpage.active .hint")).userSelect, input: getComputedStyle(document.getElementById("globalMaxSpeedMbsInput")).userSelect }))()');
|
||||||
check('Settings interface copy cannot be selected while input values remain selectable', settingsSelection.heading === 'none' && settingsSelection.hint === 'none' && settingsSelection.input === 'text');
|
check('Settings interface copy cannot be selected while input values remain selectable', settingsSelection.heading === 'none' && settingsSelection.hint === 'none' && settingsSelection.input === 'text');
|
||||||
const enlargedSettingsFit = await wc.executeJavaScript('(() => { const results = [...document.querySelectorAll(".settings-nav-button")].map(button => { button.click(); const page = document.querySelector(".settings-subpage.active"); return Boolean(page && page.scrollWidth <= page.clientWidth + 1); }); document.querySelector("[data-settings-page=uploads]")?.click(); return results.every(Boolean); })()');
|
const enlargedSettingsFit = await wc.executeJavaScript('(() => { const results = [...document.querySelectorAll(".settings-nav-button")].map(button => { button.click(); const page = document.querySelector(".settings-subpage.active"); return Boolean(page && page.scrollWidth <= page.clientWidth + 1); }); document.querySelector("[data-settings-page=uploads]")?.click(); return results.every(Boolean); })()');
|
||||||
@@ -3564,7 +3674,7 @@ try {
|
|||||||
|
|
||||||
const result = execFileSync(
|
const result = execFileSync(
|
||||||
electronPath,
|
electronPath,
|
||||||
[`--user-data-dir=${userDataPath}`, '--require', injectPath, mainPath],
|
[`--user-data-dir=${userDataPath}`, '--require', injectPath, mainPath, '--dev'],
|
||||||
{ cwd: path.join(__dirname, '..'), timeout: 180000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
{ cwd: path.join(__dirname, '..'), timeout: 180000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
|
||||||
);
|
);
|
||||||
console.log(result);
|
console.log(result);
|
||||||
|
|||||||
Reference in New Issue
Block a user