chore: release 1.0.2-beta12
This commit is contained in:
1 parent
bf95d715a4
commit
c44fb1a0a5
43 files changed
+2130
-470
No files matched your search
@@ -0,0 +1,27 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { shouldDelayAppUpdate } from './app-update-policy.ts'
|
||||
|
||||
const now = Date.parse('2026-10-21T08:00:00Z')
|
||||
const recent = { publishedAt: '2026-10-21T07:00:00Z' }
|
||||
|
||||
test('administrator rollbacks bypass the normal release waiting period', () => {
|
||||
assert.equal(shouldDelayAppUpdate({ ...recent, rollback: true }, 'release', false, now), false)
|
||||
assert.equal(shouldDelayAppUpdate({ rollback: true }, 'release', false, now), false)
|
||||
})
|
||||
|
||||
test('ordinary releases retain the 24-hour delay and missing dates remain delayed', () => {
|
||||
assert.equal(shouldDelayAppUpdate(recent, 'release', false, now), true)
|
||||
assert.equal(shouldDelayAppUpdate({}, 'release', false, now), true)
|
||||
assert.equal(
|
||||
shouldDelayAppUpdate({ publishedAt: '2026-10-20T08:00:00Z' }, 'release', false, now),
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
test('beta, forced updates and immediate fetching still bypass the delay', () => {
|
||||
assert.equal(shouldDelayAppUpdate(recent, 'beta', false, now), false)
|
||||
assert.equal(shouldDelayAppUpdate({ ...recent, forceUpdate: true }, 'release', false, now), false)
|
||||
assert.equal(shouldDelayAppUpdate(recent, 'release', true, now), false)
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
export function shouldDelayAppUpdate(
|
||||
update: { publishedAt?: string; forceUpdate?: boolean; rollback?: boolean },
|
||||
channel: string,
|
||||
immediateUpdateFetch: boolean,
|
||||
now = Date.now(),
|
||||
): boolean {
|
||||
if (channel !== 'release' || immediateUpdateFetch || update.forceUpdate || update.rollback)
|
||||
return false
|
||||
const publishedAt = Date.parse(update.publishedAt ?? '')
|
||||
return !Number.isFinite(publishedAt) || now < publishedAt + 24 * 60 * 60 * 1000
|
||||
}
|
||||
@@ -1,20 +1,32 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { registerHooks } from 'node:module'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
receiveSkinSiteMessage,
|
||||
resetSkinSiteSession,
|
||||
setSkinSiteFrame,
|
||||
SKIN_SITE_ORIGIN,
|
||||
} from '../composables/skin-site-session.ts'
|
||||
const hooks = registerHooks({
|
||||
resolve(specifier, context, nextResolve) {
|
||||
if (specifier === '@tauri-apps/plugin-http') {
|
||||
return {
|
||||
url: `data:text/javascript,${encodeURIComponent(`export const fetch=async url=>Response.json({payload:url.endsWith('/user/refresh')?{jwt:'fixture.jwt'}:url.endsWith('/user')?{uuid:'owner',username:'Owner',permissions:[]}:[]})`)}`,
|
||||
shortCircuit: true,
|
||||
}
|
||||
}
|
||||
if (specifier.startsWith('@/')) {
|
||||
return nextResolve(new URL(`../${specifier.slice(2)}.ts`, import.meta.url).href, context)
|
||||
}
|
||||
return nextResolve(specifier, context)
|
||||
},
|
||||
})
|
||||
const { initSkinSiteSession, resetSkinSiteSession, setSkinSiteFrame } =
|
||||
await import('../composables/skin-site-session.ts')
|
||||
hooks.deregister()
|
||||
import { useNetworkStatus } from '../composables/useNetworkStatus.ts'
|
||||
import {
|
||||
type InstancePlayer,
|
||||
const {
|
||||
onInstancePlayerChanged,
|
||||
prepareInstancePlayer,
|
||||
registerInstancePlayerPicker,
|
||||
saveInstancePlayer,
|
||||
} from './instance-player.ts'
|
||||
} = await import('./instance-player.ts')
|
||||
import type { InstancePlayer } from './instance-player.ts'
|
||||
|
||||
test('instance player selection persists independently from the globally selected account', async () => {
|
||||
const previousNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator')
|
||||
@@ -84,23 +96,16 @@ test('signed-in skin site renews the saved player without prompting and only not
|
||||
}
|
||||
const calls: string[] = []
|
||||
let failSave = false
|
||||
const frame = {
|
||||
postMessage(data: { type: string; requestId: string }) {
|
||||
if (data.type === 'starlight-pack-token-request')
|
||||
receiveSkinSiteMessage(
|
||||
{
|
||||
origin: SKIN_SITE_ORIGIN,
|
||||
source: frame,
|
||||
data: {
|
||||
type: 'starlight-pack-token-result',
|
||||
requestId: data.requestId,
|
||||
token: 'fixture.jwt',
|
||||
},
|
||||
} as unknown as MessageEvent,
|
||||
frame,
|
||||
)
|
||||
const previousStorage = Object.getOwnPropertyDescriptor(globalThis, 'localStorage')
|
||||
const storage = new Map([['starlight-skin-site-token', 'old.jwt']])
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
configurable: true,
|
||||
value: {
|
||||
getItem: (key: string) => storage.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => storage.set(key, value),
|
||||
removeItem: (key: string) => storage.delete(key),
|
||||
},
|
||||
} as unknown as Window
|
||||
})
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: {
|
||||
@@ -128,19 +133,7 @@ test('signed-in skin site renews the saved player without prompting and only not
|
||||
const stop = onInstancePlayerChanged((id) => changes.push(id))
|
||||
try {
|
||||
resetSkinSiteSession()
|
||||
setSkinSiteFrame(frame)
|
||||
receiveSkinSiteMessage(
|
||||
{
|
||||
origin: SKIN_SITE_ORIGIN,
|
||||
source: frame,
|
||||
data: {
|
||||
type: 'starlight-skin-session',
|
||||
status: 'signed-in',
|
||||
user: { uuid: 'owner', username: 'Owner' },
|
||||
},
|
||||
} as unknown as MessageEvent,
|
||||
frame,
|
||||
)
|
||||
await initSkinSiteSession()
|
||||
await prepareInstancePlayer('one')
|
||||
assert.equal(prompts, 0)
|
||||
assert.ok(calls.includes('plugin:auth|login_skin_site_player'))
|
||||
@@ -149,6 +142,7 @@ test('signed-in skin site renews the saved player without prompting and only not
|
||||
failSave = true
|
||||
await assert.rejects(saveInstancePlayer('two', player), /Save failed/)
|
||||
assert.deepEqual(changes, ['one'])
|
||||
storage.clear()
|
||||
resetSkinSiteSession()
|
||||
await prepareInstancePlayer('one')
|
||||
assert.equal(prompts, 1)
|
||||
@@ -157,6 +151,8 @@ test('signed-in skin site renews the saved player without prompting and only not
|
||||
unregister()
|
||||
resetSkinSiteSession()
|
||||
setSkinSiteFrame(null)
|
||||
if (previousStorage) Object.defineProperty(globalThis, 'localStorage', previousStorage)
|
||||
else Reflect.deleteProperty(globalThis, 'localStorage')
|
||||
if (previous) Object.defineProperty(globalThis, 'window', previous)
|
||||
else Reflect.deleteProperty(globalThis, 'window')
|
||||
if (previousNavigator) Object.defineProperty(globalThis, 'navigator', previousNavigator)
|
||||
|
||||
@@ -72,7 +72,7 @@ test('notice query uses the target instance value and registered Tauri command',
|
||||
)
|
||||
const helperSource = readFileSync(new URL('./instance.ts', import.meta.url), 'utf8')
|
||||
const tauriSource = readFileSync(
|
||||
new URL('../../../app/src/api/instance.rs', import.meta.url),
|
||||
new URL('../../../app/src/api/instance/mod.rs', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
assert.match(querySource, /\['post-upgrade-notice', instanceId\]/)
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { registerHooks } from 'node:module'
|
||||
import test from 'node:test'
|
||||
|
||||
import { loadSkinArmorPreview, saveSkinArmorPreview } from './skin-armor-preview.ts'
|
||||
const hooks = registerHooks({
|
||||
resolve(specifier, context, nextResolve) {
|
||||
if (specifier === '@modrinth/ui') {
|
||||
return nextResolve(
|
||||
new URL(
|
||||
'../../../../packages/ui/src/composables/skin-rendering/armor-preview-types.ts',
|
||||
import.meta.url,
|
||||
).href,
|
||||
context,
|
||||
)
|
||||
}
|
||||
return nextResolve(specifier, context)
|
||||
},
|
||||
})
|
||||
const { loadSkinArmorPreview, saveSkinArmorPreview } = await import('./skin-armor-preview.ts')
|
||||
hooks.deregister()
|
||||
|
||||
const originalStorage = Object.getOwnPropertyDescriptor(globalThis, 'localStorage')
|
||||
|
||||
|
||||
@@ -414,8 +414,8 @@ export function skinSitePublicDonationSummary(): Promise<
|
||||
return skinSiteRequest<SkinSitePublicDonationSummary>('donation/summary', { auth: false })
|
||||
}
|
||||
|
||||
export function skinSiteDonationList(offset = 0): Promise<SkinSiteResponse<SkinSiteDonationPage>> {
|
||||
return skinSiteRequest<SkinSiteDonationPage>(`donation/list?limit=20&offset=${offset}`, {
|
||||
export function skinSiteDonationList(offset = 0): Promise<SkinSiteResponse<SkinSitePublicDonationPage>> {
|
||||
return skinSiteRequest<SkinSitePublicDonationPage>(`donation/list?limit=20&offset=${offset}`, {
|
||||
auth: false,
|
||||
})
|
||||
}
|
||||
@@ -1214,6 +1214,10 @@ export function skinSiteActivateLauncherRelease(
|
||||
})
|
||||
}
|
||||
|
||||
export function skinSiteDeleteLauncherRelease(id: number): Promise<SkinSiteResponse<boolean>> {
|
||||
return skinSiteRequest<boolean>('launcher/admin/' + id, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
export interface SkinSiteLauncherUploadStatus {
|
||||
id: string
|
||||
chunkSize: number
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { registerHooks } from 'node:module'
|
||||
import test from 'node:test'
|
||||
|
||||
const testGlobal = globalThis as unknown as {
|
||||
skinImageTestFetch: (url: string) => Promise<Response>
|
||||
}
|
||||
const hooks = registerHooks({
|
||||
resolve(specifier, context, nextResolve) {
|
||||
if (specifier === '@tauri-apps/plugin-http') {
|
||||
return {
|
||||
url: 'data:text/javascript,export const fetch=(...args)=>globalThis.skinImageTestFetch(...args)',
|
||||
shortCircuit: true,
|
||||
}
|
||||
}
|
||||
return nextResolve(specifier, context)
|
||||
},
|
||||
})
|
||||
const { fetchImageAsDataUrl } = await import('./skin-site-render.ts')
|
||||
hooks.deregister()
|
||||
|
||||
test.beforeEach((t) => {
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
throw new TypeError('Cross-origin image request blocked')
|
||||
})
|
||||
const previousReader = Object.getOwnPropertyDescriptor(globalThis, 'FileReader')
|
||||
Object.defineProperty(globalThis, 'FileReader', {
|
||||
configurable: true,
|
||||
value: class {
|
||||
result: string | null = null
|
||||
onload?: () => void
|
||||
onerror?: () => void
|
||||
readAsDataURL(blob: Blob) {
|
||||
void blob.arrayBuffer().then(
|
||||
(bytes) => {
|
||||
this.result = `data:${blob.type};base64,${Buffer.from(bytes).toString('base64')}`
|
||||
this.onload?.()
|
||||
},
|
||||
() => this.onerror?.(),
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
t.after(() => {
|
||||
if (previousReader) Object.defineProperty(globalThis, 'FileReader', previousReader)
|
||||
else Reflect.deleteProperty(globalThis, 'FileReader')
|
||||
})
|
||||
})
|
||||
|
||||
test('skin textures download through the native client when browser CORS blocks the site', async () => {
|
||||
testGlobal.skinImageTestFetch = async () =>
|
||||
new Response(new Uint8Array([137, 80, 78, 71]), { headers: { 'Content-Type': 'image/png' } })
|
||||
assert.equal(
|
||||
await fetchImageAsDataUrl('https://skin.starlight.cool/yggdrasil/textures/test'),
|
||||
'data:image/png;base64,iVBORw==',
|
||||
)
|
||||
})
|
||||
|
||||
test('failed image downloads report the HTTP error instead of accepting the response as a texture', async () => {
|
||||
testGlobal.skinImageTestFetch = async () => new Response('Unavailable', { status: 503 })
|
||||
await assert.rejects(
|
||||
fetchImageAsDataUrl('https://skin.starlight.cool/yggdrasil/textures/test'),
|
||||
/HTTP 503/,
|
||||
)
|
||||
})
|
||||
@@ -1,3 +1,5 @@
|
||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
|
||||
|
||||
/**
|
||||
* Renders a player's skin into a base64 head image, mirroring the skin site's
|
||||
* own head cropper (front face at 8,8 plus the hat overlay at 40,8).
|
||||
@@ -28,9 +30,9 @@ function loadImage(src: string): Promise<HTMLImageElement> {
|
||||
})
|
||||
}
|
||||
|
||||
/** Crop the head (and hat overlay) out of a skin image URL. Returns a data URL. */
|
||||
export async function renderSkinHead(skinUrl: string, scale = 2): Promise<string> {
|
||||
const img = await loadImage(skinUrl)
|
||||
/** Crop the head (and hat overlay) from a locally downloaded skin data URL. */
|
||||
export async function renderSkinHead(skinDataUrl: string, scale = 2): Promise<string> {
|
||||
const img = await loadImage(skinDataUrl)
|
||||
const { canvas, ctx } = getBuffer()
|
||||
|
||||
canvas.width = HEAD_BUFFER_SIZE
|
||||
@@ -74,7 +76,7 @@ export async function renderSkinHead(skinUrl: string, scale = 2): Promise<string
|
||||
|
||||
/** Fetch an image URL as a data URL (used to keep full skin textures local). */
|
||||
export async function fetchImageAsDataUrl(url: string): Promise<string> {
|
||||
const response = await fetch(url)
|
||||
const response = await tauriFetch(url)
|
||||
if (!response.ok) throw new Error(`无法加载图片(HTTP ${response.status})`)
|
||||
const blob = await response.blob()
|
||||
return await new Promise<string>((resolve, reject) => {
|
||||
|
||||
Reference in new issue
Block a user