mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-02 15:27:58 +08:00
feat: ship verified platform upgrades with channel bundle releases
Package production dependencies per architecture for online and offline upgrades. Validate dependencies and isolated backend startup before readiness, repair once when permitted, and preserve service/data through failed preparation or rollback. Publish release bundles with master images and expose an opt-in develop build sharing the development index across Alpine and Debian images.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const ts = require('typescript');
|
||||
|
||||
function layout(width, legacy = false) {
|
||||
const values = [],
|
||||
listeners = new Set(),
|
||||
attributes = new Map(),
|
||||
effects = [];
|
||||
let cursor = 0,
|
||||
mounted = false;
|
||||
const media = { matches: width < 768 };
|
||||
const add = (fn) => listeners.add(fn),
|
||||
remove = (fn) => listeners.delete(fn);
|
||||
Object.assign(
|
||||
media,
|
||||
legacy
|
||||
? { addListener: add, removeListener: remove }
|
||||
: {
|
||||
addEventListener: (_, fn) => add(fn),
|
||||
removeEventListener: (_, fn) => remove(fn),
|
||||
},
|
||||
);
|
||||
const module = { exports: {} };
|
||||
const source = ts.transpileModule(
|
||||
fs.readFileSync(path.join(__dirname, '../../src/utils/hooks.ts'), 'utf8'),
|
||||
{
|
||||
compilerOptions: {
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
},
|
||||
},
|
||||
).outputText;
|
||||
new Function('require', 'module', 'exports', 'window', 'document', source)(
|
||||
(name) => {
|
||||
assert.equal(name, 'react');
|
||||
return {
|
||||
useState: (initial) => {
|
||||
const index = cursor++;
|
||||
if (!(index in values))
|
||||
values[index] = typeof initial === 'function' ? initial() : initial;
|
||||
return [
|
||||
values[index],
|
||||
(value) => {
|
||||
values[index] = value;
|
||||
},
|
||||
];
|
||||
},
|
||||
useEffect: (fn) => {
|
||||
if (!mounted) effects.push(fn);
|
||||
},
|
||||
};
|
||||
},
|
||||
module,
|
||||
module.exports,
|
||||
{ matchMedia: () => media, navigator: { userAgent: 'Desktop browser' } },
|
||||
{ body: { setAttribute: (key, value) => attributes.set(key, value) } },
|
||||
);
|
||||
const render = () => {
|
||||
cursor = 0;
|
||||
return module.exports.useCtx();
|
||||
};
|
||||
const initial = render();
|
||||
const cleanup = effects.map((fn) => fn());
|
||||
mounted = true;
|
||||
return {
|
||||
initial,
|
||||
attributes,
|
||||
listeners,
|
||||
resize: (width) => {
|
||||
media.matches = width < 768;
|
||||
listeners.forEach((fn) => fn());
|
||||
return render();
|
||||
},
|
||||
unmount: () => cleanup.forEach((fn) => fn?.()),
|
||||
};
|
||||
}
|
||||
|
||||
test('narrow desktop windows retain the top navigation and user controls, and follow viewport changes', () => {
|
||||
const view = layout(499);
|
||||
assert.equal(view.initial.isPhone, true);
|
||||
assert.equal(view.initial.headerStyle.marginTop, 0);
|
||||
assert.equal(view.attributes.get('data-mode'), 'phone');
|
||||
assert.equal(view.listeners.size, 1);
|
||||
const desktop = view.resize(1024);
|
||||
assert.equal(desktop.isPhone, false);
|
||||
assert.equal(desktop.headerStyle.marginTop, -48);
|
||||
assert.equal(view.attributes.get('data-mode'), 'desktop');
|
||||
const narrow = view.resize(767.5);
|
||||
assert.equal(narrow.isPhone, true);
|
||||
assert.equal(narrow.headerStyle.marginTop, 0);
|
||||
assert.equal(view.attributes.get('data-mode'), 'phone');
|
||||
assert.equal(view.resize(768).isPhone, false);
|
||||
view.unmount();
|
||||
assert.equal(view.listeners.size, 0);
|
||||
});
|
||||
|
||||
test('older media query listeners are removed when the layout unmounts', () => {
|
||||
const view = layout(1024, true);
|
||||
assert.equal(view.resize(600).isPhone, true);
|
||||
assert.equal(view.attributes.get('data-mode'), 'phone');
|
||||
view.unmount();
|
||||
assert.equal(view.listeners.size, 0);
|
||||
});
|
||||
@@ -335,11 +335,15 @@ test('the persistent yellow sidebar entry shows live progress and remains access
|
||||
assert.match(p.nodes('button')[0].props['aria-label'], /62%/);
|
||||
p.nodes('button')[0].props.onClick();
|
||||
p.render({ collapsed: true });
|
||||
assert.match(p.nodes('button')[0].props.className, /collapsed/);
|
||||
assert.equal(p.nodes('button').length, 0);
|
||||
assert.equal(p.nodes(p.Modal)[0].props.open, true);
|
||||
f.store.setJob({ status: 'ready', phase: 'ready' });
|
||||
f.store.setJob({ status: 'ready', phase: 'completed' });
|
||||
p.render();
|
||||
assert.equal(p.nodes('button')[0].props['aria-label'], '更新已就绪');
|
||||
assert.equal(p.nodes('button').length, 0);
|
||||
p.nodes(p.Modal)[0].props.onCancel();
|
||||
p.render({ collapsed: true });
|
||||
assert.match(p.nodes('button')[0].props.className, /collapsed/);
|
||||
assert.equal(p.nodes('button')[0].props['aria-label'], '下载完成,等待重启');
|
||||
});
|
||||
|
||||
test('completed update refreshes once and reminds once after reconnecting the refreshed page', async () => {
|
||||
@@ -433,3 +437,32 @@ test('update modal uses plain aligned content and explains restart state', () =>
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('sidebar and dialog use the same authoritative status even when old phase fields disagree', () => {
|
||||
const f = fixture(),
|
||||
notice = f.component('src/components/UpgradeNotice.tsx'),
|
||||
panel = f.component('src/pages/setting/checkUpdate.tsx');
|
||||
f.store.setJob({ status: 'ready', phase: 'completed' });
|
||||
notice.render();
|
||||
panel.render();
|
||||
assert.equal(
|
||||
notice.nodes('button')[0].props['aria-label'],
|
||||
f.hooks.upgradeStatusLabel(f.store.getSnapshot().job),
|
||||
);
|
||||
assert.ok(
|
||||
panel
|
||||
.nodes('Text')
|
||||
.some((node) => node.props.children.includes?.('下载完成,等待重启')),
|
||||
);
|
||||
f.store.setJob({ status: 'ready', phase: 'downloading', percent: 62 });
|
||||
notice.render();
|
||||
assert.equal(notice.nodes('button')[0].props['aria-label'], '下载完成,等待重启');
|
||||
f.store.setJob({ status: 'completed', phase: 'ready' });
|
||||
assert.equal(notice.render(), null);
|
||||
panel.render();
|
||||
assert.ok(
|
||||
panel
|
||||
.nodes('Text')
|
||||
.some((node) => node.props.children.includes?.('升级成功')),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user