From f70079ffe310b0c41ba73f8792bde9bf4cc4d65e Mon Sep 17 00:00:00 2001 From: tbphp Date: Wed, 29 Jul 2026 02:24:31 +0800 Subject: [PATCH] =?UTF-8?q?test(web):=20=E5=9B=BA=E5=AE=9A=E4=B8=89?= =?UTF-8?q?=E6=9C=9F=E8=A7=86=E8=A7=89=E4=B8=8E=E5=87=A0=E4=BD=95=E5=9C=BA?= =?UTF-8?q?=E6=99=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/e2e/business-flows.spec.ts | 76 ++++++++++++++++++++++++++++----- web/e2e/csp-smoke.spec.ts | 18 +++++--- web/e2e/fixtures.ts | 3 +- web/e2e/visual-fixtures.test.ts | 45 +++++++++++++++++++ web/e2e/visual-fixtures.ts | 34 +++++++++++++++ 5 files changed, 158 insertions(+), 18 deletions(-) create mode 100644 web/e2e/visual-fixtures.test.ts create mode 100644 web/e2e/visual-fixtures.ts diff --git a/web/e2e/business-flows.spec.ts b/web/e2e/business-flows.spec.ts index edf6dc1c..9e11507d 100644 --- a/web/e2e/business-flows.spec.ts +++ b/web/e2e/business-flows.spec.ts @@ -1,4 +1,5 @@ import { expect, test } from './fixtures' +import { visualThemes, visualViewports } from './visual-fixtures' const authKey = process.env.GPT_LOAD_E2E_AUTH_KEY const upstreamURL = process.env.GPT_LOAD_E2E_UPSTREAM_URL @@ -453,44 +454,93 @@ test('critical management journey works through the embedded binary', async ({ p }) await test.step('verify responsive light and dark layouts with table-local overflow', async () => { - const viewports = [ - { width: 375, height: 812 }, - { width: 768, height: 900 }, - { width: 1024, height: 900 }, - { width: 1440, height: 900 }, - ] as const - - for (const theme of ['light', 'dark'] as const) { + for (const theme of visualThemes) { await page.evaluate((value) => localStorage.setItem('gpt-load.theme', value), theme) - for (const viewport of viewports) { + for (const viewport of visualViewports) { await page.setViewportSize(viewport) await page.goto('/') await expect(page.locator('html')).toHaveAttribute('data-theme', theme) await expect(page.locator('[data-test="home-usage-requests"]')).toBeVisible() + await expect(page.locator('[data-test="connection-snippet"]')).toContainText( + 'GPT_LOAD_API_KEY', + ) const layout = await page.evaluate(() => { + const topbar = document.querySelector('.app-topbar')?.getBoundingClientRect() + const main = document.querySelector('#main-content')?.getBoundingClientRect() + const operational = document + .querySelector('[data-test="home-operational-overview"]') + ?.getBoundingClientRect() const overview = document.querySelector('.home-overview')?.getBoundingClientRect() const usage = document.querySelector('.usage-summary-section')?.getBoundingClientRect() const groups = document.querySelector('.groups-section')?.getBoundingClientRect() + const connection = document + .querySelector('[data-test="home-connection"]') + ?.getBoundingClientRect() return { documentWidth: document.documentElement.scrollWidth, + operationalBottom: operational?.bottom ?? -1, + operationalTop: operational?.top ?? -1, overviewBottom: overview?.bottom ?? -1, + overviewTop: overview?.top ?? -1, usageBottom: usage?.bottom ?? -1, usageTop: usage?.top ?? -1, + groupsBottom: groups?.bottom ?? -1, groupsTop: groups?.top ?? -1, + connectionTop: connection?.top ?? -1, + mainTop: main?.top ?? -1, + topbarBottom: topbar?.bottom ?? -1, + topbarHeight: topbar?.height ?? -1, viewportWidth: window.innerWidth, } }) expect(layout.documentWidth).toBeLessThanOrEqual(layout.viewportWidth) - expect(layout.usageTop).toBeGreaterThanOrEqual(layout.overviewBottom) - expect(layout.groupsTop).toBeGreaterThanOrEqual(layout.usageBottom) + expect(layout.mainTop).toBeGreaterThanOrEqual(layout.topbarBottom) + expect(layout.topbarHeight).toBeLessThanOrEqual(viewport.width < 640 ? 132 : 80) + expect(layout.overviewTop).toBeGreaterThanOrEqual(layout.operationalTop) + expect(layout.usageTop).toBeGreaterThanOrEqual(layout.overviewTop) + expect(layout.usageBottom).toBeLessThanOrEqual(layout.overviewBottom) + expect(layout.groupsTop).toBeGreaterThanOrEqual(layout.operationalBottom) + expect(layout.connectionTop).toBeGreaterThanOrEqual(layout.groupsBottom) } } + const preferences = page.getByRole('button', { name: 'Preferences' }) + await preferences.click() + await expect(page.locator('[data-test="preferences-panel"]')).toBeVisible() + await page.keyboard.press('Escape') + await expect(preferences).toBeFocused() + + const protocol = page.getByLabel('Protocol', { exact: true }) + await protocol.click() + await page.getByRole('option', { name: 'Anthropic' }).click() + await expect(page.locator('[data-test="connection-snippet"]')).toContainText('/v1/messages') + await expect(page.locator('[data-test="connection-snippet"]')).toContainText( + 'anthropic-version', + ) + await protocol.click() + await page.getByRole('option', { name: 'Gemini' }).click() + await expect(page.locator('[data-test="connection-snippet"]')).toContainText('/v1beta/models/') + await expect(page.locator('[data-test="connection-snippet"]')).toContainText('x-goog-api-key') + await page.setViewportSize({ width: 375, height: 812 }) await page.goto('/settings/model-prices') + await expect(page.locator('[data-test^="builtin-price-card-"]').first()).toBeVisible() + await expect(page.locator('[data-test^="builtin-price-row-"]')).toHaveCount(0) + + await page.goto('/access-keys') + await expect(page.locator('[data-test^="access-key-card-"]').first()).toBeVisible() + await expect(page.locator('[data-test^="access-key-row-"]')).toHaveCount(0) + + await page.setViewportSize({ width: 768, height: 900 }) + await page.goto('/settings/model-prices') + await expect(page.locator('[data-test^="builtin-price-row-"]').first()).toBeVisible() + await expect(page.locator('[data-test^="builtin-price-card-"]')).toHaveCount(0) const priceTable = page.locator('.data-table__container').first() await expect(priceTable).toBeVisible() + await expect(priceTable).toHaveAttribute('tabindex', '0') + await priceTable.focus() + await expect(priceTable).toBeFocused() const overflow = await priceTable.evaluate((table) => ({ client: table.clientWidth, scroll: table.scrollWidth, @@ -499,6 +549,10 @@ test('critical management journey works through the embedded binary', async ({ p })) expect(overflow.scroll).toBeGreaterThan(overflow.client) expect(overflow.document).toBeLessThanOrEqual(overflow.viewport) + + await page.goto('/access-keys') + await expect(page.locator('[data-test^="access-key-row-"]').first()).toBeVisible() + await expect(page.locator('[data-test^="access-key-card-"]')).toHaveCount(0) }) await test.step('inspect current route', async () => { diff --git a/web/e2e/csp-smoke.spec.ts b/web/e2e/csp-smoke.spec.ts index 3823782a..9146d6ae 100644 --- a/web/e2e/csp-smoke.spec.ts +++ b/web/e2e/csp-smoke.spec.ts @@ -10,8 +10,12 @@ test('Reka overlays work under production CSP', async ({ page }, testInfo) => { video: 'off', }) const errors: string[] = [] + const unauthorizedResponses: string[] = [] page.on('console', (message) => message.type() === 'error' && errors.push(message.text())) page.on('pageerror', (error) => errors.push(error.message)) + page.on('response', (response) => { + if (response.status() === 401) unauthorizedResponses.push(response.url()) + }) await page.addInitScript(() => { localStorage.setItem('gpt-load.locale', 'en-US') const violations: string[] = [] @@ -42,18 +46,20 @@ test('Reka overlays work under production CSP', async ({ page }, testInfo) => { expect(revealBounds).not.toBeNull() expect(revealBounds!.width).toBeGreaterThanOrEqual(44) expect(revealBounds!.height).toBeGreaterThanOrEqual(44) - const languageTrigger = page.getByLabel('Language') - await languageTrigger.click() + const preferencesTrigger = page.getByRole('button', { name: 'Preferences' }) + await preferencesTrigger.click() + await expect(page.locator('[data-test="preferences-panel"]')).toBeVisible() await page.keyboard.press('Escape') - await expect(languageTrigger).toBeFocused() - await languageTrigger.click() - await page.getByRole('option', { name: 'English' }).click() - await page.getByRole('button', { name: 'Use dark theme' }).click() + await expect(preferencesTrigger).toBeFocused() + await preferencesTrigger.click() + await page.getByLabel('English', { exact: true }).check() + await page.getByLabel('Dark', { exact: true }).check() await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark') const cspViolations = await page.evaluate( () => (window as Window & { __cspViolations?: string[] }).__cspViolations ?? [], ) expect(cspViolations).toEqual([]) + expect(unauthorizedResponses).toEqual([]) expect(errors).toEqual([]) const recordings = testInfo.attachments.filter(({ name }) => /trace|screenshot|video/i.test(name)) diff --git a/web/e2e/fixtures.ts b/web/e2e/fixtures.ts index 0bca437a..62f6375c 100644 --- a/web/e2e/fixtures.ts +++ b/web/e2e/fixtures.ts @@ -1,12 +1,13 @@ import { expect, test as base } from '@playwright/test' import { deterministicUUIDPrefix } from './deterministic-ids' +import { visualClock } from './visual-fixtures' function deterministicInitScript(uuidPrefix: string): string { return String.raw` (() => { const uuidPrefix = ${JSON.stringify(uuidPrefix)} - const fixedNow = Date.parse('2026-07-28T00:00:00.000Z') + const fixedNow = Date.parse(${JSON.stringify(visualClock)}) const NativeDate = Date class DeterministicDate extends NativeDate { constructor(...args) { diff --git a/web/e2e/visual-fixtures.test.ts b/web/e2e/visual-fixtures.test.ts new file mode 100644 index 00000000..bb626073 --- /dev/null +++ b/web/e2e/visual-fixtures.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest' + +import { + visualClock, + visualFixtureData, + visualLocales, + visualScenarios, + visualThemes, + visualViewports, +} from './visual-fixtures' + +describe('deterministic visual fixtures', () => { + it('freezes the approved scenario, viewport, theme, and locale matrix', () => { + expect(visualScenarios).toEqual([ + 'home-normal', + 'home-anomaly', + 'access-keys-long', + 'model-prices-mixed', + 'settings-dirty', + 'usage-quality', + 'logs-signal-path', + ]) + expect(visualViewports).toEqual([ + { width: 375, height: 812 }, + { width: 768, height: 900 }, + { width: 1024, height: 900 }, + { width: 1440, height: 900 }, + ]) + expect(visualThemes).toEqual(['light', 'dark']) + expect(visualLocales).toEqual(['en-US', 'zh-CN']) + }) + + it('uses stable public-safe values without plaintext credentials', () => { + expect(visualClock).toBe('2026-07-29T00:00:00.000Z') + expect(visualFixtureData.requestId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, + ) + expect(visualFixtureData.accessKeySuffix).toMatch(/^[0-9a-f]{4}$/) + expect(visualFixtureData.longIdentifier.length).toBeGreaterThan(96) + + const serialized = JSON.stringify(visualFixtureData) + expect(serialized).not.toMatch(/sk-gl-[0-9a-z]{16,}/i) + expect(serialized).not.toContain('Bearer ') + }) +}) diff --git a/web/e2e/visual-fixtures.ts b/web/e2e/visual-fixtures.ts new file mode 100644 index 00000000..027f1179 --- /dev/null +++ b/web/e2e/visual-fixtures.ts @@ -0,0 +1,34 @@ +export const visualFixtureVersion = 1 +export const visualClock = '2026-07-29T00:00:00.000Z' + +export const visualScenarios = [ + 'home-normal', + 'home-anomaly', + 'access-keys-long', + 'model-prices-mixed', + 'settings-dirty', + 'usage-quality', + 'logs-signal-path', +] as const + +export type VisualScenario = (typeof visualScenarios)[number] + +export const visualViewports = [ + { width: 375, height: 812 }, + { width: 768, height: 900 }, + { width: 1024, height: 900 }, + { width: 1440, height: 900 }, +] as const + +export const visualThemes = ['light', 'dark'] as const +export const visualLocales = ['en-US', 'zh-CN'] as const + +export const visualFixtureData = Object.freeze({ + requestId: '5297f18d-3ca7-42d9-8a5b-2ce08ccd1b01', + accessKeySuffix: 'a11e', + groupName: 'Visual Fixture OpenAI Group', + modelName: 'visual-fixture-model-with-a-stable-long-identifier', + longIdentifier: + 'visual-fixture-client-segment-segment-segment-segment-segment-segment-segment-segment-segment-segment-segment-segment', + observedAt: visualClock, +})