Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
384 changes: 380 additions & 4 deletions scripts/build-site.mjs

Large diffs are not rendered by default.

118 changes: 117 additions & 1 deletion scripts/test-site-browser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,120 @@ try {
`mobile layout overflows by ${mobile.scrollWidth - mobile.clientWidth}px`
);

await page.goto(`${baseUrl}/zh-CN/`, { waitUntil: 'networkidle0' });
await page.waitForSelector('#runtime-state[data-state="ready"]');
assert.equal(await page.$eval('html', (element) => element.lang), 'zh-CN');
assert.match(
await page.$eval('h1', (element) => element.textContent || ''),
/二进制差量/
);
assert.equal(
await page.$eval('a[hreflang="en"]', (element) =>
element.textContent.trim()
),
'English'
);

await page.setViewport({ width: 1280, height: 900, deviceScaleFactor: 1 });
await page.goto(`${baseUrl}/tools/`, { waitUntil: 'networkidle0' });
await page.waitForSelector('#tool-runtime-state[data-state="ready"]');

const fixtures = await page.evaluate(async () => {
const encoder = new TextEncoder();
const oldBytes = encoder.encode('release=1\nfeatures=native\n');
const newBytes = encoder.encode('release=2\nfeatures=native,web\n');
const { diffBytes } = await import('/web/index.mjs');
const patch = await diffBytes(oldBytes, newBytes);
return {
oldBytes: [...oldBytes],
newBytes: [...newBytes],
patch: [...patch],
};
});

const selectFile = async (selector, bytes, filename) => {
await page.evaluate(
({ inputSelector, fileBytes, fileName }) => {
const input = document.querySelector(inputSelector);
const transfer = new DataTransfer();
transfer.items.add(
new File([new Uint8Array(fileBytes)], fileName, {
type: 'application/octet-stream',
})
);
input.files = transfer.files;
input.dispatchEvent(new Event('change', { bubbles: true }));
},
{ inputSelector: selector, fileBytes: bytes, fileName: filename }
);
};

await selectFile('#create-old-file', fixtures.oldBytes, 'release-v1.bin');
await selectFile('#create-new-file', fixtures.newBytes, 'release-v2.bin');
await page.click('#create-run');
await page.waitForSelector('#create-status[data-state="success"]', {
timeout: 30_000,
});
assert.match(
await page.$eval('#create-report', (element) => element.textContent || ''),
/Byte-for-byte match: PASS/
);
assert.notEqual(
await page.$eval('#create-patch-size', (element) => element.textContent),
'—'
);

await page.click('#apply-tab');
await selectFile('#apply-old-file', fixtures.oldBytes, 'release-v1.bin');
await selectFile('#apply-patch-file', fixtures.patch, 'release-v2.patch');
await selectFile('#apply-expected-file', fixtures.newBytes, 'release-v2.bin');
await page.select('#apply-origin', 'Android');
await page.click('#apply-run');
await page.waitForSelector('#apply-status[data-state="success"]', {
timeout: 30_000,
});
assert.match(
await page.$eval('#apply-report', (element) => element.textContent || ''),
/Generated on: Android[\s\S]*Byte-for-byte match: PASS/
);

await page.click('#inspect-tab');
await selectFile('#inspect-patch-file', fixtures.patch, 'release-v2.patch');
await page.click('#inspect-run');
await page.waitForSelector('#inspect-status[data-state="success"]');
assert.equal(
await page.$eval('#inspect-format', (element) => element.textContent),
'ENDSLEY/BSDIFF43'
);

await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 1 });
await page.reload({ waitUntil: 'networkidle0' });
const toolsMobile = await page.evaluate(() => ({
clientWidth: document.documentElement.clientWidth,
scrollWidth: document.documentElement.scrollWidth,
}));
assert.ok(
toolsMobile.scrollWidth <= toolsMobile.clientWidth + 1,
`tools mobile layout overflows by ${
toolsMobile.scrollWidth - toolsMobile.clientWidth
}px`
);

await page.goto(`${baseUrl}/zh-CN/tools/`, {
waitUntil: 'networkidle0',
});
assert.equal(await page.$eval('html', (element) => element.lang), 'zh-CN');
assert.match(
await page.$eval('h1', (element) => element.textContent || ''),
/二进制补丁工具/
);
assert.equal(
await page.$eval('a[hreflang="en"]', (element) =>
element.textContent.trim()
),
'English'
);

await page.goto(`${baseUrl}/docs/api-reference/`, {
waitUntil: 'networkidle0',
});
Expand All @@ -155,7 +269,9 @@ try {
'English'
);
assert.equal(pageErrors.length, 0, pageErrors.join('\n'));
console.log('Site Playground, bilingual docs, and mobile viewport passed');
console.log(
'Site Playground, Binary Patch Toolkit, localization, and mobile viewport passed'
);
} finally {
await browser.close();
await new Promise((resolve, reject) =>
Expand Down
65 changes: 65 additions & 0 deletions scripts/test-site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ const requiredFiles = [
'assets/site.css',
'assets/site.js',
'assets/playground.js',
'assets/tools.js',
'assets/social-preview.png',
'web/index.mjs',
'web/worker.mjs',
'web/operations.mjs',
'web/bsdiffpatch.mjs',
'zh-CN/index.html',
'tools/index.html',
'zh-CN/tools/index.html',
'docs/index.html',
'docs/getting-started/index.html',
'docs/api-reference/index.html',
Expand Down Expand Up @@ -56,6 +60,18 @@ assert.equal(
(await readFile(path.join(outputDirectory, 'CNAME'), 'utf8')).trim(),
'bs-dff-patch.corerobin.com'
);
assert.match(
await readFile(path.join(outputDirectory, 'sitemap.xml'), 'utf8'),
/https:\/\/bs-dff-patch\.corerobin\.com\/zh-CN\//
);
assert.match(
await readFile(path.join(outputDirectory, 'sitemap.xml'), 'utf8'),
/https:\/\/bs-dff-patch\.corerobin\.com\/tools\//
);
assert.match(
await readFile(path.join(outputDirectory, 'sitemap.xml'), 'utf8'),
/https:\/\/bs-dff-patch\.corerobin\.com\/zh-CN\/tools\//
);

async function htmlFiles(directory) {
const entries = await readdir(directory, { withFileTypes: true });
Expand Down Expand Up @@ -170,6 +186,10 @@ assert.match(
);
assert.match(homepage, /<link rel="icon" href="\/favicon\.svg"/);
assert.match(homepage, /<link rel="manifest" href="\/site\.webmanifest"/);
assert.match(
homepage,
/<link\s+rel="alternate"\s+hreflang="zh-CN"\s+href="https:\/\/bs-dff-patch\.corerobin\.com\/zh-CN\/"/
);
assert.match(homepage, /id="playground"/);
assert.match(homepage, /id="generate-patch"/);
assert.match(homepage, /id="cancel-operation"/);
Expand All @@ -185,6 +205,51 @@ assert.match(homepage, /459 KiB unpacked · 58 files/);
assert.match(homepage, /30,697\.5 ms/);
assert.match(homepage, /assets\/playground\.js/);

const chineseHomepage = await readFile(
path.join(outputDirectory, 'zh-CN/index.html'),
'utf8'
);
assert.match(chineseHomepage, /<html lang="zh-CN">/);
assert.match(
chineseHomepage,
/<link rel="canonical" href="https:\/\/bs-dff-patch\.corerobin\.com\/zh-CN\/"/
);
assert.match(chineseHomepage, /二进制差量。/);
assert.match(chineseHomepage, /href="\.\.\/docs\/zh-CN\/">文档</);
assert.match(chineseHomepage, /href="\.\.\/"\s+hreflang="en"/);
assert.match(chineseHomepage, /href="\.\.\/assets\/site\.css"/);
assert.doesNotMatch(chineseHomepage, /Binary deltas\./);

const toolsPage = await readFile(
path.join(outputDirectory, 'tools/index.html'),
'utf8'
);
assert.match(toolsPage, /<html lang="en">/);
assert.match(
toolsPage,
/<link\s+rel="canonical"\s+href="https:\/\/bs-dff-patch\.corerobin\.com\/tools\/"/
);
assert.match(toolsPage, /id="create-panel"/);
assert.match(toolsPage, /id="apply-panel"/);
assert.match(toolsPage, /id="inspect-panel"/);
assert.match(toolsPage, /id="apply-expected-sha256"/);
assert.match(toolsPage, /id="tool-code"/);
assert.match(toolsPage, /assets\/tools\.js/);
assert.match(toolsPage, /Your files stay in this browser/);

const chineseToolsPage = await readFile(
path.join(outputDirectory, 'zh-CN/tools/index.html'),
'utf8'
);
assert.match(chineseToolsPage, /<html lang="zh-CN">/);
assert.match(
chineseToolsPage,
/<link\s+rel="canonical"\s+href="https:\/\/bs-dff-patch\.corerobin\.com\/zh-CN\/tools\/"/
);
assert.match(chineseToolsPage, /二进制补丁工具/);
assert.match(chineseToolsPage, /href="\/tools\/"\s+hreflang="en"/);
assert.doesNotMatch(chineseToolsPage, /\{\{[A-Z0-9_]+\}\}/);

function pngDimensions(buffer) {
assert.equal(buffer.toString('ascii', 1, 4), 'PNG');
return {
Expand Down
52 changes: 42 additions & 10 deletions site/assets/playground.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,34 @@ const maxOutputBytes = document.querySelector('#max-output-bytes');
const operationPhase = document.querySelector('#operation-phase');
const operationPercent = document.querySelector('#operation-percent');
const operationProgress = document.querySelector('#operation-progress');
const localized = document.documentElement.lang === 'zh-CN';
const ui = localized
? {
cancelled: '已取消',
complete: '完成',
diffing: '生成差量',
generating: '正在通过 Web Worker 生成补丁…',
limitRejected: '超过已配置的资源限制',
operationFailed: '补丁操作失败',
runtimeReady: 'WASM 已就绪',
cancellationRequested: '已请求取消…',
verifying: '验证结果',
verified: '往返结果已逐字节验证',
rejected: '已拒绝',
}
: {
cancelled: 'Cancelled',
complete: 'Complete',
diffing: 'Diffing',
generating: 'Generating patch in the Web Worker…',
limitRejected: 'The configured resource limit was exceeded',
operationFailed: 'The patch operation failed',
runtimeReady: 'WASM ready',
cancellationRequested: 'Cancellation requested…',
verifying: 'Verifying',
verified: 'Round trip verified byte-for-byte',
rejected: 'Rejected',
};

let currentPatch;
let activeController;
Expand Down Expand Up @@ -75,14 +103,14 @@ async function generatePatch() {
maxInputBytes: selectedLimit(maxInputBytes),
maxOutputBytes: selectedLimit(maxOutputBytes),
};
setProgress('Diffing', 0.2);
setStatus('running', 'Generating patch in the Web Worker…');
setProgress(ui.diffing, 0.2);
setStatus('running', ui.generating);

const startedAt = performance.now();

try {
const patchData = await diffBytes(oldData, newData, options);
setProgress('Verifying', 0.65);
setProgress(ui.verifying, 0.65);
const restoredData = await patchBytes(oldData, patchData, options);

if (!bytesEqual(restoredData, newData)) {
Expand All @@ -104,8 +132,8 @@ async function generatePatch() {
runtimeMs.textContent = `${elapsed.toFixed(2)} ms`;
errorCode.textContent = '—';
downloadButton.disabled = false;
setProgress('Complete', 1);
setStatus('success', 'Round trip verified byte-for-byte');
setProgress(ui.complete, 1);
setStatus('success', ui.verified);
} catch (error) {
currentPatch = undefined;
patchSize.textContent = '—';
Expand All @@ -116,12 +144,16 @@ async function generatePatch() {
? String(error.code)
: 'EUNKNOWN';
errorCode.textContent = code;
setProgress(code === 'EABORTED' ? 'Cancelled' : 'Rejected', 0);
setProgress(code === 'EABORTED' ? ui.cancelled : ui.rejected, 0);
setStatus(
'error',
error instanceof Error
localized && code === 'EABORTED'
? `[${code}] ${ui.cancelled}`
: localized && code === 'ERESOURCE'
? `[${code}] ${ui.limitRejected}`
: error instanceof Error
? `[${code}] ${error.message}`
: `[${code}] The patch operation failed`
: `[${code}] ${ui.operationFailed}`
);
} finally {
activeController = undefined;
Expand All @@ -135,7 +167,7 @@ function cancelOperation() {
if (!activeController) return;
activeController.abort();
cancelButton.disabled = true;
setStatus('running', 'Cancellation requested…');
setStatus('running', ui.cancellationRequested);
}

function downloadPatch() {
Expand All @@ -161,4 +193,4 @@ downloadButton.addEventListener('click', downloadPatch);

updateInputSizes();
runtimeState.dataset.state = 'ready';
runtimeState.textContent = 'WASM ready';
runtimeState.textContent = ui.runtimeReady;
Loading