Allow going back after failed patch
This commit is contained in:
@@ -870,4 +870,83 @@ test.describe('Custom patches', () => {
|
|||||||
const actualHash = crypto.createHash('sha1').update(tgzData).digest('hex');
|
const actualHash = crypto.createHash('sha1').update(tgzData).digest('hex');
|
||||||
expect(actualHash, 'restored KoboRoot.tgz SHA1 mismatch').toBe(ORIGINAL_TGZ_SHA1);
|
expect(actualHash, 'restored KoboRoot.tgz SHA1 mismatch').toBe(ORIGINAL_TGZ_SHA1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('with device — build failure shows Go Back and returns to patches', async ({ page }) => {
|
||||||
|
test.skip(!fs.existsSync(FIRMWARE_PATH), `Firmware not found at ${FIRMWARE_PATH}`);
|
||||||
|
|
||||||
|
setupFirmwareSymlink();
|
||||||
|
await connectMockDevice(page, { hasNickelMenu: false, overrideFirmware: true });
|
||||||
|
|
||||||
|
// Select Custom Patches
|
||||||
|
await page.click('#btn-device-next');
|
||||||
|
await page.click('input[name="mode"][value="patches"]');
|
||||||
|
await page.click('#btn-mode-next');
|
||||||
|
|
||||||
|
// Enable "Remove footer (row3) on new home screen"
|
||||||
|
const patchName = page.locator('.patch-name', { hasText: 'Remove footer (row3) on new home screen' }).first();
|
||||||
|
const patchSection = patchName.locator('xpath=ancestor::details');
|
||||||
|
await patchSection.locator('summary').click();
|
||||||
|
await patchName.locator('xpath=ancestor::label').locator('input').check();
|
||||||
|
await page.click('#btn-patches-next');
|
||||||
|
|
||||||
|
// Mock the WASM patcher to simulate a failure
|
||||||
|
await page.evaluate(() => {
|
||||||
|
KoboPatchRunner.prototype.patchFirmware = async function () {
|
||||||
|
throw new Error('Patch failed to apply: symbol not found');
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build — should fail due to mock
|
||||||
|
await page.click('#btn-build');
|
||||||
|
|
||||||
|
await expect(page.locator('#step-error')).not.toBeHidden({ timeout: 30_000 });
|
||||||
|
await expect(page.locator('#error-message')).toContainText('Build failed');
|
||||||
|
await expect(page.locator('#btn-error-back')).toBeVisible();
|
||||||
|
|
||||||
|
// Go Back should return to patches step
|
||||||
|
await page.click('#btn-error-back');
|
||||||
|
await expect(page.locator('#step-patches')).not.toBeHidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with device — real patch failure with Go Back (Allow rotation)', async ({ page }) => {
|
||||||
|
test.skip(!fs.existsSync(FIRMWARE_PATH), `Firmware not found at ${FIRMWARE_PATH}`);
|
||||||
|
|
||||||
|
setupFirmwareSymlink();
|
||||||
|
await connectMockDevice(page, { hasNickelMenu: false, overrideFirmware: true });
|
||||||
|
|
||||||
|
// Select Custom Patches
|
||||||
|
await page.click('#btn-device-next');
|
||||||
|
await page.click('input[name="mode"][value="patches"]');
|
||||||
|
await page.click('#btn-mode-next');
|
||||||
|
|
||||||
|
// Enable "Allow rotation on all devices" — marked as not working on 4.45.23646
|
||||||
|
const patchName = page.locator('.patch-name', { hasText: 'Allow rotation on all devices' }).first();
|
||||||
|
const patchSection = patchName.locator('xpath=ancestor::details');
|
||||||
|
await patchSection.locator('summary').click();
|
||||||
|
await expect(patchName).toBeVisible();
|
||||||
|
await patchName.locator('xpath=ancestor::label').locator('input').check();
|
||||||
|
await page.click('#btn-patches-next');
|
||||||
|
|
||||||
|
// Build
|
||||||
|
await page.click('#btn-build');
|
||||||
|
|
||||||
|
const doneOrError = await Promise.race([
|
||||||
|
page.locator('#step-done').waitFor({ state: 'visible', timeout: 240_000 }).then(() => 'done'),
|
||||||
|
page.locator('#step-error').waitFor({ state: 'visible', timeout: 240_000 }).then(() => 'error'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (doneOrError === 'error') {
|
||||||
|
// Build failed — verify Go Back works
|
||||||
|
await expect(page.locator('#error-message')).toContainText('Build failed');
|
||||||
|
await expect(page.locator('#btn-error-back')).toBeVisible();
|
||||||
|
await page.click('#btn-error-back');
|
||||||
|
await expect(page.locator('#step-patches')).not.toBeHidden();
|
||||||
|
} else {
|
||||||
|
// Build succeeded — check if the patch was skipped
|
||||||
|
const logText = await page.locator('#build-log').textContent();
|
||||||
|
console.log('Build log:', logText);
|
||||||
|
const hasSkip = logText.includes('SKIP') && logText.includes('Allow rotation on all devices');
|
||||||
|
expect(hasSkip, 'Expected "Allow rotation" to be skipped or fail').toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ module.exports = defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'cd ../../web && node build.mjs && cd ../kobopatch-wasm && bash build.sh && cd ../web && python3 -m http.server -d dist 8889',
|
command: 'cd ../../web && npm install && node build.mjs && cd ../kobopatch-wasm && bash build.sh && cd ../web && python3 -m http.server -d dist 8889',
|
||||||
port: 8889,
|
port: 8889,
|
||||||
reuseExistingServer: true,
|
reuseExistingServer: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -472,6 +472,17 @@ button.secondary:hover {
|
|||||||
border-color: #9ca3af;
|
border-color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.danger {
|
||||||
|
background: #fff;
|
||||||
|
color: var(--error-text);
|
||||||
|
border-color: var(--error-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.danger:hover {
|
||||||
|
background: var(--error-bg);
|
||||||
|
border-color: var(--error-text);
|
||||||
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
@@ -564,6 +575,10 @@ button.btn-success:hover {
|
|||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#error-message {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.status-supported {
|
.status-supported {
|
||||||
background: var(--success-bg);
|
background: var(--success-bg);
|
||||||
border: 1px solid var(--success-border);
|
border: 1px solid var(--success-border);
|
||||||
|
|||||||
@@ -364,10 +364,14 @@
|
|||||||
|
|
||||||
<!-- Error state -->
|
<!-- Error state -->
|
||||||
<section id="step-error" class="step" hidden>
|
<section id="step-error" class="step" hidden>
|
||||||
<h2>Something went wrong</h2>
|
<h2 id="error-title">Something went wrong</h2>
|
||||||
<p id="error-message" class="error"></p>
|
<p id="error-hint" hidden>Some patches may not work correctly with your software version. You can go back and try a different selection.</p>
|
||||||
<pre id="error-log" class="error-log" hidden></pre>
|
<pre id="error-log" class="error-log" hidden></pre>
|
||||||
|
<p id="error-message" class="error"></p>
|
||||||
|
<div class="step-actions">
|
||||||
|
<button id="btn-error-back" class="secondary" hidden>‹ Select different patches</button>
|
||||||
<button id="btn-retry" class="secondary">Start Over</button>
|
<button id="btn-retry" class="secondary">Start Over</button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -101,9 +101,12 @@ import JSZip from 'jszip';
|
|||||||
const btnWrite = $('btn-write');
|
const btnWrite = $('btn-write');
|
||||||
const btnDownload = $('btn-download');
|
const btnDownload = $('btn-download');
|
||||||
const btnRetry = $('btn-retry');
|
const btnRetry = $('btn-retry');
|
||||||
|
const btnErrorBack = $('btn-error-back');
|
||||||
|
|
||||||
const errorMessage = $('error-message');
|
const errorMessage = $('error-message');
|
||||||
const errorLog = $('error-log');
|
const errorLog = $('error-log');
|
||||||
|
const errorTitle = $('error-title');
|
||||||
|
const errorHint = $('error-hint');
|
||||||
const deviceStatus = $('device-status');
|
const deviceStatus = $('device-status');
|
||||||
const deviceUnknownWarning = $('device-unknown-warning');
|
const deviceUnknownWarning = $('device-unknown-warning');
|
||||||
const deviceUnknownAck = $('device-unknown-ack');
|
const deviceUnknownAck = $('device-unknown-ack');
|
||||||
@@ -165,6 +168,10 @@ import JSZip from 'jszip';
|
|||||||
stepNav.hidden = true;
|
stepNav.hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showNav() {
|
||||||
|
stepNav.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Mode selection card interactivity ---
|
// --- Mode selection card interactivity ---
|
||||||
function setupCardRadios(container, selectedClass) {
|
function setupCardRadios(container, selectedClass) {
|
||||||
const labels = $qa('label', container);
|
const labels = $qa('label', container);
|
||||||
@@ -435,6 +442,7 @@ import JSZip from 'jszip';
|
|||||||
populateSelect(manualModel, '-- Select your Kobo model --', []);
|
populateSelect(manualModel, '-- Select your Kobo model --', []);
|
||||||
manualModel.hidden = true;
|
manualModel.hidden = true;
|
||||||
btnManualConfirm.disabled = true;
|
btnManualConfirm.disabled = true;
|
||||||
|
setNavStep(2);
|
||||||
showStep(stepManualVersion);
|
showStep(stepManualVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,6 +648,7 @@ import JSZip from 'jszip';
|
|||||||
btnPatchesBack.addEventListener('click', () => {
|
btnPatchesBack.addEventListener('click', () => {
|
||||||
if (manualMode) {
|
if (manualMode) {
|
||||||
// Go back to version selection in manual mode
|
// Go back to version selection in manual mode
|
||||||
|
setNavStep(2);
|
||||||
showStep(stepManualVersion);
|
showStep(stepManualVersion);
|
||||||
} else {
|
} else {
|
||||||
goToModeSelection();
|
goToModeSelection();
|
||||||
@@ -836,7 +845,7 @@ import JSZip from 'jszip';
|
|||||||
showBuildResult();
|
showBuildResult();
|
||||||
await checkExistingTgz();
|
await checkExistingTgz();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError('Build failed: ' + err.message, buildLog.textContent);
|
showError('Build failed: ' + err.message, buildLog.textContent, stepPatches);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -874,18 +883,46 @@ import JSZip from 'jszip';
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Error / Retry ---
|
// --- Error / Retry ---
|
||||||
function showError(message, log) {
|
function showError(message, log, backStep) {
|
||||||
errorMessage.textContent = message;
|
errorMessage.textContent = message;
|
||||||
if (log) {
|
if (log) {
|
||||||
errorLog.textContent = log;
|
errorLog.textContent = log;
|
||||||
errorLog.hidden = false;
|
errorLog.hidden = false;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
errorLog.scrollTop = errorLog.scrollHeight;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
errorLog.hidden = true;
|
errorLog.hidden = true;
|
||||||
}
|
}
|
||||||
|
if (backStep) {
|
||||||
|
errorTitle.textContent = 'The patch failed to apply';
|
||||||
|
errorHint.hidden = false;
|
||||||
|
btnErrorBack.hidden = false;
|
||||||
|
btnErrorBack._backStep = backStep;
|
||||||
|
btnRetry.classList.add('danger');
|
||||||
|
} else {
|
||||||
|
errorTitle.textContent = 'Something went wrong';
|
||||||
|
errorHint.hidden = true;
|
||||||
|
btnErrorBack.hidden = true;
|
||||||
|
btnErrorBack._backStep = null;
|
||||||
|
btnRetry.classList.remove('danger');
|
||||||
|
}
|
||||||
hideNav();
|
hideNav();
|
||||||
showStep(stepError);
|
showStep(stepError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
btnErrorBack.addEventListener('click', () => {
|
||||||
|
btnErrorBack.hidden = true;
|
||||||
|
btnRetry.classList.remove('danger');
|
||||||
|
showNav();
|
||||||
|
if (manualMode) {
|
||||||
|
setNavStep(2);
|
||||||
|
showStep(stepManualVersion);
|
||||||
|
} else {
|
||||||
|
goToModeSelection();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
btnRetry.addEventListener('click', () => {
|
btnRetry.addEventListener('click', () => {
|
||||||
device.disconnect();
|
device.disconnect();
|
||||||
firmwareURL = null;
|
firmwareURL = null;
|
||||||
|
|||||||
@@ -52,4 +52,7 @@ class KoboPatchRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expose on window for E2E test compatibility
|
||||||
|
window.KoboPatchRunner = KoboPatchRunner;
|
||||||
|
|
||||||
export { KoboPatchRunner };
|
export { KoboPatchRunner };
|
||||||
|
|||||||
Reference in New Issue
Block a user