Icon-only playlist header and filter-row actions, one-row transport, resumable update download
- Playlist title wraps within 90% of the width; rename and share are icons beside it, and tapping the title opens the rename box showing the full name - Offline, pin, select and delete are icon buttons to the right of the filter input - Transport is one row again: prev, back 10, play, forward 10, next - Update download stages each file as it arrives so a retry only fetches what is missing, keeps 4 requests in flight and allows 5 minutes
This commit is contained in:
@@ -52,6 +52,7 @@ function fakeCaches(initial) {
|
||||
return {
|
||||
store,
|
||||
async keys() { return [...store.keys()]; },
|
||||
async delete(name) { return store.delete(name); },
|
||||
async open(name) {
|
||||
if (!store.has(name)) store.set(name, new Map());
|
||||
const m = store.get(name);
|
||||
@@ -59,6 +60,7 @@ function fakeCaches(initial) {
|
||||
async keys() { return [...m.keys()].map((url) => ({ url })); },
|
||||
async put(url, res) { m.set(url, res.body); },
|
||||
async delete(req) { return m.delete(req.url); },
|
||||
async match(url) { return m.has(url) ? res(m.get(url)) : undefined; },
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -154,7 +156,58 @@ test('refreshShellInPlace times out instead of hanging on a stalled request', as
|
||||
|
||||
test('refreshShellInPlace with no shell caches (uncontrolled page) is a no-op', async () => {
|
||||
const r = await refreshShellInPlace({ cachesApi: fakeCaches({}), isShellCache, fetchFn: async () => { throw new Error('no fetch expected'); } });
|
||||
assert.deepStrictEqual(r, { refreshed: 0, caches: 0 });
|
||||
assert.deepStrictEqual(r, { refreshed: 0, caches: 0, resumed: 0 });
|
||||
});
|
||||
|
||||
test('refreshShellInPlace resumes: files that arrived before a failure are not fetched again', async () => {
|
||||
const cachesApi = fakeCaches({
|
||||
'ytplayer-old': { [`${ORIGIN}/`]: 'old-index', [`${ORIGIN}/app.js`]: 'old-app', [`${ORIGIN}/styles.css`]: 'old-css' },
|
||||
});
|
||||
let cssUp = false;
|
||||
const fetched = [];
|
||||
const fetchFn = async (url) => {
|
||||
fetched.push(new URL(url).pathname);
|
||||
return url.includes('/styles.css') && !cssUp ? res('gateway timeout', 504) : res('NEW ' + new URL(url).pathname);
|
||||
};
|
||||
await assert.rejects(refreshShellInPlace({ cachesApi, isShellCache, fetchFn, stagingKey: 'B1' }), /styles\.css/);
|
||||
assert.deepStrictEqual([...cachesApi.store.get('ytplayer-old').values()], ['old-index', 'old-app', 'old-css'], 'live shell untouched');
|
||||
fetched.length = 0;
|
||||
cssUp = true;
|
||||
const r = await refreshShellInPlace({ cachesApi, isShellCache, fetchFn, stagingKey: 'B1' });
|
||||
assert.deepStrictEqual(fetched, ['/styles.css'], 'only the missing file is fetched on the retry');
|
||||
assert.strictEqual(r.resumed, 2);
|
||||
assert.strictEqual(cachesApi.store.get('ytplayer-old').get(`${ORIGIN}/app.js`), 'NEW /app.js');
|
||||
assert.strictEqual(cachesApi.store.get('ytplayer-old').get(`${ORIGIN}/styles.css`), 'NEW /styles.css');
|
||||
assert.ok(![...cachesApi.store.keys()].some((k) => k.startsWith('ytp-staging-')), 'staging removed after success');
|
||||
});
|
||||
|
||||
test('refreshShellInPlace never reuses files staged for a different build', async () => {
|
||||
const cachesApi = fakeCaches({
|
||||
'ytplayer-old': { [`${ORIGIN}/app.js`]: 'old-app' },
|
||||
'ytp-staging-OLDBUILD': { [`${ORIGIN}/app.js`]: 'STALE' },
|
||||
});
|
||||
const r = await refreshShellInPlace({ cachesApi, isShellCache, stagingKey: 'NEWBUILD', fetchFn: async () => res('FRESH') });
|
||||
assert.strictEqual(r.resumed, 0);
|
||||
assert.strictEqual(cachesApi.store.get('ytplayer-old').get(`${ORIGIN}/app.js`), 'FRESH');
|
||||
assert.ok(!cachesApi.store.has('ytp-staging-OLDBUILD'));
|
||||
});
|
||||
|
||||
test('refreshShellInPlace keeps at most `concurrency` downloads in flight', async () => {
|
||||
const files = {};
|
||||
for (let i = 0; i < 12; i++) files[`${ORIGIN}/f${i}.js`] = 'old';
|
||||
const cachesApi = fakeCaches({ 'ytplayer-old': files });
|
||||
let inFlight = 0, peak = 0;
|
||||
await refreshShellInPlace({
|
||||
cachesApi, isShellCache, concurrency: 4,
|
||||
fetchFn: async () => {
|
||||
inFlight++; peak = Math.max(peak, inFlight);
|
||||
await new Promise((r) => setImmediate(r));
|
||||
inFlight--;
|
||||
return res('NEW');
|
||||
},
|
||||
});
|
||||
assert.strictEqual(peak, 4);
|
||||
assert.ok([...cachesApi.store.get('ytplayer-old').values()].every((v) => v === 'NEW'));
|
||||
});
|
||||
|
||||
test('applyUpdate refreshes first, then activates the waiting worker, then reloads once on controllerchange', async () => {
|
||||
|
||||
Reference in New Issue
Block a user