perf(frontend): 배포 버전을 ETag로 재검증한다
This commit is contained in:
@@ -8,8 +8,10 @@ const noticeMessage = '새 버전이 준비되었습니다. 새로고침하면
|
||||
const installVersionFixture = async (page: Page) => {
|
||||
let availableCommitSha = currentCommitSha;
|
||||
let requests = 0;
|
||||
await page.route('**/deployment-version.json*', async (route) => {
|
||||
const requestUrls: string[] = [];
|
||||
await page.route('**/deployment-version.json', async (route) => {
|
||||
requests += 1;
|
||||
requestUrls.push(route.request().url());
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
@@ -21,9 +23,46 @@ const installVersionFixture = async (page: Page) => {
|
||||
availableCommitSha = nextCommitSha;
|
||||
},
|
||||
requestCount: () => requests,
|
||||
requestUrls: () => requestUrls,
|
||||
};
|
||||
};
|
||||
|
||||
test('revalidates the stable production version URL with the browser-managed ETag', async ({ page }) => {
|
||||
test.skip(process.env.PLAYWRIGHT_FRONTEND_MODE !== 'production', 'Vite production ETag is required.');
|
||||
const requests: Array<Promise<{ url: string; ifNoneMatch: string | null }>> = [];
|
||||
const responseStatuses: number[] = [];
|
||||
page.on('request', (request) => {
|
||||
if (!new URL(request.url()).pathname.endsWith('/deployment-version.json')) return;
|
||||
requests.push(
|
||||
request.headerValue('if-none-match').then((ifNoneMatch) => ({
|
||||
url: request.url(),
|
||||
ifNoneMatch,
|
||||
}))
|
||||
);
|
||||
});
|
||||
page.on('response', (response) => {
|
||||
if (!new URL(response.url()).pathname.endsWith('/deployment-version.json')) return;
|
||||
responseStatuses.push(response.status());
|
||||
});
|
||||
|
||||
await page.goto(gamePath('/version-notice-etag-fixture'));
|
||||
await expect.poll(() => requests.length).toBeGreaterThan(0);
|
||||
await expect.poll(() => responseStatuses.length).toBeGreaterThan(0);
|
||||
const completedResponses = responseStatuses.length;
|
||||
await page.evaluate(() => window.dispatchEvent(new Event('online')));
|
||||
await expect.poll(() => requests.length).toBeGreaterThan(1);
|
||||
await expect.poll(() => responseStatuses.length).toBeGreaterThan(completedResponses);
|
||||
const requestDetails = await Promise.all(requests);
|
||||
|
||||
expect(new Set(requestDetails.map(({ url }) => url))).toEqual(
|
||||
new Set([new URL('deployment-version.json', page.url()).toString()])
|
||||
);
|
||||
expect(requestDetails[0]?.ifNoneMatch).toBeNull();
|
||||
expect(requestDetails.slice(1).some(({ ifNoneMatch }) => Boolean(ifNoneMatch))).toBe(true);
|
||||
// Chromium exposes a successfully revalidated cached response to Fetch as the usable 200 response.
|
||||
expect(responseStatuses.every((status) => status === 200)).toBe(true);
|
||||
});
|
||||
|
||||
for (const viewport of [
|
||||
{ name: 'desktop', width: 1280, height: 800 },
|
||||
{ name: 'mobile', width: 390, height: 844 },
|
||||
@@ -34,6 +73,7 @@ for (const viewport of [
|
||||
await page.goto(gamePath('/version-notice-fixture'));
|
||||
await expect(page.getByRole('heading', { name: 'Not Found' })).toBeVisible();
|
||||
await expect.poll(fixture.requestCount).toBeGreaterThan(0);
|
||||
expect(fixture.requestUrls().every((url) => new URL(url).search === '')).toBe(true);
|
||||
await page.evaluate(() => {
|
||||
Object.assign(window, { __versionNoticePageMarker: 'kept' });
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ export type DeploymentVersionCheckerOptions = {
|
||||
versionUrl: string;
|
||||
fetchVersion?: typeof fetch;
|
||||
storage?: VersionStorage;
|
||||
now?: () => number;
|
||||
onVersionChanged: (availableCommitSha: string) => void;
|
||||
};
|
||||
|
||||
@@ -26,7 +25,6 @@ const notificationStorageKey = (versionUrl: string, availableCommitSha: string):
|
||||
export const createDeploymentVersionChecker = (options: DeploymentVersionCheckerOptions) => {
|
||||
const currentCommitSha = options.currentCommitSha.trim().toLowerCase();
|
||||
const fetchVersion = options.fetchVersion ?? fetch;
|
||||
const now = options.now ?? Date.now;
|
||||
let inFlight: Promise<void> | null = null;
|
||||
let lastNotifiedCommitSha: string | null = null;
|
||||
|
||||
@@ -48,10 +46,8 @@ export const createDeploymentVersionChecker = (options: DeploymentVersionChecker
|
||||
|
||||
const run = async (): Promise<void> => {
|
||||
if (!fullCommitShaPattern.test(currentCommitSha)) return;
|
||||
const separator = options.versionUrl.includes('?') ? '&' : '?';
|
||||
const response = await fetchVersion(`${options.versionUrl}${separator}t=${now()}`, {
|
||||
cache: 'no-store',
|
||||
headers: { 'Cache-Control': 'no-cache' },
|
||||
const response = await fetchVersion(options.versionUrl, {
|
||||
cache: 'no-cache',
|
||||
});
|
||||
if (!response.ok) return;
|
||||
const availableCommitSha = parseDeploymentCommitSha(await response.json());
|
||||
|
||||
@@ -28,7 +28,7 @@ void test('exposes only the read-only build commit in the deployment version ass
|
||||
assert.deepEqual(JSON.parse(deploymentVersionAssetSource(currentCommitSha)), { commitSha: currentCommitSha });
|
||||
});
|
||||
|
||||
void test('notifies once per available version and bypasses browser caches', async () => {
|
||||
void test('notifies once per available version while revalidating one stable URL', async () => {
|
||||
const requests: Array<{ url: string; init?: RequestInit }> = [];
|
||||
const stored = new Map<string, string>();
|
||||
const notifications: string[] = [];
|
||||
@@ -36,7 +36,6 @@ void test('notifies once per available version and bypasses browser caches', asy
|
||||
const checker = createDeploymentVersionChecker({
|
||||
currentCommitSha,
|
||||
versionUrl: '/che/deployment-version.json',
|
||||
now: () => 1234,
|
||||
storage: {
|
||||
getItem: (key) => stored.get(key) ?? null,
|
||||
setItem: (key, value) => void stored.set(key, value),
|
||||
@@ -56,9 +55,18 @@ void test('notifies once per available version and bypasses browser caches', asy
|
||||
await checker.check();
|
||||
|
||||
assert.deepEqual(notifications, [nextCommitSha, laterCommitSha]);
|
||||
assert.equal(requests.every(({ url }) => url === '/che/deployment-version.json?t=1234'), true);
|
||||
assert.equal(requests.every(({ init }) => init?.cache === 'no-store'), true);
|
||||
assert.equal(requests.every(({ init }) => new Headers(init?.headers).get('Cache-Control') === 'no-cache'), true);
|
||||
assert.equal(
|
||||
requests.every(({ url }) => url === '/che/deployment-version.json'),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
requests.every(({ init }) => init?.cache === 'no-cache'),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
requests.every(({ init }) => init?.headers === undefined),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
void test('shares the once-only notice within the current tab session', async () => {
|
||||
|
||||
Reference in New Issue
Block a user