All checks were successful
Build & Test WASM / build-and-test (push) Successful in 1m41s
149 lines
5.7 KiB
JavaScript
149 lines
5.7 KiB
JavaScript
// @ts-check
|
|
const { test, expect } = require('@playwright/test');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
const zlib = require('zlib');
|
|
|
|
// Expected SHA1 checksums for Kobo Libra Color, firmware 4.45.23646,
|
|
// with only "Remove footer (row3) on new home screen" enabled.
|
|
const EXPECTED_SHA1 = {
|
|
'usr/local/Kobo/libnickel.so.1.0.0': 'ef64782895a47ac85f0829f06fffa4816d23512d',
|
|
'usr/local/Kobo/nickel': '80a607bac515457a6864be8be831df631a01005c',
|
|
'usr/local/Kobo/libadobe.so': '02dc99c71c4fef75401cd49ddc2e63f928a126e1',
|
|
'usr/local/Kobo/librmsdk.so.1.0.0': 'e3819260c9fc539a53db47e9d3fe600ec11633d5',
|
|
};
|
|
|
|
const FIRMWARE_PATH = process.env.FIRMWARE_ZIP
|
|
|| path.resolve(__dirname, '..', 'kobopatch-wasm', 'testdata', 'kobo-update-4.45.23646.zip');
|
|
|
|
const WEBROOT_FIRMWARE = path.resolve(__dirname, '..', 'web', 'public', '_test_firmware.zip');
|
|
|
|
/**
|
|
* Parse a tar archive (uncompressed) and return a map of entry name -> Buffer.
|
|
*/
|
|
function parseTar(buffer) {
|
|
const entries = {};
|
|
let offset = 0;
|
|
|
|
while (offset < buffer.length) {
|
|
const header = buffer.subarray(offset, offset + 512);
|
|
if (header.every(b => b === 0)) break;
|
|
|
|
let name = header.subarray(0, 100).toString('utf8').replace(/\0+$/, '');
|
|
const prefix = header.subarray(345, 500).toString('utf8').replace(/\0+$/, '');
|
|
if (prefix) name = prefix + '/' + name;
|
|
name = name.replace(/^\.\//, '');
|
|
|
|
const sizeStr = header.subarray(124, 136).toString('utf8').replace(/\0+$/, '').trim();
|
|
const size = parseInt(sizeStr, 8) || 0;
|
|
const typeFlag = header[156];
|
|
|
|
offset += 512;
|
|
|
|
if (typeFlag === 48 || typeFlag === 0) {
|
|
entries[name] = buffer.subarray(offset, offset + size);
|
|
}
|
|
|
|
offset += Math.ceil(size / 512) * 512;
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
// Clean up the symlink after the test.
|
|
test.afterEach(() => {
|
|
try { fs.unlinkSync(WEBROOT_FIRMWARE); } catch {}
|
|
});
|
|
|
|
test('full manual mode patching pipeline', async ({ page }) => {
|
|
if (!fs.existsSync(FIRMWARE_PATH)) {
|
|
test.skip(true, `Firmware not found at ${FIRMWARE_PATH}`);
|
|
}
|
|
|
|
// Symlink the cached firmware into the webroot so the app can fetch it locally.
|
|
try { fs.unlinkSync(WEBROOT_FIRMWARE); } catch {}
|
|
fs.symlinkSync(path.resolve(FIRMWARE_PATH), WEBROOT_FIRMWARE);
|
|
|
|
await page.goto('/');
|
|
await expect(page.locator('h1')).toContainText('KoboPatch');
|
|
|
|
// Override the firmware download URLs to point at the local server.
|
|
await page.evaluate(() => {
|
|
for (const version of Object.keys(FIRMWARE_DOWNLOADS)) {
|
|
for (const prefix of Object.keys(FIRMWARE_DOWNLOADS[version])) {
|
|
FIRMWARE_DOWNLOADS[version][prefix] = '/_test_firmware.zip';
|
|
}
|
|
}
|
|
});
|
|
|
|
// Step 1: Switch to manual mode.
|
|
await page.click('#btn-manual-from-auto');
|
|
await expect(page.locator('#step-manual')).not.toBeHidden();
|
|
|
|
// Step 2: Select firmware version.
|
|
await page.selectOption('#manual-version', '4.45.23646');
|
|
await expect(page.locator('#manual-model')).not.toBeHidden();
|
|
|
|
// Step 3: Select Kobo Libra Colour (N428).
|
|
await page.selectOption('#manual-model', 'N428');
|
|
await expect(page.locator('#btn-manual-confirm')).toBeEnabled();
|
|
await page.click('#btn-manual-confirm');
|
|
|
|
// Step 4: Wait for patches to load.
|
|
await expect(page.locator('#step-patches')).not.toBeHidden();
|
|
await expect(page.locator('#patch-container .patch-file-section')).not.toHaveCount(0);
|
|
|
|
// Step 5: 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 expect(patchName).toBeVisible();
|
|
await patchName.locator('xpath=ancestor::label').locator('input').check();
|
|
|
|
// Verify patch count updated.
|
|
await expect(page.locator('#patch-count-hint')).toContainText('1 patch selected');
|
|
await expect(page.locator('#btn-patches-next')).toBeEnabled();
|
|
|
|
// Step 6: Continue to build step.
|
|
await page.click('#btn-patches-next');
|
|
await expect(page.locator('#step-firmware')).not.toBeHidden();
|
|
await expect(page.locator('#firmware-version-label')).toHaveText('4.45.23646');
|
|
await expect(page.locator('#firmware-device-label')).toHaveText('Kobo Libra Colour');
|
|
|
|
// Step 7: Build and wait for completion.
|
|
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') {
|
|
const errorMsg = await page.locator('#error-message').textContent();
|
|
throw new Error(`Build failed: ${errorMsg}`);
|
|
}
|
|
|
|
await expect(page.locator('#build-status')).toContainText('Patching complete');
|
|
|
|
// Step 8: Download KoboRoot.tgz and verify checksums.
|
|
const [download] = await Promise.all([
|
|
page.waitForEvent('download'),
|
|
page.click('#btn-download'),
|
|
]);
|
|
|
|
expect(download.suggestedFilename()).toBe('KoboRoot.tgz');
|
|
const downloadPath = await download.path();
|
|
const tgzData = fs.readFileSync(downloadPath);
|
|
|
|
const tarData = zlib.gunzipSync(tgzData);
|
|
const entries = parseTar(tarData);
|
|
|
|
for (const [name, expectedHash] of Object.entries(EXPECTED_SHA1)) {
|
|
const data = entries[name];
|
|
expect(data, `missing binary in output: ${name}`).toBeDefined();
|
|
const actualHash = crypto.createHash('sha1').update(data).digest('hex');
|
|
expect(actualHash, `SHA1 mismatch for ${name}`).toBe(expectedHash);
|
|
}
|
|
});
|