Merge remote-tracking branch 'origin/feat/agc-llm-router-official-chain' into feat/agc-llm-router-official-chain
This commit is contained in:
@@ -41,6 +41,20 @@ test('manifest contains version, download URL and integrity fields', () => {
|
||||
assert.equal(typeof manifest.size, 'number');
|
||||
});
|
||||
|
||||
test('manifest preserves multiline release notes', () => {
|
||||
const previous = process.env.AGC_UPDATE_RELEASE_NOTES;
|
||||
process.env.AGC_UPDATE_RELEASE_NOTES = '第一行\n第二行\r\n第三行';
|
||||
try {
|
||||
const manifest = createUpdateManifest(
|
||||
new URL('../package.json', import.meta.url).pathname,
|
||||
);
|
||||
assert.equal(manifest.releaseNotes, '第一行\n第二行\r\n第三行');
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.AGC_UPDATE_RELEASE_NOTES;
|
||||
else process.env.AGC_UPDATE_RELEASE_NOTES = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test('next release version follows the higher local or OSS version', () => {
|
||||
assert.equal(compareVersions('0.1.15', '0.1.12'), 1);
|
||||
assert.equal(nextPatchVersion('0.1.12', '0.1.15'), '0.1.16');
|
||||
|
||||
+52
-10
@@ -18,6 +18,11 @@ export type LocalGamePreviewFitLayout = {
|
||||
scale: number;
|
||||
};
|
||||
|
||||
type LocalGamePreviewViewportSize = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type LocalGamePreviewLike = {
|
||||
status?: string | null;
|
||||
url?: string | null;
|
||||
@@ -91,13 +96,30 @@ export function resolveLocalGamePreviewFitLayout(
|
||||
export function resolveLocalGamePreviewContentSizeUpdate(
|
||||
current: LocalGamePreviewContentSize | null,
|
||||
next: LocalGamePreviewContentSize,
|
||||
nativeViewport: { width: number; height: number },
|
||||
appliedViewport: { width: number; height: number },
|
||||
nativeViewport: LocalGamePreviewViewportSize,
|
||||
appliedViewport: LocalGamePreviewViewportSize,
|
||||
previousReportedViewport: LocalGamePreviewViewportSize | null = null,
|
||||
): LocalGamePreviewContentSize | null {
|
||||
const reportsViewport = (viewport: { width: number; height: number }) =>
|
||||
Math.abs(next.viewportWidth - viewport.width) < 1 &&
|
||||
Math.abs(next.viewportHeight - viewport.height) < 1;
|
||||
if (!reportsViewport(nativeViewport) && !reportsViewport(appliedViewport)) {
|
||||
const reportsNativeViewport = previewReportMatchesViewport(
|
||||
next,
|
||||
nativeViewport,
|
||||
);
|
||||
const reportsAppliedViewport = previewReportMatchesViewport(
|
||||
next,
|
||||
appliedViewport,
|
||||
);
|
||||
if (!reportsNativeViewport && !reportsAppliedViewport) {
|
||||
return current;
|
||||
}
|
||||
const followsViewportChange =
|
||||
previousReportedViewport !== null &&
|
||||
!previewReportMatchesViewport(next, previousReportedViewport);
|
||||
if (
|
||||
current &&
|
||||
reportsAppliedViewport &&
|
||||
!reportsNativeViewport &&
|
||||
followsViewportChange
|
||||
) {
|
||||
return current;
|
||||
}
|
||||
if (
|
||||
@@ -114,6 +136,16 @@ export function resolveLocalGamePreviewContentSizeUpdate(
|
||||
};
|
||||
}
|
||||
|
||||
function previewReportMatchesViewport(
|
||||
report: LocalGamePreviewContentSize,
|
||||
viewport: LocalGamePreviewViewportSize,
|
||||
) {
|
||||
return (
|
||||
Math.abs(report.viewportWidth - viewport.width) < 1 &&
|
||||
Math.abs(report.viewportHeight - viewport.height) < 1
|
||||
);
|
||||
}
|
||||
|
||||
export function LocalGamePreviewFrame({
|
||||
preview,
|
||||
title,
|
||||
@@ -128,6 +160,9 @@ export function LocalGamePreviewFrame({
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const measuredContainerSizeRef = useRef({ width: 1, height: 1 });
|
||||
const contentSizeRef = useRef<LocalGamePreviewContentSize | null>(null);
|
||||
const reportedViewportSizeRef = useRef<LocalGamePreviewViewportSize | null>(
|
||||
null,
|
||||
);
|
||||
const [containerSize, setContainerSize] = useState({ width: 1, height: 1 });
|
||||
const [contentSize, setContentSize] =
|
||||
useState<LocalGamePreviewContentSize | null>(null);
|
||||
@@ -150,10 +185,6 @@ export function LocalGamePreviewFrame({
|
||||
}
|
||||
measuredContainerSizeRef.current = next;
|
||||
setContainerSize(next);
|
||||
if (contentSizeRef.current) {
|
||||
contentSizeRef.current = null;
|
||||
setContentSize(null);
|
||||
}
|
||||
};
|
||||
update();
|
||||
if (typeof window.ResizeObserver === 'function') {
|
||||
@@ -166,6 +197,7 @@ export function LocalGamePreviewFrame({
|
||||
}, [embeddedUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
reportedViewportSizeRef.current = null;
|
||||
if (contentSizeRef.current) {
|
||||
contentSizeRef.current = null;
|
||||
setContentSize(null);
|
||||
@@ -190,12 +222,22 @@ export function LocalGamePreviewFrame({
|
||||
nativeViewport,
|
||||
current,
|
||||
);
|
||||
const reportsCurrentViewport =
|
||||
previewReportMatchesViewport(next, nativeViewport) ||
|
||||
previewReportMatchesViewport(next, appliedViewport);
|
||||
const resolved = resolveLocalGamePreviewContentSizeUpdate(
|
||||
current,
|
||||
next,
|
||||
nativeViewport,
|
||||
appliedViewport,
|
||||
reportedViewportSizeRef.current,
|
||||
);
|
||||
if (reportsCurrentViewport) {
|
||||
reportedViewportSizeRef.current = {
|
||||
width: next.viewportWidth,
|
||||
height: next.viewportHeight,
|
||||
};
|
||||
}
|
||||
if (resolved === current) return;
|
||||
contentSizeRef.current = resolved;
|
||||
setContentSize(resolved);
|
||||
|
||||
@@ -55,9 +55,10 @@ body {
|
||||
}
|
||||
.app-update-notice p {
|
||||
max-width: 320px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.app-update-notice button {
|
||||
flex: 0 0 auto;
|
||||
|
||||
@@ -32,4 +32,16 @@ describe('AGC update manifest', () => {
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('preserves multiline release notes', () => {
|
||||
expect(
|
||||
parseAppUpdateManifest({
|
||||
version: '0.1.13',
|
||||
downloadUrl: 'https://oss.example/agc.exe',
|
||||
releaseNotes: '第一行\n第二行\r\n第三行',
|
||||
}),
|
||||
).toMatchObject({
|
||||
releaseNotes: '第一行\n第二行\r\n第三行',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,173 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { act, render } from '@testing-library/react';
|
||||
import { createElement } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
LOCAL_GAME_PREVIEW_SIZE_MESSAGE,
|
||||
LocalGamePreviewFrame,
|
||||
parseLocalGamePreviewContentSize,
|
||||
resolveLocalGamePreviewContentSizeUpdate,
|
||||
resolveLocalGamePreviewFitLayout,
|
||||
} from '../src/features/project-workspace/LocalGamePreviewFrame';
|
||||
|
||||
describe('local game preview viewport fitting', () => {
|
||||
it('does not reset the fitted iframe to native size while its container resizes', () => {
|
||||
let containerRect = { width: 800, height: 500 };
|
||||
let resizeCallback: ResizeObserverCallback | null = null;
|
||||
const rectSpy = vi
|
||||
.spyOn(HTMLElement.prototype, 'getBoundingClientRect')
|
||||
.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
...containerRect,
|
||||
x: 0,
|
||||
y: 0,
|
||||
top: 0,
|
||||
right: containerRect.width,
|
||||
bottom: containerRect.height,
|
||||
left: 0,
|
||||
toJSON: () => ({}),
|
||||
}) as DOMRect,
|
||||
);
|
||||
const previousResizeObserver = window.ResizeObserver;
|
||||
window.ResizeObserver = class {
|
||||
constructor(callback: ResizeObserverCallback) {
|
||||
resizeCallback = callback;
|
||||
}
|
||||
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
};
|
||||
|
||||
const view = render(
|
||||
createElement(LocalGamePreviewFrame, {
|
||||
preview: { status: 'running', url: 'http://127.0.0.1:1234/' },
|
||||
title: 'preview',
|
||||
}),
|
||||
);
|
||||
const iframe = view.getByTitle('preview') as HTMLIFrameElement;
|
||||
act(() => {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
origin: 'http://127.0.0.1:1234',
|
||||
source: iframe.contentWindow,
|
||||
data: {
|
||||
type: LOCAL_GAME_PREVIEW_SIZE_MESSAGE,
|
||||
contentWidth: 800,
|
||||
contentHeight: 835,
|
||||
viewportWidth: 800,
|
||||
viewportHeight: 500,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(iframe.style.height).toBe('835px');
|
||||
|
||||
containerRect = { width: 1000, height: 600 };
|
||||
act(() => {
|
||||
if (!resizeCallback) throw new Error('ResizeObserver was not registered');
|
||||
resizeCallback([], {} as ResizeObserver);
|
||||
});
|
||||
expect(iframe.style.width).toBe('1000px');
|
||||
expect(iframe.style.height).toBe('835px');
|
||||
|
||||
view.unmount();
|
||||
window.ResizeObserver = previousResizeObserver;
|
||||
rectSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('keeps the current fit while the iframe reports its first host-applied viewport measurement', () => {
|
||||
const nativeViewport = { width: 1200, height: 700 };
|
||||
const appliedViewport = { width: 1200, height: 1000 };
|
||||
const current = {
|
||||
contentWidth: 1200,
|
||||
contentHeight: 1000,
|
||||
viewportWidth: 1200,
|
||||
viewportHeight: 700,
|
||||
};
|
||||
const firstAppliedViewportReport = {
|
||||
contentWidth: 1200,
|
||||
contentHeight: 700,
|
||||
viewportWidth: 1200,
|
||||
viewportHeight: 1000,
|
||||
};
|
||||
|
||||
expect(
|
||||
resolveLocalGamePreviewContentSizeUpdate(
|
||||
current,
|
||||
firstAppliedViewportReport,
|
||||
nativeViewport,
|
||||
appliedViewport,
|
||||
nativeViewport,
|
||||
),
|
||||
).toBe(current);
|
||||
});
|
||||
|
||||
it('does not let a delayed native report restart the fitted viewport loop', () => {
|
||||
const nativeViewport = { width: 1200, height: 700 };
|
||||
const appliedViewport = { width: 1200, height: 1000 };
|
||||
const current = {
|
||||
contentWidth: 1200,
|
||||
contentHeight: 1000,
|
||||
viewportWidth: 1200,
|
||||
viewportHeight: 700,
|
||||
};
|
||||
const delayedNativeReport = {
|
||||
contentWidth: 1200,
|
||||
contentHeight: 1000,
|
||||
viewportWidth: 1200,
|
||||
viewportHeight: 700,
|
||||
};
|
||||
|
||||
expect(
|
||||
resolveLocalGamePreviewContentSizeUpdate(
|
||||
current,
|
||||
delayedNativeReport,
|
||||
nativeViewport,
|
||||
appliedViewport,
|
||||
appliedViewport,
|
||||
),
|
||||
).toBe(current);
|
||||
});
|
||||
|
||||
it('keeps the current fit while a resized container applies its next viewport', () => {
|
||||
const resizedContainer = { width: 1000, height: 600 };
|
||||
const current = {
|
||||
contentWidth: 800,
|
||||
contentHeight: 835,
|
||||
viewportWidth: 800,
|
||||
viewportHeight: 500,
|
||||
};
|
||||
const appliedViewport = resolveLocalGamePreviewFitLayout(
|
||||
resizedContainer,
|
||||
current,
|
||||
);
|
||||
const resizedViewportReport = {
|
||||
contentWidth: 1000,
|
||||
contentHeight: 818,
|
||||
viewportWidth: appliedViewport.width,
|
||||
viewportHeight: appliedViewport.height,
|
||||
};
|
||||
|
||||
expect(appliedViewport).toEqual({
|
||||
width: 1000,
|
||||
height: 835,
|
||||
scale: 600 / 835,
|
||||
});
|
||||
expect(
|
||||
resolveLocalGamePreviewContentSizeUpdate(
|
||||
current,
|
||||
resizedViewportReport,
|
||||
resizedContainer,
|
||||
appliedViewport,
|
||||
{ width: 800, height: 835 },
|
||||
),
|
||||
).toBe(current);
|
||||
});
|
||||
|
||||
it('keeps a game at native size when its content fits', () => {
|
||||
expect(
|
||||
resolveLocalGamePreviewFitLayout(
|
||||
@@ -55,7 +215,7 @@ describe('local game preview viewport fitting', () => {
|
||||
).toEqual(report);
|
||||
});
|
||||
|
||||
it('accepts changed content after the iframe has adopted the first fit viewport', () => {
|
||||
it('accepts changed content after the fitted iframe viewport has stabilized', () => {
|
||||
const nativeViewport = { width: 1200, height: 700 };
|
||||
const current = {
|
||||
contentWidth: 1200,
|
||||
@@ -79,6 +239,7 @@ describe('local game preview viewport fitting', () => {
|
||||
width: 1200,
|
||||
height: 1000,
|
||||
},
|
||||
{ width: 1200, height: 1000 },
|
||||
),
|
||||
).toEqual({
|
||||
contentWidth: 1200,
|
||||
@@ -88,7 +249,7 @@ describe('local game preview viewport fitting', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shrinks the fitted frame when content becomes shorter inside the applied viewport', () => {
|
||||
it('shrinks the fitted frame when content becomes shorter inside a stable applied viewport', () => {
|
||||
const nativeViewport = { width: 1200, height: 700 };
|
||||
const current = {
|
||||
contentWidth: 1200,
|
||||
@@ -108,6 +269,7 @@ describe('local game preview viewport fitting', () => {
|
||||
changed,
|
||||
nativeViewport,
|
||||
{ width: 1200, height: 1000 },
|
||||
{ width: 1200, height: 1000 },
|
||||
);
|
||||
expect(updated).toEqual({
|
||||
contentWidth: 1200,
|
||||
|
||||
Reference in New Issue
Block a user