1
0

Allow going back after failed patch

This commit is contained in:
2026-03-19 19:12:58 +01:00
parent 7aef8d8ed3
commit 57f3811932
6 changed files with 144 additions and 6 deletions

View File

@@ -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);
}
});
}); });

View File

@@ -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,
}, },

View File

@@ -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);

View File

@@ -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>
<button id="btn-retry" class="secondary">Start Over</button> <p id="error-message" class="error"></p>
<div class="step-actions">
<button id="btn-error-back" class="secondary" hidden>&#x2039; Select different patches</button>
<button id="btn-retry" class="secondary">Start Over</button>
</div>
</section> </section>
</main> </main>

View File

@@ -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;

View File

@@ -52,4 +52,7 @@ class KoboPatchRunner {
} }
} }
// Expose on window for E2E test compatibility
window.KoboPatchRunner = KoboPatchRunner;
export { KoboPatchRunner }; export { KoboPatchRunner };